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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midi-audio-player",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Javascript Midi Audio Player for WebAudioFont",
5
5
  "keywords": [
6
6
  "audio",
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; // Retourne l'objet au cas où on veut l'utiliser immédiatement
27
+ return data;
49
28
 
50
29
  } catch (error) {
51
30
  console.error(`❌ Échec : ${error.message}`);
52
- throw error; // On relance l'erreur pour que l'appelant puisse la gérer
31
+ throw error;
53
32
  }
54
33
  }
@@ -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.adjustPreset(this.#audioCtx, this.#preset);
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.resumeContext(this.#audioCtx);
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.afterTime;
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.findEnvelope(this.#audioCtx, this.#audioCtx.destination);
131
- this.setupEnvelope(this.#audioCtx, envelope, zone, vol, startWhen, waveDuration, duration);
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
- const duration = Math.min(noteDuration, sampleDuration - this.afterTime);
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.noZeroVolume(initialVol), when);
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.noZeroVolume(volume * n), when + duration);
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.noZeroVolume(volume * lastVolume), when + lastTime);
163
+ envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(volume * lastVolume), when + lastTime);
191
164
  }
192
165
  }
193
- envelope.gain.linearRampToValueAtTime(this.noZeroVolume(0), when + duration + this.afterTime);
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 { envelope.audioBufferSourceNode.stop(0); envelope.audioBufferSourceNode.disconnect(); } catch (e) { }
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 = audioContext.createGain();
208
- envelope.target = target;
209
- envelope.connect(target);
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 > audioContext.currentTime) {
186
+ if (envelope.when + envelope.duration > this.#audioCtx.currentTime) {
212
187
  envelope.gain.cancelScheduledValues(0);
213
- envelope.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.1);
214
- 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;
215
190
  envelope.duration = 0;
216
191
  }
217
192
  };
218
- this.envelopes.push(envelope);
193
+ this.#envelopes.push(envelope);
219
194
  }
220
195
  return envelope;
221
196
  }
222
197
 
223
- findZone = (audioContext, preset, pitch) => {
224
- const zone = preset.zones.findLast(z => pitch >= z.keyRangeLow && pitch <= z.keyRangeHigh + 1);
225
- 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);
226
202
  return zone;
227
203
  };
228
204
 
229
- limitVolume = (v) => {
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
- resumeContext(audioContext) {
237
- if (audioContext.state === 'suspended') audioContext.resume().catch(() => { });
211
+
212
+ #noZeroVolume(n) {
213
+ return n > this.#nearZero ? n : this.#nearZero;
238
214
  }
239
215
 
240
- queueChord(ctx, tgt, prst, w, pchs, d, v, s) {
241
- const vol = this.limitVolume(v);
242
- 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;
243
219
  }
244
220
 
245
- cancelQueue(audioContext) {
246
- this.envelopes.forEach(e => {
247
- e.gain.cancelScheduledValues(0);
248
- e.gain.setValueAtTime(this.nearZero, audioContext.currentTime);
249
- e.when = -1;
250
- 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;
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;