public_repo
权限,这个权限包括读写用户的所有公开仓库……
即使我不会拿来做坏事,但是万一用户的登录令牌泄漏,风险还是很大的,而且这里存在一个信任问题,用户凭什么会相信你不会做坏事呢?所以我毅然决定迁移到 GitHub App。
GitHub OAuth App 是 GitHub 官方提供的一种授权方式,它可以让用户以自己的身份登录到第三方应用并进行相关操作,很多评论系统目前都是基于这种方式实现的。
GitHub App 其实和 OAuth 一样,也是用来给用户授权的,但是中间多了一层,也就是 App 本身。针对评论系统而言,用 Bot 来描述 GitHub App 可能会更加准确一些,它像是一个在用户和第三方应用(评论系统)之间传话的人,用户需要做什么事情可以先通过 App 来告诉第三方应用,然后第三方应用再执行对应的操作,并且我们可以选择这个操作,它代表的是用户还是 App 自身(做自动化的时候很有用)。最重要的一点是,GitHub App 可以选择超细粒度的权限,这样无论是对用户还是对第三方应用来说,都是更加安全的交互方式。
还有,相比较 OAuth 的一次性授权,GitHub Apps 其实是两段式授权,第一段是用户授权,这个没有什么区别,第二段是 App 的安装授权,针对评论系统而言,如果我希望 App 能够代表某一个用户对我某一个代码仓库的 Issues 进行操作,那么这个 App 就必须安装我的 GitHub 账号内,安装成功之后的 GitHub App 才有权限操作。而对于整个评论系统来说,我们甚至都不需要用户的 Issues 权限,只需要验证用户来自 GitHub 即可,极大程度上减少了用户的隐私风险。
进入创建 App 的页面,和创建 OAuth App 一样,我们需要填写 App 名称、App 主页地址和一个回调地址。不同的是,我们多了一个「Expire user authorization tokens」的选项,如果勾选的话,GitHub 返回给我们的令牌会有一个 8 小时的有效期,如果需要续期则需要用refresh_token
进行续期,由于我们的评论系统敏感度不高,这里可以不勾选,这样我们的拿到令牌就会一直有效。这一步在 OAuth App 中是没有的,有得选总比没得选好!
再往下走,我们会看到一个选择权限的区域,这里列出了大概三四十个权限,而我们只需要选择「Repository permissions」中的「Issues」读写权限即可。注意了,这里的权限设置的是 GitHub App 对于你自己 GitHub 账号的权限,而不是用户的权限,在创建 App 成功之后,你需要安装这个应用到你自己的账号中,这样你的 GitHub App 才有权限操作你的仓库。
表单的最后,你可以选择当前 GitHub App 可以被谁安装,因为我们只是一个评论系统,所以选择「Only on this account」即可。
创建完成之后,GitHub 会提示你需要创建一个私钥,这一步其实可以忽略,因为我们的 App 需要以用户身份进行操作,私钥是在以 App 身份进行操作时生成 JWT 用的。
在创建 GitHub App 之后,我们需要将这个 App 安装到我们自己的 GitHub 账号中,这样我们的 GitHub App 才有权限操作我们的仓库。在 App 的设置页面,我们可以看到一个「Install App」的按钮,点击之后,会跳转到一个安装页面,这里会列出所有的仓库,我们可以选择需要安装的仓库,这里我们只需要选择我们的博客仓库即可。
至此,我们的 GitHub App 就配置完成了,接下来我们需要修改评论系统的代码,让它能够对接此 GitHub App 进行操作。
在 OAuth 和 GitHub App 中,我们的授权地址都是https://github.com/login/oauth/authorize
,但是 GitHub App 没有 scope
参数,因为我们的权限已经提前定义好了,其他参数可以保持不变,具体的可以参考官方文档。
再往后的逻辑就基本上一致了,用户点击授权之后,GitHub 会带着code
参数重定向到你的回调地址,拿着code
换取令牌即可。
至此,迁移结束。
其实迁移的工作量真的很少,但是这里要吐槽一下 GitHub 的文档:场面话一堆,重点过于分散,时间全都花在了磕文档上。
希望这篇文章能够帮助到你,如果有什么问题,欢迎在评论区留言。
相关博客:
]]>There’s a simple secret to building a faster website — just ship less.
<p style="text-align: center"> <svg style="display: inline-block" height="40" viewBox="0 0 85 107" class="ml-1" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M27.5893 91.1365C22.7555 86.7178 21.3443 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4374 78.2199 52.0358 77.7822 59.2231 74.2459C60.0453 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.3179 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6844 102.089C39.2086 99.8193 38.5065 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5893 91.1365Z" fill="currentColor"/> <path d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z" fill="currentColor"/> </svg> </p>
说白了,Astro 就是一个模版引擎,这不是什么新鲜玩意儿,在 PHP 称霸 Web 开发的年代,我们有 Smarty,进入 NodeJs 时代之后我们有 EJS、Pug 等基于 JS 的模版引擎。这些模版引擎所做的事情无非就是把页面代码模块化,最后编译成静态文件,这样的应用维护成本相对较低,且成品能够提供很好的搜索引擎优化能力,爬虫能够非常容易获取网站内容,页面的性能也会比单页面应用要更好,因为省去了客户端渲染步骤。市面上有那么多模版引擎,为什么选 Astro 呢?很简单的一个原因,它能集成 Vue/React 组件,并最终编译为静态。
既然是换博客引擎,那一定少不了折腾,之前从 Jekyll 转到 Hexo 其实还算是比较无痛的,但是从 Hexo 转到 Astro 就有点蛋疼了,因为 Astro 的定位并不是一个博客引擎,而是模版引擎,意味着 Astro 在代码层面其实更加靠底层,所以大部分博客的功能需要手动实现,新站点倒还好,老站点如果需要实现向下兼容,还真不是一件容易的事情。
我之前用的链接结构长这样:
:year/:month/:day/:title.html
但是我的 markdown 文件是平铺的:
:year-:month-:day-:title.md
而 Astro 使用的是基于文件夹的路由,如果需要达成和 Hexo 一样的效果,最先进我脑子的想法就是把 markdown 文件拆分到对应的 Astro 文件夹(路由)中去,这个想法也仅仅是存在了 3 秒,有没有一种更聪明的做法呢?有!Astro 提供了动态路由功能,和 Vue 的路由一样,对于同种类型的链接,我们只需要定义一遍路由即可,不同的是,Astro 路由的定义需要文件夹结构和名称来完成:
src
└── pages
└── [year]
└── [month]
└── [date]
└── [slug].astro
中括号里面的字符就是路由变量,我们可以在全局变量Astro.params
中获取到,而针对任何动态路由,Astro 在文档中也明确指出了,需要导出一个getStaticPaths
方法,因为 Astro 是一个静态网站生成器,它需要明确知道,动态路由中每一个变量都存在哪些值。
---
// src/pages/[year]/[month]/[date]/[slug].astro
import {getCollection, getEntry} from 'astro:content'
export async function getStaticPaths() {
const allPosts = await getCollection('posts')
return allPosts.map(post => {
let date_str = post.slug
let date_array = date_str.split('-')
let year = date_array[0]
let month = date_array[1]
let day = date_array[2]
let postPath = post.slug.slice(11)
return {
params: {
slug: postPath,
year: year,
month: month,
date: day
},
props: {
entry: post
}
}
})
}
const entry = await getEntry('posts', Astro.props.entry.slug)
const {Content} = await entry.render()
---
<Content/>
里面 .astro
组件的代码就是这样的,我们这边用到了一个getCollection
和
getEntry
的方法,我们暂时先放一旁,后面会讲到。
在用 Hexo 写博客的时候,我们可以很方面的在 Markdown 里面加上 tags 数组来告诉 Hexo 这篇文章属于哪些分类,或者说拥有哪些标签,这对于 SEO 来说也是非常有帮助的,Astro 默认没有这个功能,但是实现起来不困难,还是动态路由。我们需要在pages
文件夹下创建一个新的文件夹叫tags
,然后再在这个文件夹下创建一个[tag].astro
的文件,这个文件就是我们的分类页面,然后我们需要在[tag].astro
里面写入下面的代码:
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
let allPosts = await getCollection('posts')
let allTags = []
allPosts.map(post => {
allTags = allTags.concat(post.data.tags)
})
return allTags.map(tag => {
return {
params: {
tag: tag,
}
}
})
}
let allPosts = await getCollection('posts')
// 获取当前 tag
const currentTag = Astro.params.tag
---
{
// allPosts.filter ...
// allPosts.map ...
// 你的列表实现,jsx语法
}
这一步和上一步的代码基本相同,只是这里我们需要先获取所有的 tag,然后再根据当前 tag 来过滤文章,最后渲染出列表。
Markdown 文件如果直接放在src/pages
下面,你的链接结构会变得非常简单,就是domain.com/:title
,也不会需要动态路由,你的 Markdown 文件名就是你的路由。按照 Astro 官网的描述,任何 src/pages
下被支持的文件(.astro
.md
.mdx
.html
.js
)都会被编译到路由中去。所以自然而然的,我也把之前的 Markdown 文件全都放到了 pages 里面,但是文章的链接结构会变成这样:
:year-:month-:day-:title
如果我需要保持原有的链接结构,我就需要把 Markdown 文件放到src/content/posts
下面,然后再用getCollection
和getEntry
来获取文章内容。需要注意的是,这里我们多了一个 posts 文件夹,按照官网的描述,这个文件夹可以看作是一个集合,你可以定义多个集合比如门户、新闻或者博客,这里我们只需要 posts 一个文件夹用来放文章就好了。不过,要让这个集合能被正常使用,我们还需要在src/content
下面创建一个config.js
来定义我们这个集合的行为。
import { z, defineCollection } from 'astro:content'
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
tags: z.array(z.string()).optional(),
deprecated: z.boolean().optional(),
draft: z.boolean().optional()
})
})
export const collections = {
posts: blogCollection
}
我们用了defineCollection
这个方法来定义集合中每一篇博客返回的数据(Markdown 中的 Frontmatter),这样我们才可以使用getCollection
和getEntry
来获取文章内容了。
const allPosts = await getCollection('posts')
const entry = await getEntry('posts', slug)
前面我们了解到,Astro 支持将 pages 文件夹中的文件转换为路由,其中就包括 js,官方把它叫做 Endpoints,通俗来讲的话,其实就是接口。
我们可以先来看一下这个 js 文件的结构:
export async function GET(context) {
let req = context.request
return new Response('Hello World!', {
headers: {
'content-type': 'text/plain'
}
})
}
这个文件的结构和 Serverless Functions 非常类似,我们可以在这个文件里面写入我们的逻辑,然后返回一个 Response 对象,在这里我们返回了一个简单的字符串,同样,我们也可以返回一个 XML 文件,这样就可以生成 RSS Feed 了。
// src/pages/atom.xml.js
import { getCollection } from 'astro:content'
export async function GET(context) {
let allPosts = await getCollection('posts')
let atom = `<feed xmlns="http://www.w3.org/2005/Atom">
<title>一个球的博客</title>
<link href="https://jw1.dev/"/>
<id>https://jw1.dev/</id>`
allPosts.map((post, index) => {
let { date, deprecated, draft } = post.data
// filter...
// sort...
// form other data...
atom += `<entry>
your stuff here
</entry>`
})
atom += `</feed>`
return new Response(atom, {
headers: {
// important
'content-type': 'application/xml;charset=UTF-8'
}
})
}
最激动人心的功能就是这个了,Astro 可以使用 MDX(JSX in Markdown)作为文章入口,而 MDX 中可以混用 7 种前端框架的组件。
在使用之前我们需要先安装 Vue 和 MDX 依赖:
npx astro add vue
npx astro add mdx
现在我们就可以在 MDX 文件中使用 Vue 组件了,如果需要在已有的 Markdown 文件中使用的话,我们只需要修改文件后缀为.mdx
就可以了。
---
layout: ../../layouts/post-layout.astro
title: My Awesome Post
---
import AppAudio from '../../components/app-audio.vue'
<AppAudio src="remote audio file" client:only />
需要注意的是,在 MDX 中传入 props 参数的时候,我们需要遵循 JSX 的语法,而不是 Vue 的语法。
图片懒加载
我需要一个能在img
标签上插入loading="lazy"
的机制,理想情况下,对于新博客而言,我推荐你们全部使用 MDX 作为默认的书写介质,这样就可以使用 Astro 自带的<Image/>
组件,这个组件默认就会带上loading="lazy"
属性,而且你也可以开发和使用自己的组件,一张图能被玩出花儿来,但是对于老博客迁移过来的 .md
文件,一个个修改的话多少还是有点不太现实,虽然可以用 remark 插件来做 post editing(我查到的实现方式),但是牺牲了图片的alt
属性,这并不是我想要的。
生成的链接并不包含.html
后缀
如果你之前是用的是 Hexo,那么你的链接结构可能是这样的:domain.com/:year/:month/:day/:title.html
,但是 Astro 生成的结构其实是这样的:domain.com/:year/:month/:day/:title/index.html
,要解决这个问题其实也很简单,我们在src/pages/[year]/[month]/[date]/
中新增一个名为[slug].html.astro
的文件即可,你可以完全复制[slug].astro
的逻辑,或者用来重定向,重定向的写法如下:
---
import {getCollection} from 'astro:content'
export async function getStaticPaths() {
// 与 `[slug].astro` 一致
}
let {year, month, date, slug} = Astro.params
---
<meta
http-equiv="refresh"
content={`0; url=/${year}/${month}/${date}/${slug}`}
>
<video src="https://blog-r2.jw1.dev/3b8wzvg9E4_tDvUx.mp4" preload="none" playsinline muted loop controls></video>
今天就来浅浅的认识一下傅里叶变换吧!(着实没想到本数学渣也会有今天)
注意,这篇博客不会深究傅里叶变换的具体细节,相反,我会用两种不同的方式试着去解释傅里叶变换在音频数据转换方面究竟做了什么事情。
按照维基百科的定义,傅里叶变换是一种线性积分变换,用于信号在时域和频域之间的变换。
线性积分变换我们先放一旁,不用着急去理解(着急也没用),我们可以把注意力放在「时域」和「频域」这两个词上面,翻译成英语的话,一个叫「Time Domain」,另一个叫「Frequency Domain」。作为一个音乐制作人,这两个东西在我们这里犹如吃饭和喝水,是的,时域所表达的数据就是声音本身,也就是「波形」:
<video preload="none" src="https://blog-r2.jw1.dev/time_domain.mp4" playsinline muted loop controls></video>
而频域,相信你也猜到了,他所表达的数据,正是频率:
<video preload="none" src="https://blog-r2.jw1.dev/lIBMdb_F5As3-nav.mp4" playsinline muted loop controls></video>
而我好奇的是,这两种数据,究竟是如何转换的呢?先来看看它们之间的关联,这是一个 440Hz 的正弦波在时域和频域上的表现:
可以看到,时域数据由 440Hz 的正弦波上下振动构成,我们将这个时间段内产生的振动想象成一个矩形。而频域数据则表现为一条有凸起的线,如果这时候我们再去加一个 2000Hz 的正弦波,这时候再去看这两个数据,会是什么样的呢?
这时候的时域数据得到了增益,我们可以认为先前想象的矩形的高度变大了,且产生的波形也不再是一个单一频率的正弦波,而是同时包含 2 个频率的波形。这时候再来看右侧频域数据,和我们期望的一样,一条线上有两个峰值,一个在 440Hz,一个在 2000Hz。
为了更加容易理解这种数据上的变化,我们可以想象一个三维空间,我们以频域这条线延伸出一个面,这样我们就得到了一张「纸」,一个时间单位内的信号变化会让这张纸同时在时域(X)轴和频域(Y)轴以及振幅(Z)轴产生变化,如果我们从 X 轴向原点看去,我们会看到纸张的一侧在二维平面投影出了一条线,也就是频域(Y 轴),如果我们从 Y 轴向原点看去,我们会看到纸张在二维平面投影出一个矩形,也就是时域(X 轴)。傅里叶变换所做的事情无异于改变了观看数据的视角,不过现实情况中,这种转换会稍微复杂那么 🤏 亿点点。
<video preload="none" src="https://blog-r2.jw1.dev/neA4FgWFaGEolIIp.mp4" playsinline muted loop controls></video>
抛开那些让人头疼的数学公式,我们先来捋一捋目前我们都知道些什么。
由此三条,我们不妨做出一个大胆的假设,我们使用与原始信号同样位数的 1Hz 到 20000Hz 的正弦波依次与原始数据相乘得到点积并计算出 Y 轴所有点积的平均值(相位)并记录,由于信号的重叠,一部分发生了增益反应,一部分发生了抵消反应,如果我们把记录下来的平均相位值进行记录,你会发现,我们得到了频域数据。
这里我多次提到了「相位」这个词,为了方便理解,我们可以认为在直角坐标系中的相位是一段波形在 X 轴上的起点,而在极坐标中,相位代表了起始角度,同样的两条波形发生重叠后,如果相位一致,那么合成的波形能量会乘 2,相位相反则能量互相抵消为 0 。
我们可以做一个小工具来验证这个猜想,先在 48KHz 的采样率下生成一段 512 位 440Hz 的正弦波。
先从 1Hz 的频率开始对比,直到 20000Hz,我们来看看记录下的图形:
<video preload="none" src="https://blog-r2.jw1.dev/GJPt7TrezLFnTvRx.mp4" playsinline muted loop controls></video>
由此我们得到了一张频谱图,且有一个峰值,这个峰值所在的地方就是 440Hz 在 0 - 20000Hz 所处的位置,为了验证该想法的确实成立,我们试试看在同样的采样率下生成一段包含两个不同频率的正弦波形(1000 + 2000Hz),再放进图表中进行计算看看:
记录下的数据完美的展示出两个峰值,想法成立!计算峰值频率也很简单,哪个正弦波能触发最高相位,那么波形所在频率就一定是这个正弦波的频率,而且这个方法在理论上的分析精确度可以做到比傅里叶变换要高,但是缺点也是有的:
我们再来想象另一种方法,假设目前给定的时域数据中包含 4 个完全一致且周期完整的正弦波,这时候的波形平均相位是 0,我们把最后一个正弦波拆解并放置在 X 轴的起始位,你会发现这时候的相位提升了一点点,因为有一个正弦波得到了增益,如果我们再拆一个正弦波出来放在 X 轴的起点,把之前拆出来正弦波向后移动一个周期,这时候你会发现总体相位平均值又上升了一级,如果我们把 4 个正弦波全部叠加在一起,至此,我们从原始波形中拆解出来的波形能够组成的最高相位出现了!
这样想象还不够直观,我们还是来做一个小工具,并且在上面的描述中做一个小改动,我们把之前使用的直角坐标系换成极坐标系,采样率 48KHz 的情况下生成一段 375Hz 的正弦波,放进图表里看看:
<video preload="none" src="https://blog-r2.jw1.dev/Ie2Z3GTnFhXNyJoI.mp4" playsinline muted loop controls></video>
在极坐标中,X 轴由线变成了点,Y 轴也失去了负数象限,于是 4 个正弦波在极坐标上均匀的绘制出了 8 个叶片。想象这样一个图形有一定的质量,在我们将数据从尾部一点点提取到头部的时候,图形的质心一定会发生变化。
如果我们在移动数据的时候监测质心的变化会发生什么呢?
<video preload="none" src="https://blog-r2.jw1.dev/RVpGMhV8MD_KYlmb.mp4" playsinline muted loop controls></video>
可以看到,当线条在某一方向更加集中时,质心离原点距离越远,当所有的正弦波在某个方向上完全重叠时,记录表上会出现一个峰值,这就可以理解为是当前波形中最突出的频率。
如果我们需要计算峰值频率,则需要代入采样率和数据节点长度以及当前转动的圈数,最后我们对 X 轴数据进行重新分布,则会得到以下结果:
<video preload="none" src="https://blog-r2.jw1.dev/e5hcP5iopg1ozzAf.mp4" playsinline muted loop controls></video>
经过计算之后可得,峰值频率为 375Hz,与我们生成的正弦波频率完全一致!这个方法这么精确的吗?再生成一段 15017Hz 的正弦波试试:
计算得出峰值频率为 16000Hz,误差还是有点大的,这是因为在对比中,样本数据的每一次提前操作,对比圈数都是指数递增的,这会导致我们在记录低频时的信息密度比高频的大很多,如果我们像之前正弦波扫描那样提升对比圈数的精度,那么最后我们也可以得到一幅非常精准的频谱图。
真正的傅里叶变换其实可以想象为两个方法的结合,我们可以看下面的代码(https://gist.github.com/anonymous/129d477ddb1c8025c9ac):
Fourier.Transform = function (data) {
var N = data.length;
var frequencies = [];
// for every frequency...
for (var freq = 0; freq < N; freq++) {
var re = 0;
var im = 0;
// for every point in time...
for (var t = 0; t < N; t++) {
// Spin the signal _backwards_ at each frequency (as radians/s, not Hertz)
var rate = -1 * (2 * Math.PI) * freq;
// How far around the circle have we gone at time=t?
var time = t / N;
var distance = rate * time;
// datapoint * e^(-i*2*pi*f) is complex, store each part
var re_part = data[t] * Math.cos(distance);
var im_part = data[t] * Math.sin(distance);
// add this data point's contribution
re += re_part;
im += im_part;
}
// Close to zero? You're zero.
if (Math.abs(re) < 1e-10) {
re = 0;
}
if (Math.abs(im) < 1e-10) {
im = 0;
}
// Average contribution at this frequency
re = re / N;
im = im / N;
frequencies[freq] = {
re: re,
im: im,
freq: freq,
amp: Math.sqrt(re * re + im * im),
phase: (Math.atan2(im, re) * 180) / Math.PI, // in degrees
};
}
return frequencies;
};
第一次循环相当于是在生成整个频谱图上所需要的频率用于对比,第二次循环则是针对原始波形进行解构,只不过这种解构是在复数平面(由实数与虚数构成的坐标体系)完成的,而复数平面可以理解为极坐标与直角坐标的结合体。傅里叶变换还保留了声音信号的相位信息,这对于重构时域信息是非常重要的。我们来看看一个标准的傅里叶变换所产出的 440Hz 频谱图吧!
可以看到,这张图和我们使用正弦波扫描得到的图几乎一模一样!
在理解傅里叶变换的同时,也有了一些意外收获:
好了,关于傅里叶变换就说到这里,再次感叹,学海无涯啊!
最后,感谢 3Blue1Brown 的视频和 Better Explained 的博客:
文本向量化最直观的表现就是,输入一段文字,返回一串数字,是把信息从直观转为抽象的过程。文本向量化的传统做法一般分为四步:
凭心而论,以上任何一步放在以前都会让我想死,但是今时不同往日,我们有现成的向量化大模型,而我们需要做的只是调用一下 API 而已。
如果说要在 js 里表现的话,就是这样:
// 这是一条向量化数据
const vectorizedData = [
-0.006929283495992422,
-0.005336422007530928,
// ...
-4.547132266452536e-05,
-0.024047505110502243
]
如果是 OpenAI text-embedding-ada-002
模型输出的向量数据,那么这个数组将会有 1536 条数据,或者说,维度。维度是啥?维度就是之前我们在编码中提到过的词属性,但是实际应用中,大模型生成的向量数据还包含很多其他维度的属性。
根据 OpenAI 的描述,向量化之后的数据大概可以做:
以下是我简化后的版本:
个人认为向量化的数据其实只有这两个功能,什么聚类、推荐、异常检测等等,都是数据分类后的自然而然得到的能力,而我们要做的就是计算向量数据的相似度。那我们应该要怎么计算相似度呢?
最常见的方式应该就是计算余弦相似度了,计算步骤为:
用代码来写的话就是这样:
// 点积
function dotProduct(vec1, vec2) {
let product = 0;
for (let i = 0; i < vec1.length; i++) {
product += vec1[i] * vec2[i];
}
return product;
}
// 模长
function magnitude(vec) {
let sumOfSquares = 0;
for (let i = 0; i < vec.length; i++) {
sumOfSquares += vec[i] * vec[i];
}
return Math.sqrt(sumOfSquares);
}
// 余弦相似度
function cosineSimilarity(vec1, vec2) {
const dotProd = dotProduct(vec1, vec2);
const mag1 = magnitude(vec1);
const mag2 = magnitude(vec2);
if (mag1 === 0 || mag2 === 0) {
return 0;
}
return dotProd / (mag1 * mag2);
}
假设我们现在向量化三个句子:
今天天气怎么样
今天天气如何
这苹果真好吃
套用余弦相似度方法来检查第一句和第二句的相似度,我们会得到值 0.9717939245504936
,这表示第一句和第二句相似度为 97%,再来看看第一句和第三句的相似度,我们会得到 0.7795850235231011
,可以看到,这个算法基本上算是成立的。
今天天气怎么样,对比,今天天气如何 0.9717939245504936
今天天气怎么样,对比,这苹果真好吃 0.7795850235231011
余弦相似度算法的特点在于,两个向量数据的高维数据,也就是说文本,长度不一定需要相近也能得到很高的相似度。打个比方,“苹果好吃” 和 “隔壁又开了一家苹果店,我早上起床去买了一斤,还挺甜的” 可能会得到一个相对于其他算法更高的相似度,因为两者在语义上描述的主体一致。
欧式距离也可以叫欧几里德距离,计算步骤为:
代码这样写:
function euclideanDistance(vec1, vec2) {
var sum = 0;
for (var i = 0; i < vec1.length; i++) {
sum += Math.pow(vec1[i] - vec2[i], 2);
}
return Math.sqrt(sum);
}
还是代入之前的数据我们来看一下结果:
今天天气怎么样,对比,今天天气如何 0.23751243483566573
今天天气怎么样,对比,这苹果真好吃 0.6639503107502986
情况好像有些不对?为什么第一个对比得到的相似度会比第二个对比要少?
因为这里我们计算的是欧几里德距离,距离越近越相似,所以这个算法依旧成立。我们可以用 1 / (1 + distance)
来获取一个 0 - 1 之间的数值,这个数值就是相似度了。
function euclideanDistance(vec1, vec2) {
var sum = 0;
for (var i = 0; i < vec1.length; i++) {
sum += Math.pow(vec1[i] - vec2[i], 2);
}
let distance = Math.sqrt(sum)
return 1 / (1 + distance);
}
使用经过修改的代码再次代入数据进行计算会输出:
今天天气怎么样,对比,今天天气如何 0.8080726882819518
今天天气怎么样,对比,这苹果真好吃 0.6009794845070139
可以看到,文本长度和语义对结果的影响更大了,欧几里德距离算法的特点就是如此,如果原始文本长度差异较大,就可能导致计算出来的相似度越小。在应用方面,这种算法可以用来计算图片的相似度,两张图片通过一定的预处理可以得到相同维度的像素信息,通过对比各像素点在欧几里德空间上的距离就可以计算出图片的相似度,也可以用在推荐系统中充当某一权重,计算用户针对某一项产品的兴趣程度等等。
得,越来越离谱了,这是个啥玩意儿?
我为啥会知道这个东西?因为 OpenAI 关于 Embeddings 的官方文档里有写,它是一种分类算法,可以将向量数据转换成 2 维或者 3 维的点,个人理解,这也是一种相似度算法,计算结果趋于把数据集结成簇(cluster),但是计算步骤对比前面两个算法会稍微有那么亿点点复杂,因为涉及到很多我也不是很懂的专业词汇,在这里就不放了,但是我们有现成的 npm 库:https://www.npmjs.com/package/tsne-js 🤝,t-SNE 可以将向量数据在 2 维平面或 3 维空间可视化,能够让我们更好的理解机器是怎么样看数据的。
那么具体表现是什么呢?
好了,科学时间到!
一开始我用 9 个句子生成了 9 条不同的向量数据,每 3 个句子中分别包含一个统一话题(天气,水果,手机),然而计算后的数据表现却不尽人意:
是不是样本太少了?但是上哪找那么多提前分好类的数据呢?直到我看到了这个:https://github.com/ultralytics/mnist
虽然他的数据格式我不能直接拿来用,但是对我启发很大,直接自己造了一个小工具,一点一点手动收集手绘的数字信息。
最后做出来的样本长这样:
由于我们的手绘数据都是由 0 和 1 组成,可以看作是一系列布尔向量,t-SNE 算法使用的参数如下:
{
dim: 2,
perplexity: 5,
earlyExaggeration: 4.0,
learningRate: 100.0,
nIter: 500,
metric: 'dice'
}
生成的图表如下:
我们大概可以看出,大部分相同的数字都能集结成簇,但是需要注意的是,这里每个节点的颜色都是我们手动预分配的而不是计算出来的,如果需要聚类计算我们还需要 k-means 算法,经过 k-means 算法聚类后重新分配颜色可以得出下图:
现在我们看到的分类才是算法认为的分类,可以看到第二第三象限中粉色的一簇多由 3、5、8 组成,这表示 t-SNE 算法认为,这些数字在一定程序上有很大的相似度,我知道这对一些朋友来讲可能会觉得荒谬,因为对于人类来说,辨别阿拉伯数字的技能早已刻进大脑,但是对于机器而言,他们可能只学了几百毫秒就要给出结果,如果能够给予足够的迭代次数和庞大的样本数量,机器可以比人更精准,事实上,这就是机器学习的核心。
好了,扯远了,现在讲讲怎么应用在相似度检测上,假设你现在拥有一堆向量数据,如果我们要进行搜索,则必然会先提供搜索词,我们可以将搜索词也向量化,然后一起进行 t-SNE 运算,最后我们找到关键词所在的簇,这个时候我们可以认为,这个簇除了关键词本身的所有项就是与关键词最相似的向量数据,但是缺点也是有的,k-means 生成的簇的长度不可控,可能会有一个簇中只有关键词一个向量数据的情况。
以上这些只是向量数据相似度计算中一些比较常见的算法,在不同的应用场景中会有各种各样的相似度算法,机器学习的世界真的会让人感受到什么是学海无涯,哪怕只是其中一小片的概念就有这么多门道。作为一个普通的前端开发者,我只能说,非常荣幸能够生活在这样一个时代。
好了,今天就说到这里,拜拜👋
]]>2023 年 9 月 8 日,Bun.js 发出了第一个稳定版本:Bun 1.0
Bun 1.0 is finally here.
Bun is a fast, all-in-one toolkit for running, building, testing, and debugging JavaScript and TypeScript, from a single file to a full-stack application. Today, Bun is stable and production-ready.
…
Bun is a drop-in replacement for Node.js. That means existing Node.js applications and npm packages just work in Bun.
根据官网的描述来看,Bun 是一个多合一的 JS 工具箱,其中包括运行、构建和调试 JS 和 TS 代码,也就是和 NodeJS、Deno 一样,同样属于 JS 的运行时,不过 NodeJS 已称霸江湖多时,而 Deno 却一直不温不火,就在我们以为 NodeJs 将会一直稳坐王位的时候,前几天这个突然印入眼帘的家伙,Bun,貌似在圈中产生了巨大的讨论,目前看下来,究其原因,一个是基本完全兼容 NodeJs,官方用词为“drop-in”,我在这里翻译为“无脑”,是的,无脑替换 NodeJs。还有一个就是讨论声最大的——性能。
首先我能想到最常见的场景就是 API serving,通过 NodeJs 的 http 模块和 Bun 的 Serve 模块,我做了两个功能一摸一样的 API,GET 的时候返回“Hello World”:
// NodeJS
import http from 'http'
const server = http.createServer((req, res) => {
res.end('Hello World')
})
server.listen(7900, 'localhost', () => {
console.log('NodeJs server started at http://localhost:7900')
})
// Bun
Bun.serve({
fetch(req) {
return new Response('Hello world')
},
port: 7901
})
console.log('Bun server started at http://localhost:7901')
两边写法不是很一样,但是基本上都用很短的代码就完成了一个简单的 API,那么测试的代码我们这样写:
let test_subject = 'Node.js' // 或 Bun
let endpoint = 'http://localhost:7900' // 或 7901
// 两次测试以下代码完全一样
let count = 1000
let start = Date.now()
let err_count = 0
console.log('\n========')
console.log(`${count} hello-world api running through ${test_subject}`)
while (count > 0) {
let res = await fetch(endpoint)
await res.text()
count--
}
let end = Date.now()
console.log(`Took ${end - start}ms.`)
console.log(`Got ${err_count} errors.`)
console.log('========\n')
NodeJs 这边三次测试,每次调用 1000 次 API,平均大概在 300ms 左右。再来看看 Bun 这边:
基本维持在 80ms 左右,也就是说,在这个测试中,Bun 的速度比 NodeJS 快大概 3.75 倍。
在 API 测试中我们看到,Bun 的速度确实是要比 NodeJs 快的,现在我们再来看看加密的速度是不是有区别:
import AES from 'crypto-js/aes.js'
let test_subject = 'Bun.js' // 或 NodeJs
// 两次测试以下代码完全一样
let count = 10000
let start = Date.now()
console.log('\n========')
console.log(`${count} AES encryptions running through ${test_subject}`)
while (count > 0) {
let ciphertext = AES.encrypt('my message', 'secret key 123').toString()
count--
}
let end = Date.now()
console.log(`Took ${end - start}ms.`)
console.log('========\n')
NodeJs 这边 1 万次 AES 加密消耗时间大约在 650ms,再来看看 Bun 这边:
基本在 140ms 左右,在以上测试中我们也看到了,Bun 的速度确实是比 NodeJS 要快的,难道 NodeJs 真的就是被完爆吗?
只能说,不完全是。
我们使用程序随机生成两个数字 a 和 b,再去计算 a 的 b 平方,分别计算 1 亿次。
let a = Math.random()
let b = Math.random()
let start = Date.now()
let count = 100000000
console.log('\n========')
console.log(`${count} Math.pow() running through NodeJs`)
while (count > 0) {
Math.pow(a, b)
count--
}
let end = Date.now()
console.log(`Took ${end - start}ms.`)
console.log('========\n')
运行之后来看看 NodeJs 这边的结果:
基本维持在 60ms,再来看看 Bun:
Bun 居然还比 NodeJs 慢了 40ms。
🥲
怎么说呢,以我的智商想要给你们解释这件事情……还是太难为我了,我猜大概率还是因为底层 JS 引擎不同导致的(Bun 使用 Safari 的 Js core,NodeJs 使用 Chrome 的 V8),不过 1 亿次计算,40ms 的差距貌似在生产环境中看起来也不是那么明显,毕竟是第一个稳定版本嘛,相信后面 Bun 团队应该会解决的!
就在我基本拥抱 Serverless 的时候,Bun 出现了,一开始我会觉得它的出现有点不是时候,但是亲自上手之后,可以说 Bun 又激起了我写 Server-ful App 的兴趣,不仅仅因为性能,更加是因为 Bun 的开发体验,很多经过重构的 API 简直美的不像话!
<br/>
读取文件:
const file = Bun.file('package.json')
const contents = await file.text()
<br/>
WebSocket:
Bun.serve({
fetch() { ...
},
websocket: {
open(ws) { ...
},
message(ws, data) { ...
},
close(ws, code, reason) { ...
},
},
});
综合来看,Bun 确实是比 NodeJs 要快的,而且是呈倍数的那种快,再加上几乎完美继承 NodeJs 的生态,真的非常看好这个运行时,期待 Bun 团队后面再整大活!
]]><br>
「大疫将至」
2019 年末,Covid-19 新型冠状病毒开始肆虐。
<br>
「全面解封」
2022 年的最后一个月。解封后一直想着是不是应该写点什么,也确实写了一些东西,但是没有发出来,说实话,满屏的「操你妈」确实显得有些没文化。于是我决定让情绪沉淀一点时间,彻底忘记这件事,等到我能相对客观描述这件事的时候再去下笔,也就是你们现在所看到的文字。
2022 年 4 月的上海,相信大家应该都有印象,尤其作为一个亲身经历的人。不过说实话,封控对我的影响和其他人相比还是小了很多,我是程序员,我女朋友是 BD 运营,我们俩都可以远程工作,甚至 BD 运营受疫情的影响反而越来越吃香。看着很多人就医、吃饭、和工作上遇到的各种不便与不公,这时候的我才开始觉得,这是一场灾难,人为的灾难,我对这场灾难的厌恶情绪已经无法用语言来描述,我开始讨厌这一刀切的政策,开始讨厌这个城市,开始讨厌这个国家,甚至想去那天夜里的某条路上去参加抗议。后来通过警局的朋友知道,当天的活动确实有不少境外势力,但是这并不能抹去我对于封控的痛恨,那些声音依旧在我脑海里回荡。
2023 年,新年之始。很多人坐地铁已经不带口罩了,我就是那很多人中一个,利剑消散之时,相信每个人都是开心的,我们终于能回到以前的生活了,不是吗?也没有人会怀念天天测核酸的这段时间吧?对吧?
<br>
「关于技术」
技术方面发生了很多事情,也折腾了不少新玩意儿,说说变化的:
<br>
AI
关于沉淀情绪这件事情,我觉得我做的挺对,毕竟在风口浪尖上写出来的东西,最后大概率会被我标上“Deprecated”。GPT 类型的 AI 在今年的发展历程在很多人看来都可以用“昙花一现”来形容,最开始确实很惊艳,OpenAI 也一举成为了用户量增长最快的公司,不过后来很多人开始意识到,自己的生活里能用到 AI 的地方很少,这也是我所认为的 GPT 类型的 AI 目前的状态:高不成低不就。真要说没用吧,用他来查一些现有的技术文档也不错,Copilot 也已然成为月订的常驻成员,但是你要说这玩意儿能取代谁,我觉得目前它取代不了任何人。不过绘画类的 AI 倒确实取代了一部分人工,甚至取代了一部分商用图片提供商,目前已经看到很多视频/文字博主使用这些绘画模型来生成场景所需要的图片了,但是实际应用场景依然是非常有限。不过可以确实的是,今年这些大模型的产生绝对是 AI 发展的一个里程碑,也许在不久的将来我们真的能见到强人工智慧的诞生。
<br>
Serverless
2023 年 1 月,ChatGPT 进入人们的视野,可以说从那个时候 OpenAI 真正进入了快速成长期。而我,有幸跟上了时代的步伐,在 2 月份成功申请到了 API,开始了我的 ChatGPT 客户端开发之旅(没火,没吃到红利,生气),当中折腾最多的地方就是 Serverless Functions,从最开始的 AWS Lambda,到 Vercel Serverless/Edge Functions 以及 Cloudflare Workers,只能说真的学到了很多,后面有时间我会再写一篇博客来详细说说这些服务以及它们的优缺点。
<br>
Twitter/X
不知道马斯克怎么想的,但是事情就是这么发生了,推特变成了 X。我从 2016 年开始玩推特,当中甚至经历了三代女友 😅,可以说对这个社交平台有着很深的感情,然后这一切都被这个老哥的一句话给毁了。
警醒世人:不要给亿万富翁瞎出主意,因为他可能真的会去做!
<br>
「关于生活」
今年 8 月尾去安吉玩了两天,不愧是周末胜地,一个小溪都能有很多人,吃饭也是真的困难,第一天晚上想去吃的餐厅需要排队两个半小时,等 104 桌,果断选择放弃,去吃了一家没什么人的砂锅饭,便宜,也确实难吃。第二天总算是如愿了,两个人吃了 187 块钱,结账时还给抹了个零儿,180 块钱对于一家晚市需要排队两个半小时的店来讲算比较实惠的了吧。吃完饭去莫干山转了一圈,心满意足打道回府。
从年初至今,去了海南、南京、桐庐和安吉,还有上海一些没去过的地方,对我这样一个不喜欢到处跑的人来说,算是比较勤快了。出门游玩确实会让人感觉到很开心,哪怕什么事情都不做,只是坐着淌水都是心旷神怡,那就发点照片吧,你们自行感受,溜了!
<br>
安吉
<video src="https://blog-r2.jw1.dev/IMG_0038.MOV" controls muted style="margin: 0" preload="none" playsinline></video>
<br>
共青公园
<br>
桐庐
<br>
南京
<br>
海南
]]>在说 m42 的设计思路之前,假定情况如下:
m42 针对以上情况给出的设计思路:
localStorage
,并通过WebSocket
交换公钥。再来看看后端:
在实现上遇到的唯一问题就是如何实现客户端间互发消息,感谢 StackOverflow 给出的答案:
// 核心代码
const express = require('express')
const app = express()
const { WebSocketServer } = require('ws')
const authenticate = function (req, next) {
next(null, req.headers['sec-websocket-key'])
}
const wss = new WebSocketServer({
noServer: true
})
let lookup = {}
wss.on('connection', function connection(ws, req, client) {
lookup[client] = ws
lookup[client].send(
JSON.stringify({
clientID: client
})
)
})
const server = app.listen(port, host, () => {
console.log(`app is on, http://${host}:${port}`)
})
server.on('upgrade', function upgrade(request, socket, head) {
authenticate(request, function next(err, client) {
if (err || !client) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy()
return
}
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, client)
})
})
})
这样每次连接 WebSocket 都会获得一个独一无二的 ClientID,身份验证的问题就解决了。
下面重点来说说前端:
这一步并不难,只是我走了很多弯路。我最初的设计是在双方客户端各生成一段密码,然后通过 WebSocket 交换,这种做法在现在看来根本不能叫做端到端加密,因为它把密码明文传输了,即使 Alice 和 Bob 之后的消息都是加密过的,但是只要中间人拿到了一开始双方互换密码的请求,那么一切都是无用功。同时这也暴露了我对于信息传输加密方面的无知。
经过一番查阅后,我理解了为什么端到端加密中一定会有一个交换公钥的过程,注意是交换公钥,而不是交换密码。密钥对的作用在这里就显现出来了,公钥用于加密,而私钥用于解密,也可以理解为,用于加密的密码和解密的密码完全不一样。以目前家用电脑的算力来讲,强行破解的可能性几乎为零。
Web API 中已经支持了很多加密方式,m42 采用的是 RSA-OAEP-256。具体用法可以看这个Repo。
端到端加密的问题解决,但是在随后的开发中又遇到了新问题:RSA-OAEP-256 只能加密长度 190byte的数据,超出则报错。这个问题最后是通过切割 Blob 解决了。
// 将字符切割为数组
function splitAsChunk(size, str, cb) {
str = encodeURI(str)
let blob = new Blob([str], {
type: 'text/plain'
})
let splitSize = size || 150
let chunkArr = []
let loopTimes = Math.ceil(blob.size / splitSize)
for (let i = 0; i < loopTimes; i++) {
let el = blob.slice(i * splitSize, (i + 1) * splitSize)
el.text()
.then((res) => {
chunkArr[i] = res
if (i + 1 === loopTimes) {
cb && cb(null, chunkArr)
}
})
.catch((err) => {
cb && cb(err)
})
}
}
// 将上面方法切割好的数组重组为字符
function reformChunkAsString(chunks, cb) {
let blob = new Blob(chunks, {
type: 'text/plain'
})
blob
.text()
.then((res) => {
cb && cb(null, decodeURI(res))
})
.catch((err) => {
cb && cb(err)
})
}
到这里,最基础的发送加密文字功能就算是完成了,但是如果就在这里停下来,这个聊天 App 未免也太素了,所以我又加上了发送文件功能。没错,文件分享也是端到端加密的!不过这里也走了一些弯路,起初我是想通过处理字符的方法来处理文件加密,但是后面发现,这性能确实太拉垮了!把一张 1Mb 的图片以每 190Byte 进行切片,需要循环超过 5000 次,且每次循环出来的切片都要使用公钥进行加密!这样的操作在电脑的浏览器上都要卡上 4 - 5 秒,更别说手机了!最后我给出解决方案是——更换加密方法:
最后实测 1Mb 的图片几乎可以瞬间加密完成,但是由于 WebSocket 传输文件效率的问题,能实际在 m42 传输的文件大小被我限制在了 30Mb,超过这个尺寸的文件还是建议大家使用专业的文件传输服务。不过这个解决方案也并不完美:
这两个问题可以通过服务器中转加密文件解决,但是这样也违背了 m42 的设计初衷——不在服务器保存任何用户资料。
m42是我一时兴起而开发的项目,在使用上不保证绝对的安全,也希望大家不要用它来做坏事。
好了,今天就说到这里!拜拜!👋
]]>思来想去还是决定加上评论系统,但是这次,我要自己来!在网上浏览了一些方案之后,我最终锁定了方案:实现一个基于 GitHub Issues 的评论系统。Issues 本来是用来提交软件问题的一个板块,由于做得太 🐮🍺,已经有不少开源项目拿它做评论系统甚至社交软件了。下面我们就来看看我具体怎么实现吧!
这里使用了 GitHub 自己的 OAuth API 来作为通信桥梁,第一步自然就是创建一个 OAuth APP。点击这里可以直接跳转到创建页面:
创建过程中只需要填写这三个参数:
创建完成之后你会获得:
请注意一定要保存好自己的 Client secrets,且不要告诉其他人!
具体步骤可以看官方给出的文档,但是这里有几点需要注意的地方:
第一个接口:
GET https://github.com/login/oauth/authorize
redirect_uri
的域名必须和创建过程中的Authorization callback URL
一样redirect_uri
可以是https://<redirect_uri>?r=<当前博客地址>
,这样自己的服务器就可以知道在 OAuth 验证成功之后往哪里跳转了。scope
默认为空,但是如果你想让所有用户都可以正常评论,scope
应该等于public_repo
假设:
redirect_uri = https://api.jw1.dev/gho
当前博客地址 = https://jw1.dev/2022/10/12/a01.html
那么跳转链接则为:
https://github.com/login/oauth/authorize?
client_id=<client_id>&
scope=public_repo&
redirect_uri=https://api.jw1.dev/gho?r=https://jw1.dev/2022/10/12/a01.html
# 换行是为了可读性,请不要在真实代码中使用换行符号
当用户授权之后,GitHub 会向你的接口发送一个 get 请求:
GET https://api.jw1.dev/gho?
r=https://jw1.dev/2022/10/12/a01.html&
code=<code>
该请求会带上一个 query 参数code
,如果你在 OAuth 验证链接中填了redirect_uri
,那么 query 中也会一并带上,这里我们可以暂存一下r
参数。带上这个 code 参数,我们来到第二个接口:
POST https://github.com/login/oauth/access_token
必需参数有三个:
client_id
client_secret
code
如果请求参数全部正确,GitHub 会返回一个 URL encoded 字符串:
access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=public_repo&token_type=bearer
你可以直接使用这个字符串,但是为了易用性,我们可以设置请求头,让 GitHub 返回 json 格式的数据:
Accept: application/json
{
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"scope": "public_repo",
"token_type": "bearer"
}
到这里,我们才完成 App 的验证,拿到了进入 GitHub 大门的钥匙:access_token
。
拿到access_token
之后,我们需要返回到用户离开时的页面,这时候之前存的r
参数就能派上用场了:
r = 'https://jw1.dev/2022/10/12/a01.html'
redir_url = `${r}?token=${access_token}`
redirect(302, redir_url)
这里我们就成功回到了之前的页面,并且获得了access_token
。
// express代码
const express = require('express')
const router = express.Router()
const needle = require('needle')
const client_id = '你的client id'
const secret = '你的client secret'
let gho = router.get('/', function (req, res) {
let code = req.query.code
let redir = req.query.r
needle.post(
'https://github.com/login/oauth/access_token',
{
client_id: client_id,
client_secret: secret,
code: code
},
{},
function (err, resp) {
let params = resp.body.toString()
res.redirect(302, `${redir}?${params}`)
}
)
})
module.exports = {
gho
}
最难的部分算是解决了,接下来就是使用 token 获取数据了,这里其实反而没有什么难度,我就把用到的接口列出来好了:
获取评论数据
https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER/comments
创建评论
https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER/comments
删除评论
https://api.github.com/repos/OWNER/REPO/issues/comments/COMMENT_ID
这里有一个小细节要注意一下,以上接口中提到的ISSUE_NUMBER
即 GitHub Issues 的编号,关于这个编号怎么获取我走了一点点弯路,原本的思路是这样的:
这个思路有几个很严重的问题:
针对以上问题,我重新调整了思路:
之前的问题将不复存在,而且节省了一个 请求数量 (GitHub 有着 5000 QPH 的限制,所以用的越少越好)
写好布局,写好操作逻辑,这样一个评论系统就诞生了!样式和逻辑都在前端,你们想看都看得到,这个博客网站也是开源的(想抄就抄吧😎)。
感兴趣的朋友可以在下面评论区发一句“test”试一下。🤣
好了,今天就到这里,拜拜!👋
]]>我有一个梦想
我梦想在冬天某个周日的下午
坐在星巴克靠窗的位置
喝着咖啡
晒着太阳
抱着 iPad
—— 写代码(过于装逼)
梦想是个好梦想,奈何 iPad 上一直没有好用的代码编辑器,也没有完备的代码运行环境(你的下一台电脑,还得是电脑),直到 Visual Studio Code 的出现。当时知道这玩意儿是 Electron 写出来的之后,其实脑中有闪过这样一个念头:那是不是浏览器里也可以运行了?不过也只是个念头,哪敢往那想啊?
不过随着 VsCode 的发展,近几年确实出现了一小批基于 VsCode 的,可以在浏览器里运行的代码编辑器 —— Remote IDE(远程集成开发环境)。
这代码在我机器上能跑啊!
这构建也太慢了,我想下班!
下班忘记 push 代码了!
作为一个开发者,这些话大家应该多少都见过,或者大概率在自己身上发生过吧?如何解决这样的问题,或者弱化这些问题的存在感,远程开发的魅力在这个时候一下子就体现出来了:
不再会有谁的机器跑不了某段代码,不再会有谁被构建速度挡住下班路,也不再会有忘记 push 代码,回家之后发现不能接着工作的怨种。而对于我来说,拥有一个 Remote IDE 最大的快乐就是手边只要有任何安装了浏览器的设备,就可以随时随地开始写代码(卷王是我)!也就是说,喝咖啡,晒太阳,抱着 iPad 写代码的梦想终于可以实现了!那么今天就来讲讲目前一些我认为做得还不错的 Remote IDE:
不瞒你们,要不是我在 Vite 官网看到他的广告,我是真的不知道有这玩意儿,体验下来之后发现功能还是比较强大的,从最基本的 VanillaJS 到 React、Vue,都有一键生成的模版,且部署速度非常快。业务方面,折腾自学、多人协作、教学演示这些都不在话下。
虽强大如此,但也不是完全没有缺点:
其实光是 9 刀一个月这么多功能我觉得还是挺合理的,但是总觉得有点亏,因为不是自由的。你不能使用 VsCode 的大部分功能,如扩展、主题皮肤;你也不能在终端里自由地安装一些软件工具,如 vim、curl。在这个平台上,你唯一能做事情就是开发 web 相关的东西,其他一律不能碰!
话又说回来,如果你真的只想专注于 web 开发,那这个平台还是很香的,因为他真的超级快,我做了一个小测试,在删除了node_modules
和package-lock.json
之后运行 yarn,结果只用了 1.365 秒就完成了依赖的安装,看输出,下载依赖包更是只用了 0.1 秒,说明平台内建了 npm 缓存。安装依赖在整体开发中并不是什么重要角色,但是这样的速度却很能提升开发者的幸福感,让开发者更愿意去写代码,至少对于我来说是这样的。
不得不说,还是微软牛逼。Github Codespaces 直接和本地 VsCode 接轨了,他真的完完全全在浏览器里实现了一个 Vscode,你在本地安装的扩展、本地的设置,全部都可以通过微软账号或 Github 账号同步到 Codespaces!这也意味着,你可以使用 VsCode 的全部功能!而 Codespaces 不同于 StackBlitz,他给了你完整的服务器权限,你可以安装各种软件,可以享受完整的 linux 生态。 缺点也是有的:
<div class="table-wrap"> <table><thead><tr><th>Product</th><th>SKU</th><th>Unit of measure</th><th>Price</th></tr></thead><tbody><tr><td>Codespaces Compute</td><td>2 core</td><td>1 hour</td><td>$0.18</td></tr><tr><td></td><td>4 core</td><td>1 hour</td><td>$0.36</td></tr><tr><td></td><td>8 core</td><td>1 hour</td><td>$0.72</td></tr><tr><td></td><td>16 core</td><td>1 hour</td><td>$1.44</td></tr><tr><td></td><td>32 core</td><td>1 hour</td><td>$2.88</td></tr><tr><td>Codespaces Storage</td><td>Storage</td><td>1 GB-month</td><td>$0.07</td></tr></tbody></table> </div>
这是 Github 官方文档中给出的价格表,浅浅地算一下,我们选最低配置 2 核,一小时 0.18 刀,假设平均每天写代码 6 小时,平均每月工作 21 天,那么一个月大概需要 22.68 刀来维持 Codespaces 的正常运转,怎么说呢,不算贵,但是一个月支出 150 多人民币在这玩意儿上面,好像也还是有点亏。
由于 Codespaces 集成了 VsCode 的所有功能,在体验上肯定做不到 StackBlitz 的轻量与快速,而且国内的网络环境大家也都知道,有时候 Github 都连不上去,更别说 Codespaces 了,虽然极大增加了自由度,但是总体体验可能还不如 StackBlitz。
code-server 由 Coder 团队打造,目前有两个版本,一个是开源免费的,一个是供企业使用的闭源产品。 而这也是我目前主要使用的远程代码编辑器。
StackBlitz 和 Codespaces 都是计时计费的运营模式,这也意味着,一旦你关闭编辑器,过段时间他们的服务器算力资源就会被回收,无法在后台运行任何服务,而 code-server 是自建服务,在 code-server 写好代码可以立即在后台运行并建立对公服务(骄傲脸)。且自建服务还有一个优点,那就是可以选择适合自己的服务器,不用操心每个月的账单里会有什么样的“惊喜”。
目前我的 code-server 搭建在腾讯云(不是广告)的 54 元档的港澳地区轻量服务器,关于定价可以看一下官网给出的价格表,不想备案的朋友们,可以试试香港或者首尔的机器,性价比高,连接速度也相对较快,最便宜的机器只要 32 元/月。
code-server 没有明显的缺点,硬是要说的话,那就只有一个:
因为版权问题,code-server 选择了 open-vsx 作为扩展市场,虽然他拥有大部分 VsCode 的扩展,但是一些微软开发的扩展暂时还是没有的,不过解决方案也很简单,就是会麻烦一些。
code-server 支持从.vsix
安装扩展,大家只需要从 VsCode 官网下载好需要的扩展文件,传到 code-server 上安装就好了。
好了,今天就讲这么多,拜拜!
]]><img src="https://blog-r2.jw1.dev/p_assets/202208/a01/Untitled.jpeg" style="max-width: 375px; width: 100%">
<hr>
我们先抛开顾虑,直接查MDN堆代码看看效果。
<div id="app">
<audio id="music" src="test-2.mp3"></audio>
<button @click="play">play</button>
<div style="display: flex; height: 60px; align-items: flex-end; margin-top: 20px">
<div class="item" v-for="item in fData" :style="{height: item / 255 * 100 + '%'}"></div>
</div>
</div>
<script src="vue.js"></script>
.item {
width: 6px;
min-height: 6px;
background: #333333;
align-items: flex-end;
border-radius: 6px;
margin-right: 3px;
}
new Vue({
el: '#app',
data: function () {
return {
fData: []
}
},
methods: {
play() {
let _ = this
let audio = document.getElementById('music')
audio.play()
let audioContext = new AudioContext()
let audioSrc = audioContext.createMediaElementSource(audio)
let analyzer = audioContext.createAnalyser()
analyzer.fftSize = 32
audioSrc.connect(analyzer)
analyzer.connect(audioContext.destination)
let bufferLength = analyzer.frequencyBinCount
let frequencyData = new Uint8Array(bufferLength)
setInterval(() => {
analyzer.getByteFrequencyData(frequencyData)
_.fData = _.uint8ArrayToArray(frequencyData)
}, 1000 / 24)
},
uint8ArrayToArray(uint8Array) {
let array = []
for (let i = 0; i < uint8Array.byteLength; i++) {
array[i] = uint8Array[i]
}
return array
}
}
})
结果:
<video src="https://blog-r2.jw1.dev/p_assets/202208/a01/001.mp4" muted autoplay playsinline controls></video>
这样一通操作下来,一个简单的频谱显示器就完成了(老有成就感了),大概原理也很简单:
上下文
(Audio Context)<audio>
并赋值为 audioSrc
分析仪
节点(Analyzer Node)audioSrc
连接 分析仪
,分析仪
再连接到 ctx.destination
即用户端输出分析仪
的数据<hr>
现在,真正的问题来了:不管我尝试什么音乐,我的频谱图永远都是这样的 ⬇️(成就感啪一下没了)
可能会有一些小差异,但是呈现的效果基本差不多:低频看起来很足,高频几乎没有
这时候,作为一名“准专业编曲师”,我有一种直觉,Web Audio API 给出来的频谱数据,可能是线性分布的。为了验证我的猜想,我制作了一段从 30Hz 到 20000Hz 慢慢上升的正弦波音频,调高分析仪的 fftSize ,放进代码里看看输出:
<video src="https://blog-r2.jw1.dev/p_assets/202208/a01/002.mp4" muted autoplay playsinline controls></video>
再来看看同样的音频在 Ableton Live 中 Spectrum 线性模式下的输出:
<video src="https://blog-r2.jw1.dev/p_assets/202208/a01/003.mp4" muted autoplay playsinline controls></video>
这表现可以说,基本一致,同时也验证了我的猜想 —— Web Audio API 输出的频谱数据是线性分布的。线性分布是什么分布?很好理解:一个坐标系中,X轴上10到20需要走的距离,与2000到2010需要走的距离一样。然而对于人来讲,线性分布这个解决方案,并不是最优的。我们可以先看一些专业EQ插件的截图,看看他们是怎么做频谱分布的。
FabFilter Pro Q3:
Eiosis Air EQ Premium:
SlateDigital Inf EQ:
Ableton Live EQ Eight
可以看到,这些分布基本都是在类似指数分布的频率上进行分段式的对数分布,而且似乎是遵循着等响曲线做出的优化。
我们经常听到“指数级增长”这个词,举个简单的例子:10、100、1000、10000,这个数列就是指数增长。上述频谱图中,频率分布基本是按照指数来的。
关于对数我们不需要了解太多,对数分布也可以根据上面我对线性分布的理解做出延伸:一个坐标系中,X轴上10到20需要走的距离,与2000到2010,同样是10的差距,但是需要走的距离却不一样。如果我们以Ableton Live更加青睐的分布规则,把频谱分析仪的可视化面板看作是一个坐标系,则 X 轴上,10 - 10000 被分成三段,分别为10 - 100、100 - 1000、1000 - 10000,每一段距离都一样且被分成9份进行对数分布。
说到等响曲线,它并不像对数那样客观,而是一个主观概念。此概念阐述的内容如果用大白话来讲就是:同样音量但不同频率的声音,给人听起来的强弱是不一样的。即:60Db 的 4000Hz 正弦波与 60Db 的 10000Hz 正弦波,前者听起来会更响一些。此概念也同样可以用来解释,在删除歌曲 10kHz 以上部分的时候(可以理解为对半切),你不会觉得整首歌的质量下降了50%,主观上听起来可能只有10%。
个人认为,频谱这样的分布模式,其实是为了弥补人耳的缺点,在视觉上模拟出与听觉类似的“缺陷”,真正做到音视一致从而提升用户体验。
<hr>
关于如何在浏览器里实现,其实很简单,我们只需要模拟指数级分布就好,不用去考虑分段中的对数分布。演示地址: https://jw1.dev/frequency-test/test-2.html
现在再来看一下对比:
哪个体验更好,一眼看过去便知道了。
至此,一个不算完美但是看起来还不错的频谱显示器就算是完成了,我也给 webAudio 开源项目提了issue,希望未来能够添加支持,为开发者带来更佳的体验!
✌️
]]><br>
Turns out I wasn’t suck at making things, I suck at doing things patiently. I bought lots and lots of plugins and I did nothing with them but only made me a poor ass motherfucker, is this the software to blame? or am I just a lazy fucking human-shaped meatball?
<br>
I haven’t touched my skateboard since WWI. Skateboarding isn’t too hard for me, and it did a lot of favors of keeping me in shape. So why did I quit you might ask, cuz I’m a lazy fucking human-shaped meatball ain’t I? Grab your fucking skateboard and go fucking skate meatball!
<br>
Talking about coding, oh, I really learned a lot in this year, I mean last year (2021), if you have a time machine and arrest the 2020 me, I could do a 10-0 to him, so now there are two me(s), it’s like, me-me. Yes meme! The fucking memes, I watched too many memes, every time I wanted to do something seriously, these are the obstacles that’ll always ruin it, these are the mind poison that’ll always destroy me! Destroy the real me!!
<br>
I’m tired of me being like this. Maybe this year will be a good start, <br> <br>to change.
]]><div class="sugg-back"> <strong>看起来你正在使用RSS阅读器浏览本页面,通常情况下作者不会要求您去官网进行阅读,但是此页面包含一些自定义音频组件,为了得到更好的体验,请访问博客官网 https://jw1.dev 进行阅读。谢谢!</strong> <br/> </div>
在油管看一些音乐制作大佬的视频的时候,总是能发现一些大盒子上插满了线,上面的灯光还总是一闪一闪的,在好奇心的驱使下我终于知道,这些大盒子是模块化合成器。模块化合成器有很多种类,其中Eurorack算是受众比较广的,不过他的价格也是让人望而却步,那个时候我就在想,如果能有软件模拟Eurorack就好了……
VCV Rack 做到了,他把昂贵无比的Eurorack直接塞进了你的电脑。
早在VCV Rack 1.0还在公测的时候,我就关注上了这款软件。开源,免费,安全,有趣,有了这些标签的加持,使得VCV Rack迅速获得了一大批用户,也从侧面证明了它的优秀。然而在当时,VCV Rack还只能算是一款玩具软件,根本不能称得上是生产力工具。不过,就在2021年年底,我收到了来自VCV官方的邮件:2.0 版本,来了!
先来看看我能直接感受到的关于VCV Rack 2.0的一些新变化:
不过,以上这些变化本质上只是让VCV Rack成为了一个更好的玩具,真正让他变成生产力工具的,是Pro版本中的官方VST插件!啊,盼星星盼月亮,它终于来了!
<br/>
Sylenth1, Pigments, Serum, Vital, PhasePlant, 想必这些软件合成器大家多多少少都用过,其中任何一个都有着庞大的用户群,有着庞大的预设库(无论免费与否),有着各不一样又无与伦比的优势。而VCV Rack出厂只有34个模块以及一个合成器示例,由于是新兴软件,用户群相对于以上我提到的软件也不是很庞大,也就是说,除了官方手册和一小部分来自社区的助力,很大程度上你需要自己去挖掘和摸索VCV Rack究竟能做什么,听起来好像毫无优势,但是……
做音乐是一项创造性的工作,创造性怎么来?如果你一直用着别人的预设而不去想着去创造自己的东西,创造性又何从谈起?我不是说其他合成器做不了创造性工作,更没有贬低他们的意思,以上提到的那些软件我自己也有在用,我的意思是: 如果你不熟悉合成器的原理,VCV Rack将会是一个绝佳的学习工具,以后面对任意一种软件或硬件合成器时,你都可以很快的上手。如果你已经非常熟悉许多合成器的操作以及原理,那VCV Rack能给你的,是更高的自由度,更多的选择,更多的乐趣。VCV Rack的主旨从来都不是成为主流合成器软件的竞品,他给了用户一个全新的方向去探索电子音乐的本质。
好了,高级话术吹是吹了一堆,但是实际体验下来究竟怎么样?请大家欣赏我做的Demo。除了一些声效(SFX)和一轨Horn,其他全部使用了VCV Rack。
<AppAudio src="https://blog-r2.jw1.dev/p_assets/202112/vcv-test.mp3" label="VCV Rack in action" client:only />
]]>初入 Github Pages 这个大坑的时候,官方推荐的博客引擎就是 Jekyll,我也一直用到了现在,当中不乏一些(很多)折腾,总体用下来其实没啥大毛病,直到上一次我更新了 macOS Monterey 导致 Gem 和 Bundler 全炸 (jekyll/issues/8784),而我对于其报错信息只有两眼一抹黑,这给了我极大的不安全感,而 Hexo 完全基于 Nodejs,对于做前端的我而言,从开发到部署的学习成本应该都会小很多,于是萌生了切换博客引擎的想法,目前感受还不错,分享一下经验。
根据官网文档来看,数据迁移其实很容易搞,只需要把之前 Jekyll _post
文件夹下所有文件复制到 Hexo source/_posts
文件夹,然后在_config.yml
中将new_post_name
改成:year-:month-:day-:title.md
,这样博客数据的迁移就完成了。
之前用 Jekyll 的时候就是纯手写的模版和样式,哪怕到了 Hexo 我的打算也一样,并且还要在原有基础上再次精简代码。不过,过程并不是我想象中的那样一帆风顺:
EJS 不说了,之前有用过模版引擎的朋友们应该都能很快上手,遇到问题比较多的方面是 Hexo 的一些全局变量。
<div class="table-wrap">
Jekyll | Hexo | |
---|---|---|
<div style="width: 140px;"></div>获取 Post 列表 | site.posts |
page.posts |
获取博客主标题 | site.title |
config.title |
获取 Post 日期及格式化 | <code>article.published_at | date: "%Y-%m-%d"</code> | moment(page.date).format('YYYY-MM-DD') |
插入公共代码 | include footer.html |
partial('partial/footer') |
</div>
搞脑子的部分解决之后,剩下的就只有舒适了,模版内的语法完全就是我们熟悉的 JavaScript,写起来如鱼得水。
文件结构比 Jekyll 清爽很多
├── scaffolds
├── source
| ├── _data
| └── _posts
├── themes
| └── jw1dev
└── _config.yml
用上了 Github Actions,之前 Jekyll 一直有些 bug,所以一直都是手动部署。虽然自动部署很爽,但是刚上线还是遇到一个坑,那就是时区,部署上去发现有些文章时间会相差一天,很快意识到这可能是 Github Actions 的虚拟机使用了 UTC 时间,不过也好解决,workflow 配置文件加几行就可以:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
+ - uses: szenius/set-timezone@v1.0
+ with:
+ timezoneLinux: "Asia/Shanghai"
删除了大量的无用代码
优化了侧栏的设计,现在移动到了 slogan 下方,提升了可访问性
<br>
好了,就说这些。拜拜!
]]>就JavaScript而言,在作者的观点中,一个pro应该:
然而事实上,我们真的需要这样写代码吗?
先来看这样一段代码:
[].forEach.call($$('*'), function(a) {a.style.outline = "1px solid #" + (~~(Math.random()*(1<<24))).toString(16)})
这是Addy Osmani在Github Gist上面发布一段用来调试CSS的代码,如果你把它复制进浏览器的Devtools控制台并按下回车执行它,它会给每一个元素都加上不同颜色的outline。抛开代码中用到的技术不谈,单论这样一行代码,你觉得它专业吗?我的答案是:
It depends.
如果你是参加一个编程比赛,主题是用最少的代码让浏览器上每个元素都显示出不同颜色的outline,那这样一行代码可以说是非常专业了,很可能你就是冠军了。但是如果这是一个团队协作的项目,你给出了这样的代码,code reviewer反而可能会骂死你。为什么?因为你的代码可读性几乎为零,除了炫技,一无是处。作为团队中的一员,保证其他成员能以最低成本看懂你的代码才应该是最高优先级的。
/**
* @description put outline on every single element in the document
*/
function outlineEverything() {
let everyElements = document.querySelectorAll('*')
everyElements.forEach((el) => {
let randomColor = makeRandomColor()
el.style.outline = `1px solid #${randomColor}`
})
}
/**
* @description get a random hex color value `rrggbb` without the `#`
* @returns {string}
*/
function makeRandomColor() {
// get a random number between 0 - (256 * 256 * 256)
let _r = parseInt(Math.random() * (256 * 256 * 256))
// return number in hexadecimal
return _r.toString(16)
}
outlineEverything()
再来看看现在的代码,在保证同样功能性的前提下,大幅提高了代码的可读性,这才是我们在开发阶段需要的代码啊!
可能有人会说性能问题,那如果我告诉你后者的性能只比前者差不到5%你会怎么想?
这5%的性能损失可能会换回同事的不杀之恩也不一定呢?而且很多时候,并不是代码少,程序就能跑得快的,关于这一点以后可以专门写一篇博客来说明一下。
—— 在开发阶段,你越是压缩代码的写法,在维护阶段它越有可能被删掉。
下面再来说说,自己的代码中究竟应不应该处理所有可能出现的错误。来看看这样一段代码:
let itemData = {
price: 10,
quantity: 3,
discount: 0.05 // rate in 0-1 scale
}
/**
* @description get items total amount of money with discount applied
* @param {object} item
* @param {number} item.price
* @param {number} item.discount
* @param {number} item.quantity
* @returns {string} total money
*/
function getTotal(item) {
let _total = item.quantity * item.price
let _discountAppliedTotal = _total * (1 - item.discount)
return _discountAppliedTotal.toFixed(2)
}
console.log(getTotal(itemData))
如果执行正确的话,那么控制台应该输出28.50
,因为一共3件商品,商品单价为10,再加上5%的折扣,那总价确实是28.50,按照现实中的逻辑,这样的代码不管是从可读性和健壮性来说其实已经非常好了。但是以视频作者的意思,任何可能出错的地方都要加以处理,也就是说,我们需要考虑商品数量、折扣和单价都有可能会出现null
或者undefined
值,如此缜密的心思当然是值得鼓励的,但是从实际开发角度看去这会让原本clean的代码变得not that clean:
function getTotal(item) {
if(item.discount === undefined || item.discount === null){
return 0
}
if(item.price === undefined || item.price === null || item.price === 0){
return 0
}
if(item.quantity === undefined || item.quantity === null || item.quantity === 0){
return 0
}
let _total = item.quantity * item.price
let _discountAppliedTotal = _total * (1 - item.discount)
return _discountAppliedTotal.toFixed(2)
}
getTotal
方法的代码瞬间比之前多了一倍,而且这么写下来真的有意义吗?如果后端真的返回给你的数值是负数或者为null
,这难道不应该是后端的问题吗?为什么要在前端进行静默处理?是觉得debug难度还不够大吗?
—— 任何代码的出现都应当以实际业务逻辑为参考系,否则毫无意义。
总结:脚踏实地,事无巨细,这样写出来的代码,才是专业的代码。
]]><div class="sugg-back"> <strong>看起来你正在使用RSS阅读器浏览本页面,通常情况下作者不会要求您去官网进行阅读,但是此页面包含一些自定义音频组件,为了得到更好的体验,请访问博客官网 https://jw1.dev 进行阅读。谢谢!</strong> <br/> </div>
对于已经接触音乐制作行业许久的各位大佬而言,瞬态控制器真的一点都不陌生,甚至可以说是太熟悉。但是对于刚入门的新同学来说,这个概念还是非常模糊的,所以今天就来聊聊,什么是瞬态控制器?该在什么地方用它?
瞬态控制器,我们可以“顾名思义”一下,它会控制音频起音的那一瞬间的音量。不过没有音频样本对比的话我们还是很难知道这个叫瞬态控制器的东西究竟做了什么。
先来听一下原声:
<AppAudio src="https://blog-r2.jw1.dev/p_assets/202109/a01/a01_3.mp3" client:only />
再来听一下瞬态控制器处理过的声音:
<AppAudio src="https://blog-r2.jw1.dev/p_assets/202109/a01/a01_4.mp3" client:only />
听起来好像也许可能大概瞬态控制器处理过的鼓组更加有力一点?再组合起来听一下(可以打开循环按钮,来回切换声音源感受):
<AppAudioDiff set={[ {src: 'https://blog-r2.jw1.dev/p_assets/202109/a01/a01_3.mp3', label: '原声'}, {src: 'https://blog-r2.jw1.dev/p_assets/202109/a01/a01_4.mp3', label: '经过瞬态控制器处理的声音'} ]} client:only/>
如果还是不确定的话我们再来看看图:
上面一轨是原声,下面一轨是处理过的。我们可以很明显的看到,被处理过的音频的音量明显比没处理过的…… 更小了?其他好像也没啥区别啊?但是我们主观上,第二轨确实听起来更加有力一点。
好,我先解释第一点,为什么音量变小了?
这里就要引入另一个混音的概念了 —— 音量补偿 (Volume compensation)
在混音里,我们会经常性地开关效果器以此来监听音频被处理前后的区别,但是一些插件的处理难免会增加或减小原始音频的 响度 (Loudness) 或 电平 (Volume) ,而人耳会主观的认为,更大的响度听起来更好一点,这就影响了我们对于调整效果器参数或者选择完全不同效果器的客观性。这里就不再展开来讲了,大家只需要知道,我针对处理过的音频做了对应音量上的修改以此来维持大家的监听客观性就可以了。
再来说这段音频处理前后到底有什么区别,我们来把电平图放大了对比来看看:
我们可以非常清晰的看到,起音电平被放大了将近三倍。
现在我们终于知道瞬态控制器究竟控制的是什么了! 同时我们也知道了为什么第二轨听起来这么有力的原因。
至于瞬态控制器可以用在哪里,除了我刚才已经演示过的鼓组,也可以用在其他乐器上,比如大提琴:
原声:
<AppAudio src="https://blog-r2.jw1.dev/p_assets/202109/a01/a01_5.mp3" client:only />
处理之后:
<AppAudio src="https://blog-r2.jw1.dev/p_assets/202109/a01/a01_6.mp3" client:only />
没错!这里我弱化了大提琴演奏pizzicato技法时候的瞬态,获得了更加柔和的声音。
谁说瞬态控制器只能用在打击乐上了?有时候我们可以creative一点,你完全可以把瞬态控制器用在人声上,用来提升或降低人声中爆破音的力度,毕竟说白了,瞬态控制器也是从压缩器转变过来的。
好了,今天就讲这么多!peace out 😎
]]>不过讲真的,第一天使用WebStorm最直接的感受就是——累。
等等等等,还有数不清的无法习惯的习惯,让我一度开始怀疑自己这项决定是否正确和值得。
光是解决tabSize
就花了我半天时间,你需要进每个文件类型选择相应的tabSize
。这一点相比VSCode确实不怎么方便。但是往好了想,独立的文件配置确实会让你看起来更加专业,对于一些特殊的文件类型也可以自定义配置。这就好像WebPack和Parcel的区别,一个方便但是傻,一个聪明但是麻烦。好在这些东西你只需要设置一遍,因为WebStorm和VSCode一样,都有同步设置的选项。
WebStorm同步主要有两种:
作为小白的我第一次用就算是踩到了坑,直接使用了Git仓库同步。你需要在任意Git平台新开一个仓库来存储所有的设置,可以从本地提交覆盖线上的设置(Push),也可以从线上下载覆盖本地的设置(Pull),听起来一切都很完美,然而Git仓库不支持同步插件。主要是,在你使用Git仓库同步设置之后,使用JetBrains账号同步的功能是置灰的,也就是说一次只能使用一个同步方式,为此我还特地去论坛上查了一下怎么使用JetBrains账户同步设置。好吧,你需要先把之前设置的Git仓库地址删掉,之后才可以使用JetBrains账号来同步。当然了,这一项功能仅限购买了正版WebStorm的用户使用😎。使用账户同步之后一切都变得简单了,一切设置和插件更改都能多端同步。
说到插件,WebStorm的插件市场和VSCode比起来可以说是“发育不良”。截至我写博客的这个时间点,VSCode已经有22k+之多的插件了,而WebStorm只有2.7k+。造成这一现象的原因,我猜不只是因为VSCode更受欢迎或者开源之类的,WebStorm作为一个IDE本来就包揽了强大功能的同时也造就了插件市场的萎靡。不过苦苦寻找之后,作者还是发现了几个之前在VSCode上钟爱的几款插件:
最喜欢的当然还是VSCode Keymap,习惯一款软件的快捷键需要漫长的时间,这是我当下不愿意支出的时间成本。
最后两点让我很难受就是版本控制的UI设计和一次只能打开一个项目的设计了。
为什么说版本控制的UI让我很难受,确实,信息量对比无插件的VSCode来说多了不少,但是Push操作面板要和提交面板分开?是为了不让用户误触吗?是为了防止新手乱搞吗?然而问题是,菜鸟总有办法乱搞,Pro总有办法恢复。
至于单项目设计,我看论坛上已经有人问过了:
如何在一个WebStorm实例中打开多个项目?
官方给出的回答是:Never. (我帮你们简化了一下
不过,替代方案总归是有的,左上角找到File → Attach Project
,这样确实可以实现一个WebStorm实例打开多个项目。但是随着我更加深入的使用WebStorm,渐渐地我发现了这样设计的妙处。
一次打开一个项目可以让你更加专注在项目上,而不用多个项目切换来切换去,而且你可以针对每个项目做出不同的配置,比如Web服务、Npm脚本或者项目构建,在单实例单项目的模式下,你可以很好的专注在这些细节上。
很多人追求做Web开发就是要快,所以代码编辑器一定也不能慢,在后续一系列的测试中也表明了,VSCode不论是启动速度还是代码提示速度确实比WebStorm要快。以前的我对这些也是洋洋得意,但是看了很多反面描写的文章之后,我开始意识到,速度快,并代表你能写好代码。这也是为什么我会有这样的好奇心来试一试WebStorm是不是真的像他们说的那么好。
在实际使用中,WebStorm从启动到完全显示UI组件大概在10-20秒之间,VSCode大概在5秒以内,启动速度确实慢了一些,但是处于可接受范围内。再来看生产效率,WebStorm几乎会在任何地方给你提示:
同时,得益于单实例单项目的设计,Reference和Usage模糊查询几乎不会出错。
再说代码格式化(Reformat),WebStorm格式化一个3000行的Less文件只需要200ms左右,而同样使用Prettier引擎的VSCode需要800ms - 1000ms左右。
更甚于,WebStorm会把所有的不规范的语法,事无巨细的告诉你,初期我只觉得烦,后来慢慢的,一点点的去核查,一点点的消除警告和错误,才发现,WebStorm一直在尝试让你写出规范漂亮的代码,在项目初期就直接规避掉日后可能会使整个项目崩溃的小错误。
所以,从VSCode到WebStorm,我后悔了吗?
嗯,暂时还没有,为什么说暂时呢,按照VSCode的发展速度,以及其插件市场的扩展速度,可能没几年,VSCode的体验就会超过WebStorm,至于JetBrains公司是否会因为感到压力陡增进而加速发展对应产品,我们拭目以待吧!
]]>虽然又是社畜的一天,不过今天和往常不一样的是,下班后得去即将拍摄的工厂参观一下。
几天前,朋友找到我,说要给他家工厂做一个宣传片,看我最近在研究剪辑。于是商量能不能找我搞个宣传片。嚯!就我这个暴脾气当下就接了下来,也直接确定了到时候拍摄可以租专业设备进行拍摄的想法。这一点,对于刚刚接触影视制作没多久的我来说,诱惑着实是有点大的。
不得不说,专车接送还是非常爽的。五点半下班后,准时上了朋友的奔驰S400前往工厂。
厂子和我想象中有点不太一样,并不是很高大上的样子你们懂我意思吧🌝。但是管他呢,过两天就能玩到牛逼的设备了。朋友老爸给我介绍了一圈,对于基本的生产流程我已经了解了,剩下的就是制定拍摄方案了,原定5月29号的拍摄也延到了6月5号,一切都是为了拍摄当天不至于手忙脚乱。
于是拍摄当天我就开启了手忙脚乱模式:
这车翻的……妥妥地
千万不要在不熟悉设备的情况下直接就去拍摄,如果这是非常严肃的商业摄影,你到时候连死都不知道怎么死的。(literally
永远永远都要记住检查你的存储媒介,因为你不想出现那种,导演到了,道具准备好了,演员也就位了,都在酝酿情绪了,你却告诉导演储存卡坏了的情况...
在自己经济条件和制作成本够的上的情况下,不要吝啬租借一些高级的设备,因为你永远不知道你手头上看似过得去的设备会给你怎么样的惊喜。
最最最最最重要的一点:
千万不要在没有详细脚本和分镜的情况下直接就去拍摄。
我们现在的情况就很僵,很多镜头拍完了和朋友一对才发现他说的根本不是那个意思,更不用说有很多镜头压根儿没有拍了(被骂的很惨)。
目前来讲,最靠谱的就是重新借设备拍摄了,不过在下次拍摄之前,我希望我和我的homie们能拟一份详尽的拍摄脚本,精确到秒的那种!个人觉得,如果拍摄脚本比较细致,那这份脚本对于拍摄部署的帮助度已经能够达到80%了。
而分镜则是剩下那20%,听起来或许不是很多,但绝对是商业摄影中必不可少的一部分!
分镜是对剧本文字的视觉化。
分镜可以弥补文字脚本的不足,能让客户在正式开机之前能有一个大概的视觉印象,这对缩减制作和后期成本有非常大的作用。
好了,说完了。
希望下次拍摄顺利吧!
]]>非常典型的what-why-how问题,不急,接下来我会为大家一一讲解。
这个概念我在压缩器那篇博客中提过,也说过会单独写一篇博客讲解,这不?来了!在这我不会跟你扯什么原理之类的东西,因为,我也不懂🌝。不过不懂原理没关系,我们只需要知道这玩意儿是什么就可以了。
通常来讲,Sidechain是一项技术,一项用来调整音频音量的技术。
怎么样?是不是很直白很简单?甚至有些简单过头了,就这?
确实是这样,只不过这个音量调整。是完全自动的。
想必你们也没少听哪些大佬们说要用Sidechain要用Sidechain要用Sidechain,那...
说到为什么要用,那就要说说编曲中需要频繁调整音量的场景了:
在这种情况下,如果没有Sidechain只靠人力慢慢去调整是非常可怕的。而且后期一旦有任何曲调或结构上的改动,那些之前修改过的音量必须重新修改一遍,于是Sidechain出现了,接下来我们看看常用的Sidechain手法都有哪些吧!
从编曲角度出发,大概就是两种方式:
VST插件
这里推荐几款比较好用的Sidechain工具
压缩器
大概就是这样,至于教程我会在下面贴上链接,毕竟别人已经讲过一遍了。
祝大家编曲愉快!✌
第一次经历DAW的大版本更新还是有点激动的,感觉这次Live更新主要是针对MPE和录音优化(Comping),而且还添加了几个非常不错的Device(音频插件),可以说是诚意满满。除了官方说的这些优化,个人在实际使用中也发现了一些相比Live 10更加优秀的小细节。
这一点在我之前的博客中有提到,显然这一问题在新版本中得到了解决。
<video src="https://blog-r2.jw1.dev/p_assets/202102/1.mp4" muted loop playsinline controls preload="none"></video>
音频加载
在没有.asd文件的情况下,Live 11加载音频的速度明显要快很多
<video src="https://blog-r2.jw1.dev/p_assets/202102/2.mp4" muted loop playsinline controls preload="none"></video>
自带插件加载
在没有预加载的情况下,一些自带插件的加载速度明显快了不少,几乎就是瞬间加载完成
<video src="https://blog-r2.jw1.dev/p_assets/202102/3.mp4" muted preload="none" loop playsinline controls></video>
文件indexing速度提升
以前添加一个音效库Live都要扫描半天,更新后直接起飞
<video src="https://blog-r2.jw1.dev/p_assets/202102/4.mp4" muted preload="none" loop playsinline controls></video>
可以单独显示每轨占用的CPU资源
在Arrangement视图下点击右下角新增的C
字按钮即可显示
<video src="https://blog-r2.jw1.dev/p_assets/202102/5.mp4" muted preload="none" loop playsinline controls></video>
Comping并不只是适用于音频,MIDI也一样可以用
<video src="https://blog-r2.jw1.dev/p_assets/202102/6.mp4" muted preload="none" loop playsinline controls></video>
以上仅仅是我使用三天后发现的小细节,在后续的使用中可能还会发现更多惊喜。
Ableton Live作为我的主要音乐制作工具已经有两年了,不管是大更新还是小更新,每次似乎都能看到一堆bug fixes和优化,这次的大更新更是如此!
(Ableton牛逼!
]]>不值得。
Trackspacer从根本意义上来讲其实只是一个多频段EQ压缩器,Sidechain接收信号,然后根据信号对相应的EQ band进行音量变更。那么我们是不是可以用相同的原理,在Ableton live中自己做一个这样的工作流呢?
完全可以!而且可以做得更好!
原料:EQ 8, Compressor (是的。就这两样东西)
同时选中 (shift
+ 点击) 这两个插件,然后按下 ctrl/cmd
+ G
合并为一组
点开chain view和macro view
选中Chain并使用ctrl
+ D
复制出两个一样的chain出来,并ctrl
+ R
更改名称
到这一步,聪明的你可能已经猜出来我想要干嘛了吧。没错,我们用了三个chain把声音信号分为三份,这样就可以对每个chain单独进行处理了!
单独设置每个chain可以经过的声音频段,这里我的设置是:
slope全部都是x4
假设我们现在有这样一个工程:
<video src="https://blog-r2.jw1.dev/p_assets/202012/1.mp4" controls></video>
通过spectrum大家应该可以看到1轨道和3轨道的300 - 1000hz
频段peak都比较大,我现在希望能在这两轨同时播放时突出1轨道的300 - 1000hz
,要怎么做呢?这个时候我们刚刚做的东西 (暂时就叫他multi FX吧) 就派上用场了:
我们可以把multi FX加载到3轨道,然后调整相应的band宽度,因为Low chain已经在300hz的刻度了,所以我们不需要调整,主要看Mid chain和High chain。
然后进入Mid chain的compressor,打开Sidechain,选择1轨道,这里叫bass high
,现在按下播放键我们可以看到已经有信号传过来了。
最后,合理调整压缩器参数,我们就应该能看到如下效果:
<video src="https://blog-r2.jw1.dev/p_assets/202012/2.mp4" controls></video>
可以看到1轨道响起的瞬间,3轨道的300 - 1000 hz
频段立马就下去了。
看到了?你根本不需要花那59欧元,省着吃顿好的他不香吗?
当然不,Ableton live可以让你保存FX rack,下次使用的时候直接像插件一样拖进轨道就可以了!
不过在保存前,我们还需要再配置一番macro。是的,还记得之前我让你打开macro view吗,就是这个目的!
我们只需要map四个参数到两个knob上即可
点击Map按钮开始操作
如果不会Map的小伙伴可以看一下下面的视频。
<video src="https://blog-r2.jw1.dev/p_assets/202012/3.mp4" controls muted></video>
最后点击FX Rack右上角保存并取个名字方便识别,下次用的时候直接拖进轨道就可以了。
是的,我们直接造了一个多频段FX插件,你完全可以把压缩器替换成任何你想要的插件,也可以再复制一个FX rack,让他直接变成6-band FX插件,再复制一个,直接变成9-band FX插件,再复制一个,12-band FX,再复制一个,15-band FX,18-band FX ... 停!!!
得,你想复制多少个就复制多少个吧!🤣
祝各位编曲愉快!
]]><div class="sugg-back"> <strong> 看起来你正在使用RSS阅读器浏览本页面,通常情况下作者不会要求您去官网进行阅读,但是此页面包含一些自定义音频组件,为了得到更好的体验,请访问博客官网 https://jw1.dev 进行阅读。谢谢! </strong> </div>
刚开始搞音乐的时候,总是有朋友告诉我,“这段bass要加个压缩器”、“这人声要加个压缩器”...,好的,我加了,也调了些参数啊啥的,嗯。当时我的内心:
<br/>
这不就声音变小了嘛🤦♂️
<br/>
当然了,那时候太年轻不懂事儿,现在几乎每首歌,每个轨道,我都会用压缩器,所以究竟什么是压缩器呢?这玩意儿到底有什么用呢?不着急,请看下面。
顾名思义,它压缩你的声音。🌝
上面的解释肯定是有点太简短了,你不一定能理解,因为确实,声音这东西太抽象了。我们可以先从压缩器的几个固有参数开始说起:
threshold
ratio
attack
release
那翻译过来一定就是阈值
比
攻击
释放
,对,就是这样。
开玩笑的哈,呐,声音信号进入压缩器时,阈值就像一只手,他会检测进入的信号强度是否超过它本身,如果超过了,这只手就会往下压,而压缩比则代表了这只手下压的温柔程度,1:1
的话是溺爱,10:1
那就是无情,任何企图超越阈值的声音信号都会被压得死死的。这里的attack
也明显不是攻击的意思,它代表了阈值这只手下压的速度,也就是手速快慢与否,release
也是手速,只不过是手抬起来的速度。
这么说你们应该差不多能懂了吧。
不懂的话也可以,直接看图:
attack
和release
的值这里我都调得特别大,是为了能让你们看得更清楚,就我自己而言,实际使用中,几乎不会出现这种情况。
存在既合理,更何况压缩器这个东西,都快一个世纪了,还是有好多人在研究它。那究竟,这样一个效果器在混音中有什么样的实际应用呢?
一首歌,如果想要引起听众的共鸣,必定少不了人声。然而,即使是非常专业的歌手,也不能保证在面对麦克风时能够稳定输出音量。从混音师的角度来看,人声音量不够稳定一定会对整个混音作品造成毁灭性的打击,为什么呢?
有的地方声音太大,会盖过伴奏,有的地方声音太小,伴奏又会盖过人声。
不过压缩器的出现完美的解决了这个问题。大家可以听一下下面这个demo:
<AppAudioDiff set={[ {src: 'https://blog-r2.jw1.dev/p_assets/202009/diff-1-Audio.mp3', label: '没有压缩的人声'}, {src: 'https://blog-r2.jw1.dev/p_assets/202009/diff-2-Audio.mp3', label: '经过压缩的人声'} ]} client:only/>
原曲来自我的好朋友Wynes:https://music.163.com/#/song?id=1416869953
<br/>
可以感受到,经过压缩人声明显稳定了很多,从下图中更加可以看出,原本比较轻的人声被放大,而原本比较突兀的地方,也被压缩到了同等级别。这样的在混音中可以保持很好的稳定性,当然,前提是伴奏也要保持稳定。需要注意的是,歌手的换气声也被放大了,这里先埋个坑,以后再给大家详细讲一讲怎么处理人声。
压缩器在鼓的作用上与人声大致相同,小的声音变大,大的声音变小。
<AppAudioDiff set={[ {src: 'https://blog-r2.jw1.dev/p_assets/202009/drum-diff-2.mp3', label: '未经压缩的鼓组'}, {src: 'https://blog-r2.jw1.dev/p_assets/202009/drum-diff-1.mp3', label: '经过压缩后的鼓组'} ]} client:only />
<br/>
不过在鼓组上,压缩器的attack
和release
比较讲究。
图片是上面两个鼓组音轨的对比,我们可以看到kick进来的瞬间,有压缩的振幅明显要比未压缩的振幅大,而之后的频谱振幅则没有太大变化。这是因为压缩器设置了合适的attack
,在kick信号刚进来时不进行压缩,也就是保留了瞬态,这样做的好处是能让鼓听起来更加有力!
而适当的release
会让鼓组听起来更有弹性,太长或者太短都不行,最好是在下一个peak来临之前能够彻底release掉,也就是让压缩器回归初始状态。举个极端的例子,如果你设置了10秒的release
,kick和snare之间的间隔可能只有300毫秒,这样设置之后,阈值那只手永远也没办法抬起来,因为速度太慢了,刚抬起来一点点,下一个信号peak又来了,这样做只有一个意义——减小音量。
Sidechain,中文叫侧链,也可以叫旁链,老实说,第一次看到这个概念我也是:
关于这个概念以后我会专门写一篇博客详细阐述一下,现在我只能告诉你侧链这玩意儿的出现完全是个意外。
It may shock you to learn that the initial applications for sidechain compression in the 1930s were not directed at making French filter house. Rather, it started with a film audio engineer named Douglas Shearer, who needed a way to tame the sibilance (the hard “s” sounds) of dialogue recordings. Sidechaining was born when Shearer conceived of a compressor with a “side” signal chain (separate to the main trigger signal) with an EQ slapped on it – rather than evenly compressing the incoming signal, this “de-esser” would only be triggered when the specific sibilant sounds appeared.
大家有兴趣可以看看Ableton的官方博客原文:https://www.ableton.com/en/blog/sidechain-compression-part-1/
这是FL Studio的一张截图,(因为比较直观),Sidechain其实就是把2号轨的声音信号,传送到1号轨,但是这个声音信号不会经过主轨(Master),“旁” 路 “链” 接,大概是这个意思吧,所以这样,
1号轨上的压缩器就可以针对2号轨的信号对1号轨自身信号做出一系列调整。
大家请听示例:
<AppAudioDiff set={[ {src: 'https://blog-r2.jw1.dev/p_assets/202009/sc-diff-1.mp3', label: 'Bass没有侧链,和鼓组糊成一团'}, {src: 'https://blog-r2.jw1.dev/p_assets/202009/sc-diff-2.mp3', label: 'Bass有侧链,可以清晰听到鼓组中的kick,snare和tom'} ]} client:only />
从没有侧链的音频可以感受到,因为bass和kick都属于比较低频的声音,所以他俩打架了,让鼓组失去了爆发力。而有侧链的那段音频,我们可以很清晰的听到一系列频率较低的打击乐,我们用侧链让bass给kick主动让开了一条路。可以仔细听听bass solo的时候有侧链和没侧链的区别。
<AppAudioDiff set={[ {src: 'https://blog-r2.jw1.dev/p_assets/202009/bass-diff-1.mp3', label: 'Bass没有侧链'}, {src: 'https://blog-r2.jw1.dev/p_assets/202009/bass-diff-2.mp3', label: 'Bass有侧链'} ]} client:only />
当然了,为了区分明显,demo都比较极端,实际应用还是要看个人喜好或客户需求。在整体的混音作品里,我们可以使用侧链来突出任何你想要突出的元素,比如用人声的信号去侧链伴奏,让伴奏让开空间给人声,又比如在电子音乐中用lead去侧链chord/pad,等等等等。而这些全都离不开压缩器,这也是为什么压缩器在音乐制作业内备受关注的原因。
<br/>
好了,今天就讲到这里,希望这篇文章对你有帮助!
]]>Visual Studio Code(简称VS Code)是一个由微软开发,同时支持Windows 、 Linux和macOS等操作系統且开放源代码的程式碼编辑器[4],它支持測試,并内置了Git 版本控制功能,同时也具有开发环境功能,例如代码补全(类似于 IntelliSense)、代码片段和代码重构等,该编辑器支持用户個性化配置,例如改变主题颜色、键盘快捷方式等各种属性和参数,同时还在编辑器中内置了扩展程序管理的功能。
2015年,VS Code发布了他的第一个版本,渐渐的,这个刚出世不久的代码编辑器就占领了大半江山。它的优点不计其数,各大论坛讲的天花乱坠,但也不是吹,VS Code确实有很多优点,但是,缺点也很明显:
基于Electron
为什么说基于Electron是他的缺点呢,对于一些电脑比较好的程序员来讲,可能感受不到,但是如果你的电脑是低压CPU,机械硬盘,内存也不大,你就能感觉到了——很卡,特别卡,尤其使用了一段时间之后,打开速度几乎和IDE无异。但是有时候你可能只是想用来编辑一个小文件,或者突然有了灵感,想记录下来,那么VS Code那接近半分钟的启动速度明显不是你想要的。今天就来教大家怎么加快VS Code的启动和优化它的运行速度。
首先进入VS Code,按下ctrl/cmd
+ ,
进入设置,然后点击设置界面右上角的小图标,打开settings.json
;
关闭小地图或者关闭小地图的渲染真实字符
// 不渲染真实字符
"editor.minimap.renderCharacters": false
// 关闭小地图
"editor.minimap.enabled": false
关闭动画
// 关闭平滑滚动
"editor.smoothScrolling": false
// 关闭光标平滑滚动
"editor.cursorSmoothCaretAnimation": false
缩进提示
如果您是初级程序员,建议保留。
"editor.renderIndentGuides": false
关闭自动高亮
// 单击在某一字符串上的自动匹配高亮
"editor.occurrencesHighlight": false
// 选中在某一字符串上的自动匹配高亮
"editor.selectionHighlight": false
鼠标hover时的弹窗
"editor.hover.enabled": false
"editor.hover.sticky": false
关闭代码大纲(面包屑
"breadcrumbs.enabled": false
关闭颜色预览
"editor.colorDecorators": false
以上7步如果全部做完,你的VS Code相对以前会快很多,如果想要更快,你还可以再添加以下配置:
括号配对高亮
"editor.matchBrackets": false
当前行高亮
"editor.renderLineHighlight": "none"
关闭链接的渲染
"editor.links": false
好了,以上就是我用VS Code一年多总结出来的经验,希望这篇文章有帮到你们!
Happy coding!
✌
]]>不过不是我做(欸嘿),别人也没做出来,我也没站出来(面子最重要),于是领导不得不放低要求,当然这都是后话了。
后来想,如果我来做的话,我会怎么做?对于领导的要求,我脑子里第一个闪过的就是background-size: cover
这个玩意儿,但是第一,这个是CSS 3的属性,IE 8就歇菜了,更别说IE 7了,第二,这个属性只对图片有用......
那能不能用js来模拟呢?
说干就干,当下就开始研究cover
属性是怎么工作的,最后结论是:
cover
在正常情况下,只是单纯的将图片宽度放到与容器一样宽,在缩放过程中,当图片高度小于容器高度时,会根据原始图片的宽高比(ratio)来放大图片宽度,使原始图片始终撑满容器。
听起来有点绕,直接上代码:
;(function(w) {
var kkkover = function () {
_ = this;
this.init = function (obj) {
w.onresize = function () {
_.resized(obj);
}
obj.img.onload = function () {
obj.ratio = obj.img.clientHeight / obj.img.clientWidth;
_.calcStyle(obj);
}
obj.ratio = obj.img.clientHeight / obj.img.clientWidth;
_.calcStyle(obj);
}
this.calcStyle = function (obj) {
obj.wrapCell.style.position = 'relative';
obj.img.style.position = 'absolute';
obj.img.style.top = '50%';
obj.img.style.zIndex = '0';
obj.img.style.left = '50%';
obj.img.style.width = '100%';
obj.img.style.marginLeft = -(obj.img.clientWidth / 2) + 'px';
obj.img.style.marginTop = -(obj.img.clientHeight / 2) + 'px';
if(obj.img.clientHeight <= obj.wrapCell.clientHeight){
obj.img.style.maxWidth = 'none';
obj.img.style.height = obj.wrapCell.clientHeight;
obj.img.style.width = obj.wrapCell.clientHeight / obj.ratio + 'px';
obj.img.style.marginTop = -(obj.img.clientHeight / 2) + 'px'
obj.img.style.marginLeft = -(obj.img.clientWidth / 2) + 'px';
}
if(obj.wrapCell.clientWidth > obj.img.clientWidth){
obj.img.style.width = '100%';
obj.img.style.height = 'auto';
obj.img.style.marginLeft = -(obj.img.clientWidth / 2) + 'px';
obj.img.style.marginTop = -(obj.img.clientHeight / 2) + 'px';
}
}
this.resized = function (obj) {
_.calcStyle(obj);
}
}
var app = new kkkover();
var wp = w.prototype || w.__proto__ || w;
wp.kkkover = app.init;
}(window));
使用的话大概就是这样:
<div id="test">
<img src="path/to/img" id="test_img">
</div>
kkkover({
wrapCell: document.getElementById('test'), // 传入容器
img: document.getElementById('test_img') // 传入容器中的图片
});
可以在这里看一下演示。代码已经测试,可以兼容到IE 7。
对于视频也是一样的用法,img
传视频元素就可以了。
✌
]]>17岁,大部分同龄人还在读高二的年纪,我却已经走上了实习岗位(并不自豪),第一份工作是上汽旗下的A车站当技术员。说白了就是修车,不过说是修车,其实呢,是拿着¥1390/月的工资,洗了八个月的车。辞职那天,有点小雨,在去地铁站的路上一直在想,这就是以后我的工作吗?这样的工作我真的能坚守并热爱下去吗?
在家闲了两个星期后,我妈决定让我先随便找一份工作。于是在申通找了个仓库管理的工作,每天的工作大概就是搬东西,清点货品,爬货架,和同事唠嗑。总之,忙起来非人类,闲起来特无聊。大概做了一个月后,收到奶奶病重的消息,于是又辞职了,拿着两千多块钱回老家了。
奶奶在2015年5月去世,办完丧事后回到上海,偶然间看到华育国际 *的PHP培训广告。于是说服父母,打算转行做程序员。
在讲师放了一堆PPT和视频之后,我就觉得,“哇,这简直就是我梦寐以求的工作!”
然后,现实他就给了我一耳光。我发现我的耐心很差,后面学到Javascript的时候就有点心不在焉了。后面三个月的重点PHP课程是真的一点都没听进去,钱倒是浪费了一大半。后来找工作的时候没有办法,只能硬着头皮去面试前端。
不知道经过多少次碰一鼻子灰后,终于有一家公司录用(收留)了我。这家公司主要就是帮别人建网站的,所以工作很纯粹,就是切图,设计师给我PSD,我出HTML,没有五险一金,过年过节也没有福利,但是有些地方却让我慢慢的驱散了之前找不到工作时的不安和来自父母的压力。不管技术经理还是设计师,对我这个新人都非常有耐心,遇到问题也不会对我大喊大叫,而是很温柔的教我应该怎么处理才是正确的。这样的氛围,让我感觉整个世界都亮了起来,让我更加坚定了自己要当一个程序员的想法。就是这一年,让我明白培训机构教的东西,真的只是皮毛而已。也是这一年,让我的切图技能达到了饱和状态。后来离职,老板没有意外,他也知道在他那里再待下去,也没有什么提升空间了。
这是一家媒体公司,主要做音频优化和影音设备的,我也不明白他们为什么会需要前端,也不明白为什么会选到我,刚开始我的工作就是切切图,美化一下表单。
再后来,公司终于决定搞个大项目——在线音频优化平台。
项目研发的五个月里,我从一个不太会Javascript的小白,变成了一个可以独挡一面的中级前端工程师,从一个完全没接触过Linux的菜鸟,拥有了一个运维实习生的水平,也对各种工具有了很深的了解,而最主要的一件事,就是让我的PHP技能入门了。这着实让我很开心,在2018年,我拥有了自己的第一个域名,拥有了硕大互联网中的一个旮旯。
项目开发几近尾声,却被突然叫停,后来我上班的内容就是每天打游戏,刷油管。
我变成了一个名副其实的摸鱼族。
居安思危,居安思危啊各位!
就这样浪费了小半年时间后,公司发生变故,据上级领导讲,是融资出现问题,这时我的工资已经推迟三个月没有发,社保和公积金也停止了缴纳,无奈之下,选择离职。
很巧,现在公司,和以前修车的地方,只有100米的距离,由于急着要续社保和公积金,在这里工作也只是缓兵之计。后来,事实证明我错了,自认为已经能写好Javascript的我,在这里才知道原来JS还有一种叫封装的写法,这不禁让我感到无比惭愧。而现在我只想躲在这个小角落,潜心修行。
<hr>
如果我的职业生涯重新开始,我会不会选择同样的道路?
我想我会的,因为这世上没有后悔药,也没有时光机。无论我怎么后悔,怎么不甘,我都要接受现实。只是现在我可以把我踩过的泥坑和跌过的跟头,分享给你们作为参考,不要再犯我犯过的错误。
本来这篇文章的标题叫《写给前端新人》的,但是我不想当一个人生导师一样的人物,我也不够格。以上,有对自己的批判,也有对所有新人的期望吧。
谢谢阅读!
<hr>
注:
首先看看html吧
<audio src="path/to/your/audio/file"></audio>
<!-- 或者 -->
<audio>
<source src="path/to/your/audio/file" type="audio/mpeg">
</audio>
这时候打开页面你会发现...什么都没有。
<audio src="path/to/your/audio/file" controls></audio>
加上controls
这个属性,浏览器才会显示原生的音频组件。至于浏览器兼容性我就不讲了,还是老样子扔个MDN链接在这里😁:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#Browser_compatibility
所以,搞定了?
不!像我这种有强迫症的人,怎么可能让每个浏览器的显示都不一致!
要做的第一件事情就是,隐藏audio
的原生控制组件:
<audio src="path/to/your/audio/file"></audio>
<!-- 去掉 controls -->
(👆三遍了🤣
然后用CSS将audio
移出文档流:
audio{position:fixed;top:0;left:-12138px;}
为什么要这么做?
—— 为了避免一些非主流的浏览器会让audio
在没有controls
属性的情况下出现在不应该出现的地方。(好累
接下来看JS的部分
let audio = document.querySelector('audio')
// 或者用jQuery
let audio = $('audio')
console.log(audio)
原生JavaScript在log下只能看到标签,而jQuery则会把所有和这个元素有关的东西都列出来,不论是有用的还是没有用的。但是知道有那么多属性/事件可以用是件好事,在这里说几样比较重要的。
接下来js区域中的
audio
都是以原生js获取的对象,如果你使用jQuery,audio赋值应该为let audio = $($('audio')[0])
可以调用此方法来播放音频。因为市面上大多数浏览器不允许直接用js触发音乐播放,必须要由用户来触发该事件('click', 'scroll', 'focus', etc.),所以我们可以加一个播放按钮来触发事件:
<audio src="path/to/your/audio/file"></audio>
<button class="play">play</button>
// 获取play button
let playBtn = document.querySelector('.play');
// 给play button添加点击事件
playBtn.addEventListener('click',function(){
// play button 点击后播放音频
audio.play()
})
调用audio.pause()
来暂停音频。
调用audio.paused
来获取音频暂停状态,true
为暂停状态,false
则为播放状态。
比如我现在需要再点击一次play button来暂停音频,我们可以这样做:
playBtn.addEventListener('click',function(){
// 点击后判断音频是否为暂停状态
if(audio.paused){
// 如果audio.paused为true
// 则音频为暂停状态
// 这时候我们就要播放音频
// 同时修改button的文字为pause
audio.play()
playBtn.innerHTML = 'pause'
}else{
// 如果audio.paused为false
// 则音频为播放状态
// 这时候我们就要暂停音频
// 同时修改button的文字为play
audio.pause()
playBtn.innerHTML = 'play'
}
})
调用此属性获取音频当前已播放时间。
修改此属性可以达到切换音频当前播放位置的功能:
// 快进十秒
fastForward = function(){
audio.currentTime = audio.currentTime + 10
}
fastForward()
调用此属性获取音频的长度,单位为秒。
已知问题:
Firefox
上会出现读不到duration的情况,可能和修改audio.src
有关,我在自己项目中的做法是使用了ffmpeg
读取出duration,直接当作变量使用,没有使用浏览器给的这个属性。此方法在音频播放期间会循环调用,经测试,调用间隔大约为200ms一次。
此方法可以用在更新音频进度条上。有了上面的audio.currentTime
和audio.duration
,我们可以算出当前音频播放进度的百分比:
// 将百分比定为全局变量
let audioProgressPercent = 0;
audio.ontimeupdate = function(){
// 在audio时间更新的时候计算当前进度百分比
audioProgressPercent = audio.currentTime / audio.duration
console.log(audioProgressPercent)
}
这个时候如果你打开页面开始播放音频,就会看到控制台一直在更新进度百分比了,至于这个数值怎么用,不用我教大家了吧。😁
调用此属性可以获取当前音频的缓冲进度和区间:
// 每200ms获取一次缓冲区间
let bufferedTimeInterval = setInterval(function(){
// 缓冲可以有多个区间
// 所以我们尽量都获取到
let bufferedLength = audio.buffered.length
// 以bufferedLength做循环
for (let i = 0; i < bufferedLength; i++) {
let bufferedStart = audio.buffered.start(i)
let bufferedEnd = audio.buffered.end(i)
console.log(bufferedStart, bufferedEnd)
}
},200)
控制台输出:
> 0 17.64
56.45 78.90
输出的含义为:
这一次读取audio.buffered
得到了两个区间,所以循环了两次。
第一次获得的区间是[0,17.64]
,代表第0秒到第17.64秒之间的音频已经缓冲好了,可以直接播放。
第二次获得的区间是[56.45,78.90]
,代表这两个数字之间的音频已经缓冲好了,可以直接播放。
audio.onwaiting
会在音频开始缓冲的时候被激活;
audio.onplay
会在音频开始播放的时候被激活;
这时候我们可以做一个loading的动画,告诉用户音频正在加载了:
audio.onwaiting = function () {
console.log('audio is loading')
// 在这里激活loading动画
}
audio.onplay = function () {
console.log('audio is playing')
// 在这里关闭loading动画
}
audio.ended
顾名思义,会在音频结束后被激活;
audio.src
是音频文件的位置,可修改;
当你有一个播放列表,且想在音频结束后播放下一首歌,你就可以:
// 定义播放列表
let playList = [
'path/to/song1',
'path/to/song2',
'path/to/song3'
]
// 定义当前播放歌曲的ID
let currentSongID = 0;
// 音频结束后自动播放下一首
audio.onended = function(){
// 更新当前播放歌曲的ID
currentSongID = currentSongID + 1
// 如果当前播放歌曲为播放列表中最后一首
// 就从第一首开始放
if(currentSongID >= playList.length){
currentSongID = 0
}
// 改变audio的src属性
audio.src = playList[currentSongID]
audio.play()
}
哦还有!
调用获取音频的音量,可修改;
可以做点fade in / fade out的效果,使用jQuery可以很简单的做出来,原文中也有原生js的实现方法(太长没看
$audio.animate({volume: newVolume}, 1000)
原文链接:https://stackoverflow.com/questions/7451508/html5-audio-playback-with-fade-in-and-fade-out
好了,以上就是我开发音乐主页之后想和大家分享的。
至于题中的问题,我的答案是:真的很简单。
只是js有一些性能上的限制,不能什么功能都往网页上搬...
在我的音乐主页中有一个砍掉的功能
—— 节拍器
为啥砍掉了呢...
因为js的处理效率真的不高(或者只是我太辣鸡),中低端手机做出来的节拍器根本不准,至于实现原理,很简单:
歌曲的bpm(beat per minute)是已知的:
// 假设现在我有一首歌bpm为120
let bpm = 120
// 每40毫秒更新一次,目的是让bpm更新不低于24fps
// 然而中低端手机的效果却不尽人意
// pc端倒是没啥问题
let bpmTimeInterval = setInterval(function(){
// 计算每秒有多少beats
let interval = bpm / 60
// _b会在时间线呈现出一个锯齿状的图形
// 最高那个点,也就是齿尖,就是一个beat
let _b = audio.currentTime % interval
// ...
},40)
希望有一天js能有开发Digital Audio Workstation的能力!
✌
]]>之前想到了一大堆点子,实现上虽然也不算很困难,但是总觉得还是简单一点好(说白了还是懒)。
Anyway, 以后一定会多多更新(假话),发一些有用的东西。
博客第一天上线也没有准备什么东西,溜了!😂
]]>