Skip to content

Commit

Permalink
finish data processing, preliminarily determine the program structure
Browse files Browse the repository at this point in the history
  • Loading branch information
madderscientist committed Dec 12, 2023
1 parent 60be585 commit 23e3c0c
Show file tree
Hide file tree
Showing 19 changed files with 1,370 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# noteDigger!
前端扒谱~模仿的是软件wavetone。
正在推进~目标是全部自己造轮子~
[demo](https://github.com/madderscientist/noteDigger/blob/main/index.html)

## 重要更新记录

### 2023 12 13
从11月14日开始造js版fft轮子起,时隔一个月第一次提交项目,因为项目逻辑日渐复杂,需要能及时回退。主要完成了频谱绘制、钢琴键盘绘制、数据处理三部分,并初步确定了程序的结构框架。
数据处理核心:实数FFT,编写于我《数字信号处理》刚刚学完FFT算法之时,针对本项目的应用场景做了专门的设计,即针对音频小波变换做了适配,具体表现为:实数加速、数据预计算、空间预分配、共用数组。
由于整个项目还没搭建起来,因此不能测试NoteAnalyser类的数据处理效果。此类用于将频域数据进一步离散为音符强度数据。
关于程序结构有一版废案,在文件夹"deprecated"中,设计思路是解耦、插件化,废弃理由是根本解耦不了。因此现在的代码耦合成一坨了。这个文件夹将在下一次push时被删除,存活于历史提交之中。
tone文件夹将存放我的合成器轮子,audioplaytest是我音频播放的实验文件夹,todo.md是部分设计思路。
396 changes: 396 additions & 0 deletions app.js

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions audioplaytest/audio.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="file" id="audioInput" accept="audio/*">
<button id="playButton">Play</button>
</body>
<script>
const audioInput = document.getElementById("audioInput");
const playButton = document.getElementById("playButton");
let audioContext = new AudioContext();
let audioElement = document.createElement('audio');
// let source = audioContext.createMediaElementSource(audioElement);

audioInput.addEventListener('change', function (e) {
let file = URL.createObjectURL(e.target.files[0]);
audioElement.src = file;
audioElement.play();
// source.connect(audioContext.destination);
});

playButton.addEventListener('click', function() {
audioContext.resume().then(() => {
audioElement.play();
});
});
// currentTime:当前播放的时间(以秒为单位)。
// duration:音频的总时长(以秒为单位)。
// volume:音量,范围是0.0(静音)到1.0(最大音量)。
// playbackRate:播放速度,1.0表示正常速度,大于1.0表示加速播放,小于1.0表示减速播放。
// paused:如果音频当前处于暂停状态,则为true,否则为false。
// play():开始播放音频。
// pause():暂停播放音频。
// load():重新加载音频。
</script>

</html>
42 changes: 42 additions & 0 deletions audioplaytest/buffer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<input type="file" id="audioInput" accept="audio/*">

</body>
<script>
const audioInput = document.getElementById("audioInput");

let audioContext = new AudioContext();
let source = null;

function decodeMusic(file) {
const fileReader = new FileReader();
fileReader.onload = function () {
const audioData = fileReader.result;
audioContext.decodeAudioData(audioData, function (decodedData) {
if (source) {
source.stop();
}
source = audioContext.createBufferSource();
source.buffer = decodedData;
source.connect(audioContext.destination);
source.start();
});
};
fileReader.readAsArrayBuffer(file);
}

audioInput.addEventListener('change', function (e) {
decodeMusic(e.target.files[0]);
});
</script>

</html>
69 changes: 69 additions & 0 deletions dataProcess/analyser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
class NoteAnalyser { // 负责解析频谱数据
static freqTable(A4) {
const freqTable = new Float32Array(84); // 范围是C1-B7
let Note4 = [
A4 * 0.5946035575013605, A4 * 0.6299605249474366,
A4 * 0.6674199270850172, A4 * 0.7071067811865475,
A4 * 0.7491535384383408,
A4 * 0.7937005259840998, A4 * 0.8408964152537146,
A4 * 0.8908987181403393, A4 * 0.9438743126816935,
A4, A4 * 1.0594630943592953,
A4 * 1.122462048309373
];
freqTable.set(Note4.map(v => v / 8), 0);
freqTable.set(Note4.map(v => v / 4), 12);
freqTable.set(Note4.map(v => v / 2), 24);
freqTable.set(Note4, 36);
freqTable.set(Note4.map(v => v * 2), 48);
freqTable.set(Note4.map(v => v * 4), 60);
freqTable.set(Note4.map(v => v * 8), 72);
return freqTable;
}
constructor(df, A4 = 440) {
this.df = df;
this.A4 = A4; // 中央A频率
this.freqTable = null; // 频率表
}
set A4(fre) {
this.freqTable = NoteAnalyser.freqTable(fre);
this.updateRange();
}
get A4() {
return this.freqTable[45];
}
updateRange() {
let at = Array.from(this.freqTable.map((value) => Math.round(value / this.df)));
at.push(Math.round((this.freqTable[this.freqTable.length - 1] * 1.059463) / this.df))
const range = new Float32Array(84); // 第i个区间的终点
for (let i = 0; i < at.length - 1; i++) {
range[i] = (at[i] + at[i + 1]) / 2;
} this.rangeTable = range;
}
/**
* 从频谱提取音符的频谱 原理是区间内求和
* @param {Float32Array} real 实部
* @param {Float32Array} imag 虚部
* @returns {Float32Array} 音符的幅度谱 数据很小
*/
analyse(real, imag) {
const noteAm = new Float32Array(84);
let at = this.rangeTable[0];
for (let i = 0; i < this.rangeTable.length; i++) {
let end = this.rangeTable[i];
if(at==end) { // 如果相等则就算一次 乘法比幂运算快
noteAm[i] = real[at] * real[at] + imag[at] * imag[at];
} else {
for (; at < end; at++) {
noteAm[i] += real[at] * real[at] + imag[at] * imag[at];
}
if (at == end) { // end是整数,需要对半分
let a2 = (real[end] * real[end] + imag[end] * imag[end]) / 2;
noteAm[i] += a2;
if (i < noteAm.length - 1) noteAm[i + 1] += a2;
}
}
// FFT的结果需要除以N才是DTFT的结果
noteAm[i] = Math.sqrt(noteAm[i])/real.length;
} return noteAm;
}
}
129 changes: 129 additions & 0 deletions dataProcess/fft_real.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* 目前我写的最快的实数FFT。为音乐频谱分析设计
*/
class realFFT {
/**
* 位反转数组 最大支持2^16点
* @param {Number} N 2的正整数幂
* @returns {[Uint16Array, Uint8Array]} 根据N的大小决定的位反转结果
*/
static reverseBits(N) {
const reverseBits = new Uint16Array(N); // 实际N最大2^15
let id = 0;
function _fft(offset, step, N) {
if (N == 2) {
// 由于是实数FFT,偶次为实部,奇次为虚部,故布局为2
reverseBits[id++] = offset << 1;
reverseBits[id++] = (offset + step) << 1;
return;
}
let step2 = step << 1;
N >>= 1;
_fft(offset, step2, N);
_fft(offset + step, step2, N);
}
_fft(0, 1, N);
return reverseBits;
}
/**
* 复数乘法
* @param {Number} a 第一个数的实部
* @param {Number} b 第一个数的虚部
* @param {Number} c 第二个数的实部
* @param {Number} d 第二个数的虚部
* @returns {Array} [实部, 虚部]
*/
static ComplexMul(a = 0, b = 0, c = 0, d = 0) {
return [a * c - b * d, a * d + b * c];
}
/**
* 计算复数的幅度
* @param {Float32Array} r 实部数组
* @param {Float32Array} i 虚部数组
* @returns {Float32Array} 幅度
*/
static ComplexAbs(r, i, l) {
l = l || r.length;
const ABS = new Float32Array(l);
for (let j = 0; j < l; j++) {
ABS[j] = Math.sqrt(r[j] * r[j] + i[j] * i[j]);
} return ABS;
}

/**
*
* @param {Number} N 要做几点的实数FFT
*/
constructor(N) {
this.ini(N);
this.bufferr = new Float32Array(this.N);
this.bufferi = new Float32Array(this.N);
this.Xr = new Float32Array(this.N);
this.Xi = new Float32Array(this.N);
}
/**
* 预计算常量
* @param {Number} N 2的正整数次幂
*/
ini(N) {
// 确定FFT长度
N = Math.pow(2, Math.ceil(Math.log2(N)) - 1);
this.N = N; // 存的是实际FFT的点数
// 位反转预计算 实际做N/2的FFT
this.reverseBits = realFFT.reverseBits(N);
// 旋转因子预计算 仍然需要N点的,但是只取前一半
this._Wr = new Float32Array(Array.from({ length: N }, (_, i) => Math.cos(Math.PI / N * i)));
this._Wi = new Float32Array(Array.from({ length: N }, (_, i) => -Math.sin(Math.PI / N * i)));
}
/**
*
* @param {Float32Array} input 输入
* @param {Number} offset 偏移量
* @returns [实部, 虚部]
*/
fft(input, offset = 0) {
// 偶数次和奇数次组合并计算第一层
for (let i = 0, ii = 1, offseti = offset + 1; i < this.N; i += 2, ii += 2) {
let xr1 = input[this.reverseBits[i] + offset] || 0;
let xi1 = input[this.reverseBits[i] + offseti] || 0;
let xr2 = input[this.reverseBits[ii] + offset] || 0;
let xi2 = input[this.reverseBits[ii] + offseti] || 0;
this.bufferr[i] = xr1 + xr2;
this.bufferi[i] = xi1 + xi2;
this.bufferr[ii] = xr1 - xr2;
this.bufferi[ii] = xi1 - xi2;
}
for (let groupNum = this.N >> 2, groupMem = 2; groupNum; groupNum >>= 1) {
// groupNum: 组数;groupMem:一组里有几个蝶形结构,同时也是一个蝶形结构两个元素的序号差值
// groupNum: N/4, N/8, ..., 1
// groupMem: 2, 4, ..., N/2
// W's base: 4, 8, ..., N
// W's base desired: 2N
// times to k: N/2, N/4 --> equals to 2*groupNum (W_base*k_times=W_base_desired)
// offset between groups: 4, 8, ..., N --> equals to 2*groupMem
let groupOffset = groupMem << 1;
for (let mem = 0, k = 0, dk = groupNum << 1; mem < groupMem; mem++, k += dk) {
let [Wr, Wi] = [this._Wr[k], this._Wi[k]];
for (let gn = mem; gn < this.N; gn += groupOffset) {
let gn2 = gn + groupMem;
let [gwr, gwi] = realFFT.ComplexMul(this.bufferr[gn2], this.bufferi[gn2], Wr, Wi);
this.Xr[gn] = this.bufferr[gn] + gwr;
this.Xi[gn] = this.bufferi[gn] + gwi;
this.Xr[gn2] = this.bufferr[gn] - gwr;
this.Xi[gn2] = this.bufferi[gn] - gwi;
}
}
[this.bufferr, this.bufferi, this.Xr, this.Xi] = [this.Xr, this.Xi, this.bufferr, this.bufferi];
groupMem = groupOffset;
}
// 合并为实数FFT的结果
this.Xr[0] = this.bufferi[0] + this.bufferr[0];
this.Xi[0] = 0;
for (let k = 1, Nk = this.N - 1; Nk; k++, Nk--) {
let [Ir, Ii] = realFFT.ComplexMul(this.bufferi[k] + this.bufferi[Nk], this.bufferr[Nk] - this.bufferr[k], this._Wr[k], this._Wi[k]);
this.Xr[k] = (this.bufferr[k] + this.bufferr[Nk] + Ir) * 0.5;
this.Xi[k] = (this.bufferi[k] - this.bufferi[Nk] + Ii) * 0.5;
}
return [this.Xr, this.Xi];
}
}
40 changes: 40 additions & 0 deletions dataProcess/testFFT.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./fft_real.js"></script>
</head>

<body>
<canvas id="spectrum" width="200px" height="200px"></canvas>
</body>
<script>
// 测试FFT代码
var canvas = document.getElementById('spectrum');
var ctx = canvas.getContext('2d');
function drawSpectrum(data) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
var width = canvas.width / data.length;
for (var i = 0; i < data.length; i++) {
var x = i * width;
var height = 8 * data[i] / 256 * canvas.height;
ctx.moveTo(x, canvas.height);
ctx.lineTo(x, canvas.height - height);
ctx.arc(x, canvas.height - height, 2, 0, 2 * Math.PI);
}
ctx.strokeStyle = 'rgb(0,255,0)';
ctx.stroke();
}
function testTri() {
var f = new realFFT(8);
var test = new Float32Array([0,1,2,3,4,3,2,1]);
let data = f.fft(test, 0);
let A = realFFT.ComplexAbs(data[0], data[1]);
drawSpectrum(A);
}
testTri();
</script>
Loading

0 comments on commit 23e3c0c

Please sign in to comment.