“Note Digger”——音符挖掘者,即扒谱。模仿的是软件wavetone,但是是双击即用、现代UI的前端应用。
目标是全部自己造轮子!即:不使用框架、不使用外部库;目的是减小项目大小,并掌握各个环节。目前频谱分析的软件非常多,功能也超级强大,自知比不过……所以唯一能一战的就是项目体积了!作为一个纯前端项目,就要把易用的优点完全发扬!
在线使用
视频演示(视频发布与更细节奏对齐之前)
- 在线or下载到本地,用主流现代浏览器打开(开发使用Chrome)。
- 导入音频——文件-上传,或直接将音频拖拽进去!
- 选择声道分析,或者导入之前分析的结果(只有选择音频之后才有导入之前结果的接口)
- 根据频谱分析,开始绘制midi音符!调整音量,反复比对。
- 导出为midi等,或者暂时导出项目(下次继续)
- 导出进度: 结果是.nd的二进制文件,保存分析结果(频谱图)和音符音轨。导入的时候并不会强制要求匹配原曲!(会根据文件名判断一下,但不强制)
- 导出为midi: 有两个模式。模式一只保证能听,节拍默认4/4,bpm默认60,midi类型默认1(同步多音轨);模式二会根据小节线进行对齐,可以直接用于制谱,算法概述见下面“节奏对齐”。两个模式下第10轨都不会分配为鼓点轨(本项目设计并不考虑扒鼓)。
- 导入midi: 将midi音符导入,只保证音轨、音符、音色能对应,音量默认127。如果导入后没有超过总音轨数,会在后面增加;否则会覆盖后面几轨(有提示)。
- 空格: 播放
- 双击时间轴: 从双击的位置开始播放
- 在时间轴上拖拽: 设置重复区间
- 在时间轴上拉动小节线: 设置小节bpm
- 鼠标中键时间轴: 将时间设置到点击位置,播放状态保持上一刻
- 鼠标右键时间轴(上半/下半): 具体设置重复时间/小节
- 按住空白拖动: 在当前音轨绘制一个音符
- 按住音符左半边拖动: 改变位置
- 按住音符右半边拖动: 改变时长
- Ctrl+点击音符: 多选音符
- delete: 删除选中的音符
- Ctrl+滚轮: 横向缩放
- 按住中键拖拽、触摸板滑动: 移动视野
- ←↑→↓: 视野移动一格
只有在导入并分析音频之后才能使用这些快捷键
- Ctrl+Z: 撤销(音轨状态的改变不会引发存档,且只记录16次历史)
- Ctrl+Y: 重做
- Ctrl+A: 全选当前音轨
- Ctrl+Shift+A: 全选所有音轨
- Ctrl+D: 取消选中
- Ctrl+C: 复制选中的音符
- Ctrl+X: 剪贴选中的音符
- Ctrl+V: 粘贴到选中的音轨上(暂不实现跨页面粘贴)
- Ctrl+B: 呼出/收回音轨面板
- Shift+右键: 菜单,包含撤销/重做、复制/粘贴、反选当前轨、删除
- 滑动条,如果旁边有数字,点击就可以恢复初始值。
- 多次点击“笔”右侧的选择工具,可以切换选择模式。(注意,只能选中当前音轨的音符)
- 点击某个音符可以选中该轨。
推荐使用常见的mp3、wav文件;除此之外,视频类文件也可以使用,比如mp4、mov、m4v。 但是如下格式不支持(浏览器API不支持解析)(仅仅在Chrome浏览器尝试过):
- aiff(苹果的音频格式)
对于ios的Safari浏览器,上音频文件也许有些困难。可以选择视频。(不过为什么要用触屏控制啊,根本没适配)
分析-自动填充,原理是将大于阈值的标记出来,效果不堪入目……目前没有什么好的算法。如有想法欢迎call me
我一直以来都是扒数字谱的,所以没关注过节奏。但是只能用于数字谱这个应用也太弱了。所以加入了小节对齐功能。“丑话说在前面”,绘制音符大概是不可能对齐小节线了(但是导出midi的时候会对齐),需要强迫症忍受一下。
乐谱的单位是"x分音符",而音乐的单位是"秒"。如果要实现"小节对齐",单位要换成"x分音符"。整个程序时间轴一定要按照"秒"为单位,这是由频谱分析决定的;如果要实现制谱软件一样的对齐,那么音符绘制需要换成"x分音符"的对齐方式。这意味着在120bpm的小节下的音符,拉到60bpm的小节下,在以秒为尺度的时间轴下,音符会变长。wavetone就是这样处理的。
但是对着原曲扒谱,最好还是根据"秒"来绘制音符。用wavetone扒谱的体验中,我最讨厌的就是被"x分音符"限制。用秒可以保证和原曲完全贴合,使用很灵活。但是这样导出的midi就不能直接制谱。按照"x分音符"来绘制音符还会导致程序很难写。开发者和使用者都不快乐。
扒谱用秒为单位合适,而制谱用x分音符合适。为了跨越这个鸿沟,我决定这样设计程序:使用midi文件作为对外的桥梁,在我的程序内用秒为单位扒谱,导出为midi的时候根据小节进行四舍五入的量化,形成规整的midi用于制谱。具体实现是:在秒轴上加入小节轴,用户可以拖动小节轴的某个小节调节后面紧跟的bpm相同的小节。小节轴只提供视觉上的辅助,对于画音符没一点限制。
对齐算法有一定的限制,比如四分音符按照八分音符的划分对齐、八分音符按十六分音符的划分对齐……比如四分音符不可能在第三个16分音符开始,只可能在整数倍个8分音符的时长处开始。所以,绘制音符的时候到底可以偏差小节线多远心里有数了吧?
│ app.js: 最重要的文件,主程序
| beatBar.js: 节奏信息的稀疏数组存储
│ channelDiv.js: 多音轨的UI界面类, 可拖拽列表
│ contextMenu.js: 右键菜单类
│ favicon.ico: 小图标
│ index.html: 程序入口, 其js主要是按钮的onclick
│ LICENSE
│ midi.js: midi创建、解析类
│ myRange.js: 横向滑动条的封装类
│ README.md
│ saver.js: 二进制保存相关
│ siderMenu.js: 侧边栏菜单类
│ snapshot.js: 快照类, 实现撤销和重做
│ tinySynth.js: 合成器类, 负责播放音频
| fakeAudio.js: 模拟了不会响的Audio,用于midi编辑器模式
│ todo.md: 一些设计思路和权衡
│
├─dataProcess
| │ analyser.js: 频域数据分析与简化
| │ fft_real.js: 执行实数FFT获取频域数据
| │ midiExport.js: 对绘制的音符进行近似以导出为足以制谱的midi
| │
| └─CQT
| │ cqt.js: 开启worker进行后台CQT
| │ cqt.wasm.js: emcc编译的胶水代码
| │ cqt.wasm.wasm: emcc编译的wasm
| │ cqt_wasm.cpp: wasm源文件
| │ cqt_worker.js: 新线程
| │
| └─.vscode
| c_cpp_properties.json: 环境配置
| tasks.json: emcc编译命令
|
├─img
│ github-mark-white.png
│ logo-small.png
│ logo.png
│ logo_text.png
│
└─style
│ askUI.css: 达到类似<dialog>效果
│ channelDiv.css: 多音轨UI样式
│ contextMenu.css: 右键菜单样式
│ myRange.css: 包装滑动条
│ siderMenu.css: 侧边菜单样式
│ style.css: index中独立元素的样式
│
└─icon: 从阿里图标库得到的icon
iconfont.css
iconfont.ttf
iconfont.woff
iconfont.woff2
引入了理论上更精确的CQT分析。非file协议时(不是双击html文件打开时),当STFT(默认的计算方法)计算完成会在后台自动开启CQT计算,CQT结果将与当前频谱融合(会发现突然频谱变了)。CQT计算非常慢,因此在后台计算以防阻塞,且用C++实现、编译为WASM以提速。
中途遇到很多坑,记录分布在/dataProcess/CQT的各个文件中,但效果其实并不值得这样的计算量。5分30秒的音频进行双声道CQT分析,需要45秒(从开启worker开始算),和直接进行js版的CQT用时差不多,加速了个寂寞。
关于CQT的研究,记录在《CQT:从理论到代码实现》。
此外尝试了“一边分析一边绘制频谱”,试图通过删除进度条达到感官上加速的效果。但是放在主线程造成严重卡顿,放弃。
完成了issue2:不导入音频的midi编辑器。点击文件菜单下的“MIDI编辑器模式”就可以进入。
视野的宽度取决于最后一个音符,模仿的是signal。也尝试过自动增加视野,可以一直往右拉,但是这样在播放的时候,开启“自动翻页”会永远停不下来(翻一页就自动拓展宽度)。
扒谱框架下的midi编辑器还是有些反人类,因为绘制音符时的单位是时间而不是x分音符。不过也能用。
原理是实现了一个空壳的Audio,只有计时功能,没有发声功能。一些做法写在了todo.md上。
加入了节拍对齐功能,使用逻辑是:扒谱界面提供视觉辅助,导出midi会自动对齐,以实现制谱友好。详细对齐的原理请参看“关于节奏对齐”板块和midiExport.js文件。
有一些细节:
-
如果每个小节bpm都不一样(原曲的速度不稳,有波动),那导出midi前的对齐操作会以上一小节bpm为基准进行动态适应:先根据本小节的bpm量化音符为"x分音符",如果本小节bpm和上一小节的bpm差别在一定范围内,则再将"x分音符"的bpm设置全局量BPM;否则将全局BPM设置为当前小节的bpm。这个算法的要求是:的确要变速的前后bpm差异应该较大。
-
在一个小节内,音符的近似方法:
- 记一个四分音符的格数为aqt(因为音符的实际使用单位是格。这里隐含了一个时间到格数的变换),某时刻t对应音符长度为ntlen,小节开始时刻记为mt。首先获取音符时刻相对小节开头的位置nt=t-mt。(音符时刻:将一个音符拆分为开始时刻和结束时刻。一个音符可能跨好几个小节,因此这样处理最为合适)
- 假设前提:时长长的音符的起点和终点的精度也低(精度这里指最小单位时长,低精度指单位时长对应的实际时间长)。因此近似精度accu采用自适应的方式:该音符可以用(ntlen/aqt)个四份音符表示,设其可以用一个(4*2^n)分音符近似,其中n满足:(1/2)^n<=ntlen/aqt<(1/2)^(n-1),则该音符的时长为aqt/(2^n),则精度设置为这个近似音符的一半:accu = aqt/(2^(n+1))。比如四份音符的精度是一个八分音符的时长。
- 近似后的时刻为:round(nt/accu)*accu。同时设置一个最低精度:八分音符。因此accu=min(aqt/2, aqt/(2^(n+1))),其中(1/2)^n<=ntlen/aqt<(1/2)^(n-1)。
-
小节信息如何存储、数据结构如何设计需要好好想想。大部分情况下(在原音频节奏稳定的情况下)只会变速几次,此时存变动时刻的bpm值就足矣。极端情况下每个小节都单独设置了bpm。如何设计数据结构能在两种情况下都取得较好的性能?使用稀疏数组。
在今年完成了所有基本功能!本次更新了设置相关,简单地设计了调性分析的算法,已经完全可以用了!【随后在bilibil投稿了视频】
文件系统已经完善!已经可以随心所欲导入导出保存啦!同时修复了一些小bug、完善了一些api。
界面上,本打算将文件相关选项放到logo上,但是侧边菜单似乎有些空了,于是就加入到侧边栏,而logo设置为刷新或开新界面(考察了其他网站的logo的用途)。同时给侧边菜单加入了“设置”和“分析”,但本次更新没做。
midi相关操作来自我的另一个项目的midi类。将用midi转的wav导入分析,再导入原midi,两者同步播放的感觉真好!
已经能用于扒谱了!完成了midi和原曲的播放与同步,填补了扒谱过程最重要的一环。
UI基本完成!将侧边栏、滑动条封装成了js类。在此基础上,设计了类似VScode的菜单,用于存放不常用的功能和界面;而顶部窄窄一条用于放置常用功能。
此外,完成了logo的设计。在2月4日的commit记录中(因为现在已经删除)可以看到设计的多种logo,最终选定了“在勺子里的音符”,这是一个被勺子dig出来的音符。其他思路可以概括为:“音符和铲子的组合”(logo2)、“埋在地里的音符”(logo5 logo6)、“像植物一样生长的八分音符”(logo8 logo10)、“音符和铲子结合”(logo12)。
完成了多音轨、合成器和主线的整合,象征着midi系统的完成!
统一了UI风格;完善了快捷键功能;新增框选功能;修复了大部分bug。
完成了midi合成器tinySynth.js,实现了128种音色的播放。只有演奏音符的作用,控制器一点没做。
原理是多个基础波形合成一个音色。波形参数来自 https://github.com/g200kg/webaudio-tinysynth ,因此程序设计也参考了它的设计。修改记录在todo.md中
对于reference的解析(作者注释一点没写,变量命名极为简单,因此主要是变量解释)存放于“./tone/解析.md”(文件夹已被删除,请去历史提交查看)。文件夹中还有tinySynth的测试页面。在下一次push时将删除tone文件夹。
这段时间内还完成了以下内容(全部记录在commit history的comments内):
- 基本程序界面(三个画布:键盘、时频图、时间轴;UI界面:右键菜单、多音轨、滑动条)
- 基本逻辑功能:音符交互绘制、快捷键以及模块的关联协同
从11月14日开始造js版fft轮子起,时隔一个月第一次提交项目,因为项目逻辑日渐复杂,需要能及时回退。主要完成了频谱绘制、钢琴键盘绘制、数据处理三部分,并初步确定了程序的结构框架。
数据处理核心:实数FFT,编写于我《数字信号处理》刚刚学完FFT算法之时,针对本项目的应用场景做了专门的设计,即针对音频STFT做了适配,具体表现为:实数加速、数据预计算、空间预分配、共用数组。
由于整个项目还没搭建起来,因此不能测试NoteAnalyser类的数据处理效果。此类用于将频域数据进一步离散为音符强度数据。
关于程序结构有一版废案,在文件夹"deprecated"中,设计思路是解耦、插件化,废弃理由是根本解耦不了。因此现在的代码耦合成一坨了。这个文件夹将在下一次push时被删除,存活于历史提交之中。
tone文件夹将存放我的合成器轮子,audioplaytest是我音频播放的实验文件夹,todo.md是部分设计思路。
2024/4/8补记:时频分析方法是STFT,但是面临时间和频率分辨率矛盾的问题,现在的分析精度只能到F#2。解决办法是用小波变换,或者更本质一点:用84个滤波器提取84个基准音以及其周围的频率的能量。这样能达到更高的频率分辨率和时间分辨率。但是现在的STFT用起来效果还可以,就不换了哈。