midi-audio-player 1.0.1 → 1.0.3
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/package.json +1 -1
- package/src/downloader.js +3 -15
- package/src/midiaudioplayer.js +23 -26
- package/src/webaudiofontplayer.js +149 -147
package/package.json
CHANGED
package/src/downloader.js
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Télécharge une police audio WebAudioFont et la convertit en JSON
|
|
6
|
-
* @param {string} id - L'ID de la police (ex: "0810_GeneralUserGS_sf2_file")
|
|
7
|
-
* @param {string} filename - Le nom du fichier de sortie (ex: "ma_police.json")
|
|
8
|
-
*/
|
|
4
|
+
|
|
9
5
|
export async function downloadWebAudioFont(id, filename) {
|
|
10
6
|
const cleanFilename = filename.endsWith('.json') ? filename : `${filename}.json`;
|
|
11
|
-
|
|
12
7
|
const destPath = path.join(process.cwd(), cleanFilename);
|
|
13
8
|
const url = `https://surikov.github.io/webaudiofontdata/sound/${id}.js`;
|
|
14
9
|
|
|
15
10
|
try {
|
|
16
11
|
console.log(`📡 Téléchargement de : ${id}...`);
|
|
17
|
-
|
|
18
12
|
const response = await fetch(url);
|
|
19
|
-
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
throw new Error(`Erreur HTTP: ${response.status} (Vérifiez l'ID)`);
|
|
22
|
-
}
|
|
13
|
+
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status} (Vérifiez l'ID)`);
|
|
23
14
|
|
|
24
15
|
const rawContent = await response.text();
|
|
25
|
-
|
|
26
16
|
const firstBrace = rawContent.indexOf('{');
|
|
27
17
|
const lastBrace = rawContent.lastIndexOf('}');
|
|
28
18
|
|
|
29
|
-
if (firstBrace === -1 || lastBrace === -1)
|
|
30
|
-
throw new Error("Format de fichier invalide : structure d'objet introuvable.");
|
|
31
|
-
}
|
|
19
|
+
if (firstBrace === -1 || lastBrace === -1) throw new Error("Format de fichier invalide : structure d'objet introuvable.");
|
|
32
20
|
|
|
33
21
|
const objectString = rawContent.substring(firstBrace, lastBrace + 1);
|
|
34
22
|
const data = new Function(`return ${objectString}`)();
|
package/src/midiaudioplayer.js
CHANGED
|
@@ -31,6 +31,28 @@ export default class MidiAudioPlayer {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
async play(content = null) {
|
|
35
|
+
if(content) await this.#load(content);
|
|
36
|
+
await this.#audioCtx.resume();
|
|
37
|
+
await this.#midiPlayer.play();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async pause() {
|
|
42
|
+
await this.#midiPlayer.pause();
|
|
43
|
+
await this.#audioCtx.suspend();
|
|
44
|
+
await this.#clearActiveNotes();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async stop() {
|
|
49
|
+
await this.#midiPlayer.stop();
|
|
50
|
+
await this.#audioCtx.suspend();
|
|
51
|
+
await this.#audioPlayer.cancelQueue();
|
|
52
|
+
await this.#clearActiveNotes();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
34
56
|
async #endOfFile() {
|
|
35
57
|
if(typeof this.#opts.onEndFile == 'function') await this.#opts.onEndFile();
|
|
36
58
|
}
|
|
@@ -73,11 +95,7 @@ export default class MidiAudioPlayer {
|
|
|
73
95
|
|
|
74
96
|
#clearActiveNotes() {
|
|
75
97
|
if (this.#activeNotes) {
|
|
76
|
-
this.#activeNotes.forEach((envelope, note) => {
|
|
77
|
-
if (envelope && envelope.cancel) {
|
|
78
|
-
envelope.cancel();
|
|
79
|
-
}
|
|
80
|
-
});
|
|
98
|
+
this.#activeNotes.forEach((envelope, note) => { if (envelope && envelope.cancel) envelope.cancel(); });
|
|
81
99
|
this.#activeNotes.clear();
|
|
82
100
|
}
|
|
83
101
|
}
|
|
@@ -89,25 +107,4 @@ export default class MidiAudioPlayer {
|
|
|
89
107
|
await this.#midiPlayer.loadArrayBuffer(content);
|
|
90
108
|
}
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
async play(content = null) {
|
|
94
|
-
if(content) await this.#load(content);
|
|
95
|
-
await this.#audioCtx.resume();
|
|
96
|
-
await this.#midiPlayer.play();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
async pause() {
|
|
101
|
-
await this.#midiPlayer.pause();
|
|
102
|
-
await this.#audioCtx.suspend();
|
|
103
|
-
await this.#clearActiveNotes();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
async stop() {
|
|
108
|
-
await this.#midiPlayer.stop();
|
|
109
|
-
await this.#audioCtx.suspend();
|
|
110
|
-
await this.#clearActiveNotes();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
110
|
}
|
|
@@ -1,67 +1,92 @@
|
|
|
1
|
-
class
|
|
1
|
+
class WebAudioFontPlayer {
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
this.input = audioContext.createGain();
|
|
3
|
+
#audioCtx = null;
|
|
4
|
+
#preset = null;
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this[`band${freq < 1000 ? freq : (freq / 1024) + 'k'}`] = lastNode;
|
|
11
|
-
});
|
|
6
|
+
#envelopes = [];
|
|
7
|
+
#afterTime = 0.05;
|
|
8
|
+
#nearZero = 0.000001;
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
this
|
|
16
|
-
this
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
this.output = audioContext.createGain();
|
|
11
|
+
constructor(audioCtx, preset) {
|
|
12
|
+
this.#audioCtx = audioCtx;
|
|
13
|
+
this.#preset = preset;
|
|
14
|
+
this.#preset.zones.map(zone => this.#adjustZone(zone));
|
|
15
|
+
}
|
|
20
16
|
|
|
21
|
-
lastNode.connect(this.limiter);
|
|
22
|
-
this.limiter.connect(this.output);
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
queueWaveTable(when, pitch, duration, volume, slides) {
|
|
19
|
+
if(this.#audioCtx.state === 'suspended') this.#audioCtx.resume().catch(() => { });
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
filter.type = "peaking";
|
|
30
|
-
filter.gain.setTargetAtTime(0, 0, 0.0001);
|
|
31
|
-
filter.Q.setTargetAtTime(1.0, 0, 0.0001);
|
|
32
|
-
from.connect(filter);
|
|
33
|
-
return filter;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
21
|
+
const vol = this.#limitVolume(volume);
|
|
22
|
+
const zone = this.#findZone(pitch);
|
|
23
|
+
if (!zone?.buffer) return null;
|
|
36
24
|
|
|
37
|
-
|
|
25
|
+
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
|
26
|
+
const playbackRate = Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
|
27
|
+
const startWhen = Math.max(when, this.#audioCtx.currentTime);
|
|
28
|
+
let waveDuration = duration + this.#afterTime;
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
nearZero = 0.000001;
|
|
30
|
+
const loop = zone.loopStart >= 1 && zone.loopStart < zone.loopEnd;
|
|
31
|
+
if (!loop) waveDuration = Math.min(waveDuration, zone.buffer.duration / playbackRate);
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
const envelope = this.#findEnvelope();
|
|
34
|
+
this.#setupEnvelope(envelope, zone, vol, startWhen, waveDuration, duration);
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
const source = this.#audioCtx.createBufferSource();
|
|
37
|
+
source.buffer = zone.buffer;
|
|
38
|
+
source.playbackRate.setValueAtTime(playbackRate, 0);
|
|
39
|
+
|
|
40
|
+
if (slides?.length > 0) {
|
|
41
|
+
source.playbackRate.setValueAtTime(playbackRate, startWhen);
|
|
42
|
+
slides.forEach(s => {
|
|
43
|
+
const newRate = Math.pow(2, (100.0 * (pitch + s.delta) - baseDetune) / 1200.0);
|
|
44
|
+
source.playbackRate.linearRampToValueAtTime(newRate, startWhen + s.when);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
source.loop = loop;
|
|
49
|
+
if (loop) {
|
|
50
|
+
const d = zone.delay ?? 0;
|
|
51
|
+
source.loopStart = zone.loopStart / zone.sampleRate + d;
|
|
52
|
+
source.loopEnd = zone.loopEnd / zone.sampleRate + d;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
source.connect(envelope);
|
|
56
|
+
source.start(startWhen, zone.delay ?? 0);
|
|
57
|
+
source.stop(startWhen + waveDuration);
|
|
58
|
+
|
|
59
|
+
envelope.audioBufferSourceNode = source;
|
|
60
|
+
envelope.when = startWhen;
|
|
61
|
+
envelope.duration = waveDuration;
|
|
62
|
+
|
|
63
|
+
return envelope;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
queueChord(prst, w, pchs, d, v, s) {
|
|
68
|
+
const vol = this.#limitVolume(v);
|
|
69
|
+
return pchs.map((p, i) => this.queueWaveTable(this.#audioCtx, this.#audioCtx.destination, prst, w, p, d, vol - Math.random() * 0.01, s?.[i])).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async cancelQueue() {
|
|
74
|
+
this.#envelopes.forEach(e => {
|
|
75
|
+
e.gain.cancelScheduledValues(0);
|
|
76
|
+
e.gain.setValueAtTime(this.#nearZero, this.#audioCtx.currentTime);
|
|
77
|
+
e.when = -1;
|
|
78
|
+
try { e.audioBufferSourceNode?.disconnect(); } catch (e) { }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
56
81
|
|
|
57
82
|
|
|
58
|
-
adjustZone(
|
|
83
|
+
#adjustZone(zone) {
|
|
59
84
|
if (zone.buffer) return Promise.resolve(zone);
|
|
60
85
|
zone.delay = 0;
|
|
61
86
|
|
|
62
87
|
if (zone.sample) {
|
|
63
88
|
const decoded = atob(zone.sample);
|
|
64
|
-
zone.buffer =
|
|
89
|
+
zone.buffer = this.#audioCtx.createBuffer(1, decoded.length / 2, zone.sampleRate);
|
|
65
90
|
const float32Array = zone.buffer.getChannelData(0);
|
|
66
91
|
|
|
67
92
|
for (let i = 0; i < decoded.length / 2; i++) {
|
|
@@ -71,21 +96,19 @@ class WebAudioFontPlayer {
|
|
|
71
96
|
if (n >= 32768) n -= 65536;
|
|
72
97
|
float32Array[i] = n / 32768.0;
|
|
73
98
|
}
|
|
74
|
-
this
|
|
99
|
+
this.#applyZoneParameters(zone);
|
|
75
100
|
return zone;
|
|
76
101
|
|
|
77
102
|
} else if (zone.file) {
|
|
78
103
|
const decoded = atob(zone.file);
|
|
79
104
|
const uint8Array = new Uint8Array(decoded.length);
|
|
80
|
-
for (let i = 0; i < decoded.length; i++)
|
|
81
|
-
uint8Array[i] = decoded.charCodeAt(i);
|
|
82
|
-
}
|
|
105
|
+
for (let i = 0; i < decoded.length; i++) uint8Array[i] = decoded.charCodeAt(i);
|
|
83
106
|
|
|
84
|
-
|
|
107
|
+
this.#audioCtx.decodeAudioData(
|
|
85
108
|
uint8Array.buffer,
|
|
86
109
|
audioBuffer => {
|
|
87
110
|
zone.buffer = audioBuffer;
|
|
88
|
-
this
|
|
111
|
+
this.#applyZoneParameters(zone);
|
|
89
112
|
return zone;
|
|
90
113
|
},
|
|
91
114
|
error => {
|
|
@@ -94,74 +117,26 @@ class WebAudioFontPlayer {
|
|
|
94
117
|
}
|
|
95
118
|
);
|
|
96
119
|
} else {
|
|
97
|
-
this
|
|
120
|
+
this.#applyZoneParameters(zone);
|
|
98
121
|
return zone;
|
|
99
122
|
}
|
|
100
123
|
};
|
|
101
124
|
|
|
102
|
-
applyZoneParameters = (zone) => {
|
|
103
|
-
zone.loopStart = this.numValue(zone.loopStart, 0);
|
|
104
|
-
zone.loopEnd = this.numValue(zone.loopEnd, 0);
|
|
105
|
-
zone.coarseTune = this.numValue(zone.coarseTune, 0);
|
|
106
|
-
zone.fineTune = this.numValue(zone.fineTune, 0);
|
|
107
|
-
zone.originalPitch = this.numValue(zone.originalPitch, 6000);
|
|
108
|
-
zone.sampleRate = this.numValue(zone.sampleRate, 44100);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
queueWaveTable(when, pitch, duration, volume, slides) {
|
|
112
|
-
this.resumeContext(this.#audioCtx);
|
|
113
|
-
const vol = this.limitVolume(volume);
|
|
114
|
-
const zone = this.findZone(this.#audioCtx, this.#preset, pitch);
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!loop) {
|
|
125
|
-
waveDuration = Math.min(waveDuration, zone.buffer.duration / playbackRate);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const envelope = this.findEnvelope(this.#audioCtx, this.#audioCtx.destination);
|
|
129
|
-
this.setupEnvelope(this.#audioCtx, envelope, zone, vol, startWhen, waveDuration, duration);
|
|
130
|
-
|
|
131
|
-
const source = this.#audioCtx.createBufferSource();
|
|
132
|
-
source.buffer = zone.buffer;
|
|
133
|
-
source.playbackRate.setValueAtTime(playbackRate, 0);
|
|
134
|
-
|
|
135
|
-
if (slides?.length > 0) {
|
|
136
|
-
source.playbackRate.setValueAtTime(playbackRate, startWhen);
|
|
137
|
-
slides.forEach(s => {
|
|
138
|
-
const newRate = Math.pow(2, (100.0 * (pitch + s.delta) - baseDetune) / 1200.0);
|
|
139
|
-
source.playbackRate.linearRampToValueAtTime(newRate, startWhen + s.when);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
source.loop = loop;
|
|
144
|
-
if (loop) {
|
|
145
|
-
const d = zone.delay ?? 0;
|
|
146
|
-
source.loopStart = zone.loopStart / zone.sampleRate + d;
|
|
147
|
-
source.loopEnd = zone.loopEnd / zone.sampleRate + d;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
source.connect(envelope);
|
|
151
|
-
source.start(startWhen, zone.delay ?? 0);
|
|
152
|
-
source.stop(startWhen + waveDuration);
|
|
126
|
+
#applyZoneParameters(zone) {
|
|
127
|
+
zone.loopStart = this.#numValue(zone.loopStart, 0);
|
|
128
|
+
zone.loopEnd = this.#numValue(zone.loopEnd, 0);
|
|
129
|
+
zone.coarseTune = this.#numValue(zone.coarseTune, 0);
|
|
130
|
+
zone.fineTune = this.#numValue(zone.fineTune, 0);
|
|
131
|
+
zone.originalPitch = this.#numValue(zone.originalPitch, 6000);
|
|
132
|
+
zone.sampleRate = this.#numValue(zone.sampleRate, 44100);
|
|
133
|
+
};
|
|
153
134
|
|
|
154
|
-
envelope.audioBufferSourceNode = source;
|
|
155
|
-
envelope.when = startWhen;
|
|
156
|
-
envelope.duration = waveDuration;
|
|
157
135
|
|
|
158
|
-
|
|
159
|
-
|
|
136
|
+
#setupEnvelope(envelope, zone, volume, when, sampleDuration, noteDuration) {
|
|
137
|
+
envelope.gain.setValueAtTime(this.#noZeroVolume(0), this.#audioCtx.currentTime);
|
|
160
138
|
|
|
161
|
-
|
|
162
|
-
envelope.gain.setValueAtTime(this.noZeroVolume(0), audioContext.currentTime);
|
|
163
|
-
|
|
164
|
-
const duration = Math.min(noteDuration, sampleDuration - this.afterTime);
|
|
139
|
+
const duration = Math.min(noteDuration, sampleDuration - this.#afterTime);
|
|
165
140
|
const ahdsr = (zone.ahdsr && zone.ahdsr.length > 0) ? zone.ahdsr : [
|
|
166
141
|
{ duration: 0, volume: 1 },
|
|
167
142
|
{ duration: duration, volume: 1 }
|
|
@@ -169,7 +144,7 @@ class WebAudioFontPlayer {
|
|
|
169
144
|
|
|
170
145
|
envelope.gain.cancelScheduledValues(when);
|
|
171
146
|
const initialVol = (ahdsr[0]?.volume ?? 1) * volume;
|
|
172
|
-
envelope.gain.setValueAtTime(this
|
|
147
|
+
envelope.gain.setValueAtTime(this.#noZeroVolume(initialVol), when);
|
|
173
148
|
|
|
174
149
|
let lastTime = 0;
|
|
175
150
|
let lastVolume = ahdsr[0]?.volume ?? 1;
|
|
@@ -180,25 +155,20 @@ class WebAudioFontPlayer {
|
|
|
180
155
|
if (stage.duration + lastTime > duration) {
|
|
181
156
|
const r = 1 - (stage.duration + lastTime - duration) / stage.duration;
|
|
182
157
|
const n = lastVolume - r * (lastVolume - stage.volume);
|
|
183
|
-
envelope.gain.linearRampToValueAtTime(this
|
|
158
|
+
envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(volume * n), when + duration);
|
|
184
159
|
break;
|
|
185
160
|
}
|
|
186
161
|
lastTime += stage.duration;
|
|
187
162
|
lastVolume = stage.volume;
|
|
188
|
-
envelope.gain.linearRampToValueAtTime(this
|
|
163
|
+
envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(volume * lastVolume), when + lastTime);
|
|
189
164
|
}
|
|
190
165
|
}
|
|
191
|
-
envelope.gain.linearRampToValueAtTime(this
|
|
166
|
+
envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(0), when + duration + this.#afterTime);
|
|
192
167
|
}
|
|
193
168
|
|
|
194
169
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
findEnvelope(audioContext, target) {
|
|
198
|
-
let envelope = this.envelopes.find(e =>
|
|
199
|
-
e.target === target && audioContext.currentTime > e.when + e.duration + 0.001
|
|
200
|
-
);
|
|
201
|
-
|
|
170
|
+
#findEnvelope() {
|
|
171
|
+
let envelope = this.#envelopes.find(e => e.target === this.#audioCtx.destination && this.#audioCtx.currentTime > e.when + e.duration + 0.001);
|
|
202
172
|
if (envelope) {
|
|
203
173
|
if (envelope.audioBufferSourceNode) {
|
|
204
174
|
try {
|
|
@@ -208,56 +178,88 @@ class WebAudioFontPlayer {
|
|
|
208
178
|
envelope.audioBufferSourceNode = null;
|
|
209
179
|
}
|
|
210
180
|
} else {
|
|
211
|
-
envelope =
|
|
212
|
-
|
|
181
|
+
envelope = this.#audioCtx.createGain();
|
|
213
182
|
envelope.gain.value = 0;
|
|
214
|
-
|
|
215
|
-
envelope.
|
|
216
|
-
envelope.connect(target);
|
|
217
|
-
|
|
183
|
+
envelope.target = this.#audioCtx.destination;
|
|
184
|
+
envelope.connect(this.#audioCtx.destination);
|
|
218
185
|
envelope.cancel = () => {
|
|
219
|
-
if (envelope.when + envelope.duration >
|
|
186
|
+
if (envelope.when + envelope.duration > this.#audioCtx.currentTime) {
|
|
220
187
|
envelope.gain.cancelScheduledValues(0);
|
|
221
|
-
envelope.gain.setTargetAtTime(this
|
|
222
|
-
envelope.when =
|
|
188
|
+
envelope.gain.setTargetAtTime(this.#nearZero, this.#audioCtx.currentTime, 0.1);
|
|
189
|
+
envelope.when = this.#audioCtx.currentTime + 0.00001;
|
|
223
190
|
envelope.duration = 0;
|
|
224
191
|
}
|
|
225
192
|
};
|
|
226
|
-
this
|
|
193
|
+
this.#envelopes.push(envelope);
|
|
227
194
|
}
|
|
228
195
|
return envelope;
|
|
229
196
|
}
|
|
230
197
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
198
|
+
|
|
199
|
+
#findZone(pitch) {
|
|
200
|
+
const zone = this.#preset.zones.findLast(z => pitch >= z.keyRangeLow && pitch <= z.keyRangeHigh + 1);
|
|
201
|
+
if (zone) this.#adjustZone(this.#audioCtx, zone);
|
|
234
202
|
return zone;
|
|
235
203
|
};
|
|
236
204
|
|
|
237
|
-
|
|
205
|
+
|
|
206
|
+
#limitVolume(v) {
|
|
238
207
|
const requestedVolume = v ? 1.0 * v : 0.5;
|
|
239
208
|
return Math.min(requestedVolume, 0.8);
|
|
240
209
|
};
|
|
241
|
-
noZeroVolume = (n) => n > this.nearZero ? n : this.nearZero;
|
|
242
|
-
numValue = (a, b) => typeof a === "number" ? a : b;
|
|
243
210
|
|
|
244
|
-
|
|
245
|
-
|
|
211
|
+
|
|
212
|
+
#noZeroVolume(n) {
|
|
213
|
+
return n > this.#nearZero ? n : this.#nearZero;
|
|
246
214
|
}
|
|
247
215
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return
|
|
216
|
+
|
|
217
|
+
#numValue(a, b) {
|
|
218
|
+
return typeof a === "number" ? a : b;
|
|
251
219
|
}
|
|
252
220
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class WebAudioFontChannel {
|
|
225
|
+
|
|
226
|
+
#input = null;
|
|
227
|
+
#output = null;
|
|
228
|
+
#audioCtx = null;
|
|
229
|
+
#limiter = null;
|
|
230
|
+
|
|
231
|
+
constructor(audioCtx) {
|
|
232
|
+
this.#audioCtx = audioCtx;
|
|
233
|
+
this.#input = this.#audioCtx.createGain();
|
|
234
|
+
|
|
235
|
+
let lastNode = this.#input;
|
|
236
|
+
[32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384].forEach(freq => {
|
|
237
|
+
lastNode = this.#bandEqualizer(lastNode, freq);
|
|
238
|
+
this[`band${freq < 1000 ? freq : (freq / 1024) + 'k'}`] = lastNode;
|
|
259
239
|
});
|
|
240
|
+
|
|
241
|
+
this.#limiter = this.#audioCtx.createDynamicsCompressor();
|
|
242
|
+
this.#limiter.threshold.setValueAtTime(-3.0, this.#audioCtx.currentTime);
|
|
243
|
+
this.#limiter.ratio.setValueAtTime(40, this.#audioCtx.currentTime);
|
|
244
|
+
this.#limiter.attack.setValueAtTime(0.000, this.#audioCtx.currentTime);
|
|
245
|
+
this.#limiter.release.setValueAtTime(0.25, this.#audioCtx.currentTime);
|
|
246
|
+
this.#output = this.#audioCtx.createGain();
|
|
247
|
+
lastNode.connect(this.#limiter);
|
|
248
|
+
this.#limiter.connect(this.#output);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
#bandEqualizer(from, frequency) {
|
|
253
|
+
const filter = this.#audioCtx.createBiquadFilter();
|
|
254
|
+
filter.frequency.setTargetAtTime(frequency, 0, 0.0001);
|
|
255
|
+
filter.type = "peaking";
|
|
256
|
+
filter.gain.setTargetAtTime(0, 0, 0.0001);
|
|
257
|
+
filter.Q.setTargetAtTime(1.0, 0, 0.0001);
|
|
258
|
+
from.connect(filter);
|
|
259
|
+
return filter;
|
|
260
260
|
}
|
|
261
|
+
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
|
|
263
265
|
export default WebAudioFontPlayer;
|