-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
finish data processing, preliminarily determine the program structure
- Loading branch information
1 parent
60be585
commit 23e3c0c
Showing
19 changed files
with
1,370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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是部分设计思路。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.