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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midi-audio-player",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Javascript Midi Audio Player for WebAudioFont",
5
5
  "keywords": [
6
6
  "audio",
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}`)();
@@ -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 WebAudioFontChannel {
1
+ class WebAudioFontPlayer {
2
2
 
3
- constructor(audioContext) {
4
- this.audioContext = audioContext;
5
- this.input = audioContext.createGain();
3
+ #audioCtx = null;
4
+ #preset = null;
6
5
 
7
- let lastNode = this.input;
8
- [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384].forEach(freq => {
9
- lastNode = this.bandEqualizer(lastNode, freq);
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
- this.limiter = audioContext.createDynamicsCompressor();
15
- this.limiter.threshold.setValueAtTime(-3.0, audioContext.currentTime);
16
- this.limiter.ratio.setValueAtTime(40, audioContext.currentTime);
17
- this.limiter.attack.setValueAtTime(0.000, audioContext.currentTime);
18
- this.limiter.release.setValueAtTime(0.25, audioContext.currentTime);
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
- bandEqualizer(from, frequency) {
27
- const filter = this.audioContext.createBiquadFilter();
28
- filter.frequency.setTargetAtTime(frequency, 0, 0.0001);
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
- class WebAudioFontPlayer {
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
- envelopes = [];
40
- afterTime = 0.05;
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
- #audioCtx = null;
44
- #preset = null;
33
+ const envelope = this.#findEnvelope();
34
+ this.#setupEnvelope(envelope, zone, vol, startWhen, waveDuration, duration);
45
35
 
46
- constructor(audioCtx, preset) {
47
- this.#audioCtx = audioCtx;
48
- this.#preset = preset;
49
- this.adjustPreset(this.#audioCtx, this.#preset);
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
- adjustPreset(audioContext, preset) {
54
- return preset.zones.map(zone => this.adjustZone(audioContext, zone));
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(audioContext, zone) {
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 = audioContext.createBuffer(1, decoded.length / 2, zone.sampleRate);
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.applyZoneParameters(zone);
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
- audioContext.decodeAudioData(
107
+ this.#audioCtx.decodeAudioData(
85
108
  uint8Array.buffer,
86
109
  audioBuffer => {
87
110
  zone.buffer = audioBuffer;
88
- this.applyZoneParameters(zone);
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.applyZoneParameters(zone);
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
- if (!zone?.buffer) return null;
117
-
118
- const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
119
- const playbackRate = Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
120
- const startWhen = Math.max(when, this.#audioCtx.currentTime);
121
- let waveDuration = duration + this.afterTime;
122
-
123
- const loop = zone.loopStart >= 1 && zone.loopStart < zone.loopEnd;
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
- return envelope;
159
- }
136
+ #setupEnvelope(envelope, zone, volume, when, sampleDuration, noteDuration) {
137
+ envelope.gain.setValueAtTime(this.#noZeroVolume(0), this.#audioCtx.currentTime);
160
138
 
161
- setupEnvelope(audioContext, envelope, zone, volume, when, sampleDuration, noteDuration) {
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.noZeroVolume(initialVol), when);
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.noZeroVolume(volume * n), when + duration);
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.noZeroVolume(volume * lastVolume), when + lastTime);
163
+ envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(volume * lastVolume), when + lastTime);
189
164
  }
190
165
  }
191
- envelope.gain.linearRampToValueAtTime(this.noZeroVolume(0), when + duration + this.afterTime);
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 = audioContext.createGain();
212
-
181
+ envelope = this.#audioCtx.createGain();
213
182
  envelope.gain.value = 0;
214
-
215
- envelope.target = target;
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 > audioContext.currentTime) {
186
+ if (envelope.when + envelope.duration > this.#audioCtx.currentTime) {
220
187
  envelope.gain.cancelScheduledValues(0);
221
- envelope.gain.setTargetAtTime(this.nearZero, audioContext.currentTime, 0.1);
222
- envelope.when = audioContext.currentTime + 0.00001;
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.envelopes.push(envelope);
193
+ this.#envelopes.push(envelope);
227
194
  }
228
195
  return envelope;
229
196
  }
230
197
 
231
- findZone = (audioContext, preset, pitch) => {
232
- const zone = preset.zones.findLast(z => pitch >= z.keyRangeLow && pitch <= z.keyRangeHigh + 1);
233
- if (zone) this.adjustZone(audioContext, zone);
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
- limitVolume = (v) => {
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
- resumeContext(audioContext) {
245
- if (audioContext.state === 'suspended') audioContext.resume().catch(() => { });
211
+
212
+ #noZeroVolume(n) {
213
+ return n > this.#nearZero ? n : this.#nearZero;
246
214
  }
247
215
 
248
- queueChord(ctx, tgt, prst, w, pchs, d, v, s) {
249
- const vol = this.limitVolume(v);
250
- return pchs.map((p, i) => this.queueWaveTable(ctx, tgt, prst, w, p, d, vol - Math.random() * 0.01, s?.[i])).filter(Boolean);
216
+
217
+ #numValue(a, b) {
218
+ return typeof a === "number" ? a : b;
251
219
  }
252
220
 
253
- cancelQueue(audioContext) {
254
- this.envelopes.forEach(e => {
255
- e.gain.cancelScheduledValues(0);
256
- e.gain.setValueAtTime(this.nearZero, audioContext.currentTime);
257
- e.when = -1;
258
- try { e.audioBufferSourceNode?.disconnect(); } catch (e) { }
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;