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