midi-audio-player 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +472 -93
- package/dist/index.js +1715 -212
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +13 -13
- package/dist/index.mjs.map +4 -4
- package/dist/midi-audio-player.js +1714 -212
- package/dist/midi-audio-player.min.js +13 -14
- package/index.d.ts +905 -0
- package/package.json +13 -17
- package/src/libraries/audiocompressor.js +186 -0
- package/src/libraries/indexeddbstorage.js +107 -0
- package/src/midiaudioplayer.js +1379 -52
- package/bin/cli.js +0 -27
- package/dist/midi-audio-player.js.map +0 -7
- package/dist/midi-audio-player.min.js.map +0 -7
- package/src/downloader.js +0 -33
- package/src/presets/defaultpreset.json +0 -116
- package/src/webaudiofontplayer.js +0 -264
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "midi-audio-player",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Real MIDI playback in the browser — powered by Web Audio API and WebAudioFont.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"midi",
|
|
7
7
|
"midi-player",
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"web-audio-api",
|
|
15
15
|
"midi-playback"
|
|
16
16
|
],
|
|
17
|
-
"homepage": "https://
|
|
17
|
+
"homepage": "https://webaudiofonts.com/",
|
|
18
18
|
"bugs": {
|
|
19
|
-
"url": "https://github.com/
|
|
19
|
+
"url": "https://github.com/WebAudioFonts/midi-audio-player/issues"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/
|
|
23
|
+
"url": "git+https://github.com/WebAudioFonts/midi-audio-player.git"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"author": "Maxime Larrivée-Roy",
|
|
@@ -31,35 +31,31 @@
|
|
|
31
31
|
"jsdelivr": "./dist/midi-audio-player.min.js",
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
|
-
"bin",
|
|
35
34
|
"src",
|
|
36
35
|
"index.js",
|
|
36
|
+
"index.d.ts",
|
|
37
37
|
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
40
41
|
"exports": {
|
|
41
42
|
".": {
|
|
43
|
+
"types": "./index.d.ts",
|
|
42
44
|
"import": "./dist/index.js",
|
|
43
45
|
"script": "./dist/midi-audio-player.min.js"
|
|
44
46
|
}
|
|
45
47
|
},
|
|
48
|
+
"types": "index.d.ts",
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "node scripts/watch.js --build",
|
|
48
|
-
"dev": "node scripts/watch.js"
|
|
49
|
-
"serve:site": "browser-sync start --server --startPath /site/ --files \"site/**/*.html, site/**/*.css, dist/index.js\"",
|
|
50
|
-
"sass:watch": "sass site/:site/ --no-source-map --watch",
|
|
51
|
-
"sass:build": "sass site/:site/ --no-source-map"
|
|
52
|
-
},
|
|
53
|
-
"bin": {
|
|
54
|
-
"webaudiofont": "./bin/cli.js"
|
|
51
|
+
"dev": "node scripts/watch.js"
|
|
55
52
|
},
|
|
56
53
|
"dependencies": {
|
|
57
|
-
"midi-player-js": "^2.0.17"
|
|
54
|
+
"midi-player-js": "^2.0.17",
|
|
55
|
+
"webaudiofontplayer": "^1.0.0"
|
|
58
56
|
},
|
|
59
57
|
"devDependencies": {
|
|
60
|
-
"
|
|
61
|
-
"esbuild": "^0.28.0",
|
|
62
|
-
"sass": "^1.99.0"
|
|
58
|
+
"esbuild": "^0.28.0"
|
|
63
59
|
},
|
|
64
60
|
"engines": {
|
|
65
61
|
"node": ">=18"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export default class AudioCompressor {
|
|
2
|
+
#input = null;
|
|
3
|
+
#output = null;
|
|
4
|
+
#audioCtx = null;
|
|
5
|
+
#limiter = null;
|
|
6
|
+
#analyser = null;
|
|
7
|
+
#reverbNode = null;
|
|
8
|
+
#reverbWet = null;
|
|
9
|
+
#currentReverbLevel = 0;
|
|
10
|
+
#eqBands = new Map();
|
|
11
|
+
|
|
12
|
+
static #EQ_FREQUENCIES = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
|
|
13
|
+
static #EQ_Q = new Map([
|
|
14
|
+
[32, 0.7],
|
|
15
|
+
[64, 0.8],
|
|
16
|
+
[128, 0.9],
|
|
17
|
+
[256, 1.0],
|
|
18
|
+
[512, 1.1],
|
|
19
|
+
[1024, 1.2],
|
|
20
|
+
[2048, 1.4],
|
|
21
|
+
[4096, 1.6],
|
|
22
|
+
[8192, 1.8],
|
|
23
|
+
[16384, 2.0],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
constructor(audioCtx, volume, reverb) {
|
|
28
|
+
this.#audioCtx = audioCtx;
|
|
29
|
+
this.#input = this.#audioCtx.createGain();
|
|
30
|
+
let lastNode = this.#input;
|
|
31
|
+
const frequencies = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
|
|
32
|
+
frequencies.forEach(freq => {
|
|
33
|
+
lastNode = this.#bandEqualizer(lastNode, freq);
|
|
34
|
+
const label = freq < 1000 ? freq : (freq / 1024) + 'k';
|
|
35
|
+
this[`band${label}`] = lastNode;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.#currentReverbLevel = reverb;
|
|
39
|
+
this.#reverbNode = this.#audioCtx.createConvolver();
|
|
40
|
+
this.#reverbWet = this.#audioCtx.createGain();
|
|
41
|
+
this.#reverbWet.gain.setValueAtTime(reverb, this.#audioCtx.currentTime);
|
|
42
|
+
this.#generateImpulseResponse(1.5, 2.0);
|
|
43
|
+
|
|
44
|
+
this.#limiter = this.#audioCtx.createDynamicsCompressor();
|
|
45
|
+
this.#limiter.threshold.setValueAtTime(-10.0, this.#audioCtx.currentTime);
|
|
46
|
+
this.#limiter.ratio.setValueAtTime(20, this.#audioCtx.currentTime);
|
|
47
|
+
this.#limiter.attack.setValueAtTime(0.001, this.#audioCtx.currentTime);
|
|
48
|
+
this.#limiter.release.setValueAtTime(0.1, this.#audioCtx.currentTime);
|
|
49
|
+
this.#limiter.knee.setValueAtTime(0, this.#audioCtx.currentTime);
|
|
50
|
+
|
|
51
|
+
this.#analyser = this.#audioCtx.createAnalyser();
|
|
52
|
+
this.#analyser.fftSize = 256;
|
|
53
|
+
this.#analyser.smoothingTimeConstant = 0.6;
|
|
54
|
+
|
|
55
|
+
this.#output = this.#audioCtx.createGain();
|
|
56
|
+
this.#output.gain.setValueAtTime(volume, this.#audioCtx.currentTime);
|
|
57
|
+
|
|
58
|
+
lastNode.connect(this.#output);
|
|
59
|
+
this.#output.connect(this.#limiter);
|
|
60
|
+
this.#output.connect(this.#reverbNode);
|
|
61
|
+
this.#reverbNode.connect(this.#reverbWet);
|
|
62
|
+
this.#limiter.connect(this.#analyser);
|
|
63
|
+
this.#reverbWet.connect(this.#analyser);
|
|
64
|
+
this.#analyser.connect(this.#audioCtx.destination);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get eqFrequencies() { return AudioCompressor.#EQ_FREQUENCIES }
|
|
68
|
+
get analyser() { return this.#analyser || null; }
|
|
69
|
+
get input() { return this.#input; }
|
|
70
|
+
get reverb() { return this.#currentReverbLevel; }
|
|
71
|
+
set reverb(value) {
|
|
72
|
+
this.#currentReverbLevel = Math.max(0, Math.min(1, value));
|
|
73
|
+
this.#reverbWet.gain.setTargetAtTime(this.#currentReverbLevel, this.#audioCtx.currentTime, 0.1);
|
|
74
|
+
}
|
|
75
|
+
get masterVolume() { return this.#output.gain.value; }
|
|
76
|
+
set masterVolume(value) {
|
|
77
|
+
const linearValue = Math.max(0, Math.min(1, value));
|
|
78
|
+
const logVolume = Math.pow(linearValue, 2);
|
|
79
|
+
this.#output.gain.setTargetAtTime(logVolume, this.#audioCtx.currentTime, 0.01);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
killReverbTail() {
|
|
84
|
+
const now = this.#audioCtx.currentTime;
|
|
85
|
+
this.#reverbWet.gain.cancelScheduledValues(now);
|
|
86
|
+
this.#reverbWet.gain.setValueAtTime(0, now);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
restoreReverb() {
|
|
91
|
+
this.reverb = this.#currentReverbLevel;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
setEQ(gains, smoothTime = 0.04) {
|
|
96
|
+
const now = this.#audioCtx.currentTime;
|
|
97
|
+
const MAX_DB = 12;
|
|
98
|
+
for (const [key, val] of Object.entries(gains)) {
|
|
99
|
+
const freq = Number(key);
|
|
100
|
+
const band = this.#eqBands.get(freq);
|
|
101
|
+
if (!band) continue;
|
|
102
|
+
const dbValue = Math.max(-MAX_DB, Math.min(MAX_DB, val));
|
|
103
|
+
band.filter.gain.setTargetAtTime(dbValue, now, smoothTime);
|
|
104
|
+
band.gain = dbValue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
getEQ() {
|
|
110
|
+
const result = {};
|
|
111
|
+
for (const [freq, band] of this.#eqBands) result[freq] = band.gain;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
resetEQ(smoothTime = 0.04) {
|
|
117
|
+
const flat = {};
|
|
118
|
+
for (const freq of AudioCompressor.#EQ_FREQUENCIES) flat[freq] = 0;
|
|
119
|
+
this.setEQ(flat, smoothTime);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
setEQPreset(name) {
|
|
124
|
+
const presets = {
|
|
125
|
+
flat: { 32:0, 64:0, 128:0, 256:0, 512:0, 1024:0, 2048:0, 4096:0, 8192:0, 16384:0 },
|
|
126
|
+
bass: { 32:7, 64:6, 128:4, 256:2, 512:0, 1024:-1, 2048:-1, 4096:0, 8192:0, 16384:0 },
|
|
127
|
+
treble: { 32:0, 64:0, 128:0, 256:0, 512:0, 1024:1, 2048:3, 4096:5, 8192:7, 16384:8 },
|
|
128
|
+
vocal: { 32:-3, 64:-2, 128:0, 256:2, 512:4, 1024:5, 2048:4, 4096:2, 8192:1, 16384:0 },
|
|
129
|
+
loudness: { 32:6, 64:4, 128:1, 256:0, 512:-1, 1024:-1, 2048:0, 4096:2, 8192:4, 16384:5 },
|
|
130
|
+
classical: { 32:4, 64:3, 128:2, 256:0, 512:0, 1024:0, 2048:0, 4096:2, 8192:3, 16384:4 },
|
|
131
|
+
jazz: { 32:4, 64:3, 128:1, 256:0, 512:-1, 1024:-1, 2048:0, 4096:1, 8192:3, 16384:4 },
|
|
132
|
+
electronic: { 32:6, 64:5, 128:2, 256:-1, 512:-2, 1024:-1, 2048:2, 4096:4, 8192:5, 16384:6 },
|
|
133
|
+
};
|
|
134
|
+
const preset = presets[name];
|
|
135
|
+
if (!preset) throw new Error(`Preset EQ unkown: "${name}". Avaiables: ${Object.keys(presets).join(', ')}`);
|
|
136
|
+
this.setEQ(preset);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
#bandEqualizer(from, frequency) {
|
|
141
|
+
const filter = this.#audioCtx.createBiquadFilter();
|
|
142
|
+
filter.type = "peaking";
|
|
143
|
+
filter.frequency.setValueAtTime(frequency, this.#audioCtx.currentTime);
|
|
144
|
+
filter.gain.setValueAtTime(0, this.#audioCtx.currentTime);
|
|
145
|
+
const q = AudioCompressor.#EQ_Q.get(frequency) ?? 1.0;
|
|
146
|
+
filter.Q.setValueAtTime(q, this.#audioCtx.currentTime);
|
|
147
|
+
this.#eqBands.set(frequency, { filter, gain: 0 });
|
|
148
|
+
from.connect(filter);
|
|
149
|
+
return filter;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#generateImpulseResponse(duration, decay) {
|
|
154
|
+
const sampleRate = this.#audioCtx.sampleRate;
|
|
155
|
+
const length = sampleRate * duration;
|
|
156
|
+
const impulse = this.#audioCtx.createBuffer(2, length, sampleRate);
|
|
157
|
+
const preDelayTime = 0.015;
|
|
158
|
+
const preDelaySamples = Math.floor(preDelayTime * sampleRate);
|
|
159
|
+
for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
|
|
160
|
+
const data = impulse.getChannelData(channel);
|
|
161
|
+
let lastValue = 0;
|
|
162
|
+
const channelOffset = channel === 1 ? Math.floor(0.002 * sampleRate) : 0;
|
|
163
|
+
for (let i = 0; i < length; i++) {
|
|
164
|
+
if (i < preDelaySamples) {
|
|
165
|
+
data[i] = 0;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const t = (i - preDelaySamples) / sampleRate;
|
|
169
|
+
const envelope = Math.exp(-t * (decay / duration));
|
|
170
|
+
const dampingFactor = Math.max(0.01, 0.2 * Math.exp(-t * 2.5));
|
|
171
|
+
const whiteNoise = (Math.random() * 2 - 1);
|
|
172
|
+
lastValue = (whiteNoise * dampingFactor) + (lastValue * (1 - dampingFactor));
|
|
173
|
+
let sampleValue = lastValue * envelope;
|
|
174
|
+
if (t < 0.04) {
|
|
175
|
+
if ((i % 123 === 0) || (i % 234 === 0)) {
|
|
176
|
+
sampleValue += (Math.random() * 2 - 1) * 0.2 * (0.04 - t) / 0.04;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (i + channelOffset < length) data[i + channelOffset] = sampleValue;
|
|
180
|
+
else data[i] = sampleValue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.#reverbNode.buffer = impulse;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const DB_NAME = "MidiAudioPlayer";
|
|
2
|
+
const STORE_NAME = "KeyValues";
|
|
3
|
+
const DEFAULT_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
let dbInstance = null;
|
|
6
|
+
let currentVersion = DEFAULT_VERSION;
|
|
7
|
+
|
|
8
|
+
async function getDB(version = currentVersion) {
|
|
9
|
+
if (dbInstance && version !== currentVersion) {
|
|
10
|
+
dbInstance.close();
|
|
11
|
+
dbInstance = null;
|
|
12
|
+
currentVersion = version;
|
|
13
|
+
}
|
|
14
|
+
if (dbInstance) return dbInstance;
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const request = indexedDB.open(DB_NAME, version);
|
|
17
|
+
request.onupgradeneeded = (e) => {
|
|
18
|
+
const db = e.target.result;
|
|
19
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
20
|
+
db.createObjectStore(STORE_NAME);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
request.onsuccess = (e) => {
|
|
24
|
+
dbInstance = e.target.result;
|
|
25
|
+
resolve(dbInstance);
|
|
26
|
+
};
|
|
27
|
+
request.onerror = (e) => reject(e.target.error);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const indexedDbStorage = {
|
|
32
|
+
async setVersion(version) {
|
|
33
|
+
await getDB(version);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async setItem(key, value, compress = false) {
|
|
37
|
+
const db = await getDB();
|
|
38
|
+
let finalData = value;
|
|
39
|
+
let isCompressed = false;
|
|
40
|
+
|
|
41
|
+
if (compress) {
|
|
42
|
+
const stringData = JSON.stringify(value);
|
|
43
|
+
const stream = new Blob([stringData]).stream();
|
|
44
|
+
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
|
|
45
|
+
const response = new Response(compressedStream);
|
|
46
|
+
finalData = await response.arrayBuffer();
|
|
47
|
+
isCompressed = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const record = { data: finalData, isCompressed };
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
54
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
55
|
+
const request = store.put(record, key);
|
|
56
|
+
request.onsuccess = () => resolve();
|
|
57
|
+
request.onerror = () => reject(request.error);
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async getItem(key) {
|
|
62
|
+
const db = await getDB();
|
|
63
|
+
const record = await new Promise((resolve, reject) => {
|
|
64
|
+
const transaction = db.transaction(STORE_NAME, "readonly");
|
|
65
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
66
|
+
const request = store.get(key);
|
|
67
|
+
request.onsuccess = () => resolve(request.result);
|
|
68
|
+
request.onerror = () => reject(request.error);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!record) return null;
|
|
72
|
+
|
|
73
|
+
if (record.isCompressed) {
|
|
74
|
+
const stream = new Blob([record.data]).stream();
|
|
75
|
+
const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip"));
|
|
76
|
+
const response = new Response(decompressedStream);
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return record.data;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async removeItem(key) {
|
|
85
|
+
const db = await getDB();
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
88
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
89
|
+
const request = store.delete(key);
|
|
90
|
+
request.onsuccess = () => resolve();
|
|
91
|
+
request.onerror = () => reject(request.error);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async clear() {
|
|
96
|
+
const db = await getDB();
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
99
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
100
|
+
const request = store.clear();
|
|
101
|
+
request.onsuccess = () => resolve();
|
|
102
|
+
request.onerror = () => reject(request.error);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default indexedDbStorage;
|