Skip to content

Commit

Permalink
完成了设置和分析的UI功能
Browse files Browse the repository at this point in the history
  • Loading branch information
madderscientist committed Feb 9, 2024
1 parent c5ac1fe commit 13e2f74
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 24 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
5. 导出为midi等,或者暂时导出项目(下次继续)

## 导入导出说明
- 导出进度: 结果是.nt的二进制文件,保存分析结果(频谱图)和音符音轨。导入的时候并不会强制要求匹配原曲!(会根据文件名判断一下,但不强制)
- 导出进度: 结果是.nd的二进制文件,保存分析结果(频谱图)和音符音轨。导入的时候并不会强制要求匹配原曲!(会根据文件名判断一下,但不强制)
- 导出为midi: 只保证能听,节拍默认4/4,bpm默认60,midi类型默认1(同步多音轨)。第10轨不会分配为鼓点轨(本项目设计并不考虑扒鼓)。
- 导入midi: 将midi音符导入,只保证音轨、音符、音色能对应,音量默认127。如果导入后没有超过总音轨数,会在后面增加;否则会覆盖后面几轨(有提示)。

Expand Down Expand Up @@ -53,7 +53,53 @@
- 滑动条,如果旁边有数字,点击就可以恢复初始值。
- 多次点击“笔”右侧的选择工具,可以切换选择模式。


## 文件结构
```
│ app.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: 合成器类, 负责播放音频
│ todo.md: 一些设计思路和权衡
├─dataProcess
│ analyser.js: 频域数据分析与简化
│ fft_real.js: 执行实数FFT获取频域数据
├─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
```

## 重要更新记录
### 2024 2 9
在今年完成了所有基本功能!本次更新了设置相关,简单地设计了调性分析的算法,已经完全可以用了!

### 2024 2 8
文件系统已经完善!已经可以随心所欲导入导出保存啦!同时修复了一些小bug、完善了一些api。<br>
界面上,本打算将文件相关选项放到logo上,但是侧边菜单似乎有些空了,于是就加入到侧边栏,而logo设置为刷新或开新界面(考察了其他网站的logo的用途)。同时给侧边菜单加入了“设置”和“分析”,但本次更新没做。<br>
Expand Down
15 changes: 12 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function App() {
Object.defineProperty(this, 'width', {
get: function () { return this._width; },
set: function (w) {
if (w < 0) return;
if (w <= 0) return;
this._width = w;
this.TimeBar.updateInterval();
this.HscrollBar.refreshSize(); // 刷新横向滑动条
Expand All @@ -22,7 +22,7 @@ function App() {
Object.defineProperty(this, 'height', {
get: function () { return this._height; },
set: function (h) {
if (h < 0) return;
if (h <= 0) return;
this._height = h;
this.Keyboard._ychange.set([
-1.5 * h, -2 * h, -1.5 * h, -1.5 * h, -2 * h, -2 * h, -1.5 * h,
Expand Down Expand Up @@ -68,6 +68,7 @@ function App() {
colorStep2: 240,
multiple: parseFloat(document.getElementById('multiControl').value),// 幅度的倍数
_spectrogram: null,
mask: '#25262daa',
getColor: (value) => { // 0-step1,是蓝色的亮度从0变为50%;step1-step2,是颜色由蓝色变为红色;step2-255,保持红色
value = value || 0;
let hue = 0, lightness = 50; // Red hue
Expand Down Expand Up @@ -110,7 +111,7 @@ function App() {
ctx.fillRect(rectx, 0, w, canvas.height);
}
// 铺底色以凸显midi音符
ctx.fillStyle = '#25262daa';
ctx.fillStyle = sp.mask;
ctx.fillRect(0, 0, rectx, canvas.height);
// 更新note
ctx.fillStyle = "#ffffff4f";
Expand All @@ -131,6 +132,13 @@ function App() {
this.parent.scroll2(0, (this.parent._height * this.parent.ynum - this.parent.spectrum.height) >> 1); // 垂直方向上,视野移到中间
}
this.parent.HscrollBar.refreshSize();
},
get Alpha() {
return parseInt(this.mask.substring(7), 16);
},
set Alpha(a) {
a = Math.min(255, Math.max(a | 0, 0));
this.mask = '#25262d' + a.toString(16);
}
};
this.MidiAction = {
Expand Down Expand Up @@ -837,6 +845,7 @@ function App() {
}, {
name: "从此处播放",
callback: (e_father, e_self) => {
this.AudioPlayer.stop();
this.AudioPlayer.start((e_father.offsetX + this.scrollX) * this.dt / this._width);
}
}
Expand Down
51 changes: 50 additions & 1 deletion dataProcess/analyser.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,56 @@ class NoteAnalyser { // 负责解析频谱数据
}
}
// FFT的结果需要除以N才是DTFT的结果 由于结果太小,统一放大10倍 经验得到再乘700可在0~255得到较好效果
noteAm[i] = Math.sqrt(noteAm[i]) * 10 / real.length;
noteAm[i] = Math.sqrt(noteAm[i]) * 16 / real.length;
} return noteAm;
}
/**
* 调性分析,原理是音符能量求和
* @param {Array<Float32Array>} noteTable
* @returns {Array<String, Float32Array>} 调性和音符的能量
*/
static Tonality(noteTable) {
let energy = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (const atime of noteTable) {
energy[0] += atime[0]**2 + atime[12]**2 + atime[24]**2 + atime[36]**2 + atime[48]**2 + atime[60]**2 + atime[72]**2;
energy[1] += atime[1]**2 + atime[13]**2 + atime[25]**2 + atime[37]**2 + atime[49]**2 + atime[61]**2 + atime[73]**2;
energy[2] += atime[2]**2 + atime[14]**2 + atime[26]**2 + atime[38]**2 + atime[50]**2 + atime[62]**2 + atime[74]**2;
energy[3] += atime[3]**2 + atime[15]**2 + atime[27]**2 + atime[39]**2 + atime[51]**2 + atime[63]**2 + atime[75]**2;
energy[4] += atime[4]**2 + atime[16]**2 + atime[28]**2 + atime[40]**2 + atime[52]**2 + atime[64]**2 + atime[76]**2;
energy[5] += atime[5]**2 + atime[17]**2 + atime[29]**2 + atime[41]**2 + atime[53]**2 + atime[65]**2 + atime[77]**2;
energy[6] += atime[6]**2 + atime[18]**2 + atime[30]**2 + atime[42]**2 + atime[54]**2 + atime[66]**2 + atime[78]**2;
energy[7] += atime[7]**2 + atime[19]**2 + atime[31]**2 + atime[43]**2 + atime[55]**2 + atime[67]**2 + atime[79]**2;
energy[8] += atime[8]**2 + atime[20]**2 + atime[32]**2 + atime[44]**2 + atime[56]**2 + atime[68]**2 + atime[80]**2;
energy[9] += atime[9]**2 + atime[21]**2 + atime[33]**2 + atime[45]**2 + atime[57]**2 + atime[69]**2 + atime[81]**2;
energy[10] += atime[10]**2 + atime[22]**2 + atime[34]**2 + atime[46]**2 + atime[58]**2 + atime[70]**2 + atime[82]**2;
energy[11] += atime[11]**2 + atime[23]**2 + atime[35]**2 + atime[47]**2 + atime[59]**2 + atime[71]**2 + atime[83]**2;
}
// notes根据最大值归一化
let max = Math.max(...energy);
energy = energy.map((num) => num / max);
// 找到最大的前7个音符
const sortedIndices = energy.map((num, index) => index)
.sort((a, b) => energy[b] - energy[a])
.slice(0, 7);
sortedIndices.sort((a, b) => a - b);
// 判断调性
let tonality = sortedIndices.map((num) => {
return num.toString(16);
}).join('');
switch (tonality) {
case '024579b': tonality = 'C'; break;
case '013568a': tonality = 'C#'; break;
case '124679b': tonality = 'D'; break;
case '023578a': tonality = 'Eb'; break;
case '134689b': tonality = 'E'; break;
case '024579a': tonality = 'F'; break;
case '13568ab': tonality = 'Gb'; break;
case '024679b': tonality = 'G'; break;
case '013578a': tonality = 'Ab'; break;
case '124689b': tonality = 'A'; break;
case '023579a': tonality = 'Bb'; break;
case '13468ab': tonality = 'B'; break;
default: tonality = 'Unknown'; break;
} return [tonality, energy];
}
}
122 changes: 109 additions & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
<li>导出为midi</li>
<li id="numberedScore">
<button>转换为(更新)数字谱</button>
<textarea cols="30" rows="10"></textarea>
<textarea cols="30" rows="16"></textarea>
</li>
</ul>

Expand All @@ -111,20 +111,21 @@ <h3>EQ设置(dB)</h3>
</div>
</div>

<div class="paddingbox niceScroll">
( ̄ε(# ̄) 还没做

</div>
<ul class="paddingbox niceScroll btn-ul" id="analysePannel">
<li>调性分析</li>
( ̄ε(# ̄) 别的还没做
</ul>

<ul class="paddingbox niceScroll btn-ul" id="settingPannel">
<li><button>-</button>宽度<button>+</button></li>
<li><button>-</button>高度<button>+</button></li>
<li><button>-</button>遮罩透明度<button>+</button></li>
<li data-value="5"><button>-</button>宽度<button>+</button></li>
<li data-value="15"><button>-</button>高度<button>+</button></li>
<li data-value="170"><button>-</button>遮罩厚度<button>+</button></li>
<li>
<h5>精准设置重复区间</h5>
<input type="text" value="00:00:000" name="set-repeat"> ~
<input type="text" value="00:00:000" name="set-repeat">
<button>取消</button><button>应用</button>
精准设置重复区间
<div id="repeatRange">
<input type="text" value="00:01:000">~<input type="text" value="00:02:000">
</div>
<button>取消区间</button><button>应用</button>
</li>
</ul>
</div>
Expand Down Expand Up @@ -282,8 +283,17 @@ <h5>精准设置重复区间</h5>
});
}; input.click();
};
lis[2].onclick = () => app.Saver.write();
lis[2].onclick = () => {
if(!app.Spectrogram._spectrogram) {
alert("请先导入音频!");
return;
} app.Saver.write();
}
lis[3].onclick = () => { // midi导出
if(!app.Spectrogram._spectrogram) {
alert("请先导入音频!");
return;
}
const newMidi = new midi(60,[4,4],Math.round(1000/app.dt),[],app.AudioPlayer.name);
const mts = [];
for(const ch of app.synthesizer.channel) {
Expand Down Expand Up @@ -327,6 +337,92 @@ <h5>精准设置重复区间</h5>
};
}

// ==== 分析界面 ==== //
{
const lis = document.getElementById('analysePannel').children;
lis[0].onclick = function() {
if(!app.Spectrogram._spectrogram) {
alert("请先导入音频!");
return;
}
let [tonality, energy] = NoteAnalyser.Tonality(app.Spectrogram._spectrogram);
const div = document.createElement('div');
div.innerHTML = `<h5>调性: ${tonality}</h5>
<div class="tonalityResult">
<div style="background:#FF4500;width:${energy[0]*100}%;">C</div>
<div style="background:#FFD700;width:${energy[1]*100}%;">C#</div>
<div style="background:#32CD32;width:${energy[2]*100}%;">D</div>
<div style="background:#00BFFF;width:${energy[3]*100}%;">D#</div>
<div style="background:#FF6347;width:${energy[4]*100}%;">E</div>
<div style="background:#FF1493;width:${energy[5]*100}%;">F</div>
<div style="background:#7FFF00;width:${energy[6]*100}%;">F#</div>
<div style="background:#1E90FF;width:${energy[7]*100}%;">G</div>
<div style="background:#FFA500;width:${energy[8]*100}%;">G#</div>
<div style="background:#EE82EE;width:${energy[9]*100}%;">A</div>
<div style="background:#ADFF2F;width:${energy[10]*100}%;">A#</div>
<div style="background:#87CEFA;width:${energy[11]*100}%;">B</div>
</div>`;
this.appendChild(div);
};
}

// ==== 设置界面 ==== //
{
const lis = document.getElementById('settingPannel').children;
lis[0].firstChild.onclick = () => {
--app.width;
lis[0].dataset.value = app.width;
};
lis[0].lastChild.onclick = () => {
++app.width;
lis[0].dataset.value = app.width;
};
lis[1].firstChild.onclick = () => {
--app.height;
lis[1].dataset.value = app.height;
};
lis[1].lastChild.onclick = () => {
++app.height;
lis[1].dataset.value = app.height;
};
lis[2].firstChild.onclick = () => {
--app.Spectrogram.Alpha;
lis[2].dataset.value = app.Spectrogram.Alpha;
};
lis[2].lastChild.onclick = () => {
++app.Spectrogram.Alpha;
lis[2].dataset.value = app.Spectrogram.Alpha;
};
const repeatInput = lis[3].querySelectorAll('input');
function checkTime(time) {
const timeRegex = /^\d{1,2}:\d{1,2}:\d{1,3}$/;
return timeRegex.test(time);
}
function Time2Ms(time) {
const t = time.split(':');
return parseInt(t[0]) * 60000 + parseInt(t[1]) * 1000 + parseInt(t[2]);
}
repeatInput[0].oninput = repeatInput[1].oninput = function() {
this.style.color = checkTime(this.value) ? 'var(--theme-text)' : 'red';
};
const repbtn = lis[3].querySelectorAll('button');
repbtn[0].onclick = () => {
app.TimeBar.repeatStart = -1;
app.TimeBar.repeatEnd = -1;
};
repbtn[1].onclick = () => {
if(checkTime(repeatInput[0].value) && checkTime(repeatInput[1].value)) {
let i1 = Time2Ms(repeatInput[0].value);
let i2 = Time2Ms(repeatInput[1].value);
if(i1 > i2) {
let temp = i2;
i2 = i1; i1 = temp;
}
app.TimeBar.repeatStart = i1;
app.TimeBar.repeatEnd = i2;
} else alert('时间格式错误!');
}
}
// ==== 交互细节 ==== //
document.querySelector('.top-logo').addEventListener('click', () => {
// 如果已经有分析数据了,就打开新的界面
Expand Down
Loading

0 comments on commit 13e2f74

Please sign in to comment.