audio-mixer-engine 0.1.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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/audio-mixer-engine.cjs.js +1 -0
- package/dist/audio-mixer-engine.es.js +1669 -0
- package/package.json +54 -0
- package/src/assets/stick-4cs.mp3 +0 -0
- package/src/assets/stick-4d.mp3 +0 -0
- package/src/index.js +18 -0
- package/src/lib/audio-engine.js +526 -0
- package/src/lib/beat-mapper.js +155 -0
- package/src/lib/midi-parser.js +718 -0
- package/src/lib/midi-player.js +700 -0
- package/src/lib/playback-manager.js +1257 -0
- package/src/lib/spessasynth-audio-engine.js +310 -0
- package/src/lib/spessasynth-channel-handle.js +151 -0
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "audio-mixer-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audio engine library for audio mixer applications with MIDI parsing, playback, and synthesis",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:coverage": "NODE_OPTIONS=\"--max-old-space-size=4096\" vitest run --coverage",
|
|
13
|
+
"test:parser": "NODE_OPTIONS=\"--max-old-space-size=4096\" vitest run --coverage tests/unit/lib/midi-parser.test.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"audio",
|
|
17
|
+
"midi",
|
|
18
|
+
"choir",
|
|
19
|
+
"music",
|
|
20
|
+
"synthesis",
|
|
21
|
+
"web-audio"
|
|
22
|
+
],
|
|
23
|
+
"author": "Your Name",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@vue/test-utils": "^2.4.6",
|
|
27
|
+
"mitt": "^3.0.1",
|
|
28
|
+
"spessasynth_lib": "^3.27.8",
|
|
29
|
+
"uuid": "^11.1.0",
|
|
30
|
+
"vite-plugin-vue-devtools": "^8.0.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@vitejs/plugin-vue": "^5.2.3",
|
|
34
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
35
|
+
"jsdom": "^26.0.0",
|
|
36
|
+
"node-web-audio-api": "^1.0.4",
|
|
37
|
+
"vite": "^6.2.4",
|
|
38
|
+
"vitest": "^3.1.1"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist/",
|
|
42
|
+
"src/",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/yourusername/audio-mixer-engine.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/yourusername/audio-mixer-engine/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/yourusername/audio-mixer-engine#readme"
|
|
54
|
+
}
|
|
Binary file
|
|
Binary file
|
package/src/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Mixer Engine - Main exports
|
|
3
|
+
*
|
|
4
|
+
* This library provides audio synthesis and MIDI playback capabilities
|
|
5
|
+
* specifically designed for audio mixer applications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Core audio engine
|
|
9
|
+
export { default as AudioEngine, ChannelHandle, AudioEngineUtils } from './lib/audio-engine.js';
|
|
10
|
+
|
|
11
|
+
// SpessaSynth implementation
|
|
12
|
+
export { default as SpessaSynthAudioEngine } from './lib/spessasynth-audio-engine.js';
|
|
13
|
+
export { default as SpessaSynthChannelHandle } from './lib/spessasynth-channel-handle.js';
|
|
14
|
+
|
|
15
|
+
// MIDI processing
|
|
16
|
+
export { default as MidiParser } from './lib/midi-parser.js';
|
|
17
|
+
export { default as BeatMapper } from './lib/beat-mapper.js';
|
|
18
|
+
export { default as MidiPlayer } from './lib/midi-player.js';
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract AudioEngine - Part-centric audio synthesis for audio mixers
|
|
3
|
+
* This class provides the contract for creating and managing musical parts
|
|
4
|
+
*/
|
|
5
|
+
export default class AudioEngine {
|
|
6
|
+
/**
|
|
7
|
+
* Create a new AudioEngine instance
|
|
8
|
+
* @param {AudioContext} audioContext - Web Audio API context
|
|
9
|
+
* @param {Object} options - Engine-specific options
|
|
10
|
+
*/
|
|
11
|
+
constructor(audioContext, options = {}) {
|
|
12
|
+
if (new.target === AudioEngine) {
|
|
13
|
+
throw new Error('AudioEngine is abstract and cannot be instantiated directly');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.audioContext = audioContext;
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.isInitialized = false;
|
|
19
|
+
this.channels = new WeakMap(); // ChannelHandle -> internal channel data
|
|
20
|
+
this.activeChannels = new Set(); // Track active channels for cleanup
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the audio engine - load soundfont and set up synthesis
|
|
25
|
+
* @param {string|ArrayBuffer} soundfontData - Path to soundfont or binary data
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async initialize(soundfontData) {
|
|
29
|
+
throw new Error('initialize() must be implemented by subclass');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new channel for a musical part
|
|
34
|
+
* @param {string} partId - Unique identifier for this part (e.g., 'soprano', 'piano')
|
|
35
|
+
* @param {Object} options - Channel configuration
|
|
36
|
+
* @param {string|number} [options.instrument] - Initial instrument
|
|
37
|
+
* @param {number} [options.initialVolume=1.0] - Initial volume (0.0-1.0)
|
|
38
|
+
* @returns {ChannelHandle} Handle object for controlling this channel
|
|
39
|
+
*/
|
|
40
|
+
createChannel(partId, options = {}) {
|
|
41
|
+
throw new Error('createChannel() must be implemented by subclass');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stop all notes on all channels
|
|
47
|
+
*/
|
|
48
|
+
allSoundsOff() {
|
|
49
|
+
throw new Error('allSoundsOff() must be implemented by subclass');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get list of all active channel handles
|
|
54
|
+
* @returns {Array<ChannelHandle>} Array of active channel handles
|
|
55
|
+
*/
|
|
56
|
+
getActiveChannels() {
|
|
57
|
+
return Array.from(this.activeChannels);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clean up all resources and disconnect audio nodes
|
|
62
|
+
*/
|
|
63
|
+
destroy() {
|
|
64
|
+
// Stop all sounds
|
|
65
|
+
this.allSoundsOff();
|
|
66
|
+
|
|
67
|
+
// Clear tracking collections
|
|
68
|
+
this.activeChannels.clear();
|
|
69
|
+
|
|
70
|
+
this.isInitialized = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate that the engine is initialized
|
|
75
|
+
* @protected
|
|
76
|
+
*/
|
|
77
|
+
_validateInitialized() {
|
|
78
|
+
if (!this.isInitialized) {
|
|
79
|
+
throw new Error('AudioEngine not initialized. Call initialize() first.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a new channel handle (called by subclasses)
|
|
85
|
+
* @param {ChannelHandle} handle - Channel handle to register
|
|
86
|
+
* @protected
|
|
87
|
+
*/
|
|
88
|
+
_registerChannel(handle) {
|
|
89
|
+
this.activeChannels.add(handle);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unregister a channel handle (called when channel is destroyed)
|
|
94
|
+
* @param {ChannelHandle} handle - Channel handle to unregister
|
|
95
|
+
* @protected
|
|
96
|
+
*/
|
|
97
|
+
_unregisterChannel(handle) {
|
|
98
|
+
this.activeChannels.delete(handle);
|
|
99
|
+
this.channels.delete(handle);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ChannelHandle - Interface for controlling one musical part
|
|
105
|
+
* Each instance represents one musical part (soprano, piano, etc.)
|
|
106
|
+
*/
|
|
107
|
+
export class ChannelHandle {
|
|
108
|
+
constructor(engine, partId, options = {}) {
|
|
109
|
+
if (new.target === ChannelHandle) {
|
|
110
|
+
throw new Error('ChannelHandle is abstract and cannot be instantiated directly');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.engine = engine;
|
|
114
|
+
this.partId = partId;
|
|
115
|
+
this.options = { initialVolume: 1.0, ...options };
|
|
116
|
+
this.isDestroyed = false;
|
|
117
|
+
|
|
118
|
+
// Reference counting for overlapping notes
|
|
119
|
+
this.noteRefCounts = new Map(); // pitch -> count
|
|
120
|
+
this.scheduledEvents = new Map(); // eventId -> timeoutId
|
|
121
|
+
this.activeNotes = new Set(); // Set of currently playing pitches
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the output audio node for this channel
|
|
126
|
+
* This node can be connected to gain controls, analyzers, etc.
|
|
127
|
+
* @returns {AudioNode} Output node (typically a GainNode)
|
|
128
|
+
*/
|
|
129
|
+
getOutputNode() {
|
|
130
|
+
throw new Error('getOutputNode() must be implemented by subclass');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start a note with reference counting (handles overlaps automatically)
|
|
135
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
136
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
137
|
+
*/
|
|
138
|
+
noteOn(pitch, velocity) {
|
|
139
|
+
this._validateActive();
|
|
140
|
+
|
|
141
|
+
const currentCount = this.noteRefCounts.get(pitch) || 0;
|
|
142
|
+
|
|
143
|
+
// Always increment reference count and start the note
|
|
144
|
+
this.noteRefCounts.set(pitch, currentCount + 1);
|
|
145
|
+
this._actualNoteOn(pitch, velocity);
|
|
146
|
+
|
|
147
|
+
// Add to active notes if not already playing
|
|
148
|
+
if (currentCount === 0) {
|
|
149
|
+
this.activeNotes.add(pitch);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Stop a note with reference counting (only stops when count reaches 0)
|
|
155
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
156
|
+
*/
|
|
157
|
+
noteOff(pitch) {
|
|
158
|
+
this._validateActive();
|
|
159
|
+
|
|
160
|
+
const currentCount = this.noteRefCounts.get(pitch) || 0;
|
|
161
|
+
|
|
162
|
+
if (currentCount <= 0) {
|
|
163
|
+
return; // No notes to stop
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const newCount = currentCount - 1;
|
|
167
|
+
this.noteRefCounts.set(pitch, newCount);
|
|
168
|
+
|
|
169
|
+
// Only actually stop the note when reference count reaches 0
|
|
170
|
+
if (newCount === 0) {
|
|
171
|
+
this._actualNoteOff(pitch);
|
|
172
|
+
this.activeNotes.delete(pitch);
|
|
173
|
+
this.noteRefCounts.delete(pitch);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Schedule a note with automatic timing adjustment
|
|
179
|
+
* @param {number} startTime - Absolute audio context time when note should start
|
|
180
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
181
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
182
|
+
* @param {number} duration - Note duration in seconds
|
|
183
|
+
* @returns {string} Event ID for cancellation
|
|
184
|
+
*/
|
|
185
|
+
playNote(startTime, pitch, velocity, duration) {
|
|
186
|
+
this._validateActive();
|
|
187
|
+
|
|
188
|
+
const currentTime = this.engine.audioContext.currentTime;
|
|
189
|
+
const eventId = `${this.partId}_${startTime}_${pitch}_${Date.now()}`;
|
|
190
|
+
|
|
191
|
+
// Adjust timing if start time is in the past
|
|
192
|
+
let adjustedStartTime = startTime;
|
|
193
|
+
let adjustedDuration = duration;
|
|
194
|
+
|
|
195
|
+
if (startTime < currentTime) {
|
|
196
|
+
const timePassed = currentTime - startTime;
|
|
197
|
+
adjustedStartTime = currentTime;
|
|
198
|
+
adjustedDuration = Math.max(0, duration - timePassed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If duration is too short after adjustment, skip this note
|
|
202
|
+
if (adjustedDuration <= 0) {
|
|
203
|
+
return eventId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Schedule note on
|
|
207
|
+
const noteOnDelay = Math.max(0, (adjustedStartTime - currentTime) * 1000);
|
|
208
|
+
const noteOnTimeoutId = setTimeout(() => {
|
|
209
|
+
this.noteOn(pitch, velocity);
|
|
210
|
+
this.scheduledEvents.delete(`${eventId}_on`);
|
|
211
|
+
}, noteOnDelay);
|
|
212
|
+
|
|
213
|
+
// Schedule note off
|
|
214
|
+
const noteOffDelay = noteOnDelay + (adjustedDuration * 1000);
|
|
215
|
+
const noteOffTimeoutId = setTimeout(() => {
|
|
216
|
+
this.noteOff(pitch);
|
|
217
|
+
this.scheduledEvents.delete(`${eventId}_off`);
|
|
218
|
+
}, noteOffDelay);
|
|
219
|
+
|
|
220
|
+
// Track both events for cancellation
|
|
221
|
+
this.scheduledEvents.set(`${eventId}_on`, noteOnTimeoutId);
|
|
222
|
+
this.scheduledEvents.set(`${eventId}_off`, noteOffTimeoutId);
|
|
223
|
+
|
|
224
|
+
return eventId;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stop all notes on this channel
|
|
229
|
+
*/
|
|
230
|
+
allNotesOff() {
|
|
231
|
+
this._validateActive();
|
|
232
|
+
|
|
233
|
+
// Cancel all scheduled events
|
|
234
|
+
this.scheduledEvents.forEach(timeoutId => {
|
|
235
|
+
clearTimeout(timeoutId);
|
|
236
|
+
});
|
|
237
|
+
this.scheduledEvents.clear();
|
|
238
|
+
|
|
239
|
+
// Stop all actively playing notes
|
|
240
|
+
this.activeNotes.forEach(pitch => {
|
|
241
|
+
this._actualNoteOff(pitch);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Reset all state
|
|
245
|
+
this.noteRefCounts.clear();
|
|
246
|
+
this.activeNotes.clear();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Actually start a note (implemented by subclass)
|
|
251
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
252
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
253
|
+
* @protected
|
|
254
|
+
*/
|
|
255
|
+
_actualNoteOn(pitch, velocity) {
|
|
256
|
+
throw new Error('_actualNoteOn() must be implemented by subclass');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Actually stop a note (implemented by subclass)
|
|
261
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
262
|
+
* @protected
|
|
263
|
+
*/
|
|
264
|
+
_actualNoteOff(pitch) {
|
|
265
|
+
throw new Error('_actualNoteOff() must be implemented by subclass');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Change the instrument for this channel
|
|
270
|
+
* @param {string|number} instrument - Instrument name or program number
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async setInstrument(instrument) {
|
|
274
|
+
throw new Error('setInstrument() must be implemented by subclass');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get current instrument for this channel
|
|
279
|
+
* @returns {string|number} Current instrument
|
|
280
|
+
*/
|
|
281
|
+
getInstrument() {
|
|
282
|
+
throw new Error('getInstrument() must be implemented by subclass');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Set volume for this channel (affects the internal channel volume)
|
|
287
|
+
* Note: External volume control should use the output node from getOutputNode()
|
|
288
|
+
* @param {number} volume - Volume level (0.0-1.0)
|
|
289
|
+
*/
|
|
290
|
+
setVolume(volume) {
|
|
291
|
+
throw new Error('setVolume() must be implemented by subclass');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get current volume for this channel
|
|
296
|
+
* @returns {number} Current volume (0.0-1.0)
|
|
297
|
+
*/
|
|
298
|
+
getVolume() {
|
|
299
|
+
throw new Error('getVolume() must be implemented by subclass');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the part ID for this channel
|
|
304
|
+
* @returns {string} Part identifier
|
|
305
|
+
*/
|
|
306
|
+
getPartId() {
|
|
307
|
+
return this.partId;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check if this channel is still active
|
|
312
|
+
* @returns {boolean} True if channel is active
|
|
313
|
+
*/
|
|
314
|
+
isActive() {
|
|
315
|
+
return !this.isDestroyed && this.engine.isInitialized && this.engine.activeChannels.has(this);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Destroy this channel and clean up resources
|
|
320
|
+
*/
|
|
321
|
+
destroy() {
|
|
322
|
+
if (!this.isDestroyed) {
|
|
323
|
+
this.allNotesOff();
|
|
324
|
+
|
|
325
|
+
const outputNode = this.getOutputNode();
|
|
326
|
+
if (outputNode) {
|
|
327
|
+
outputNode.disconnect();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Clear reference counting state
|
|
331
|
+
this.noteRefCounts.clear();
|
|
332
|
+
this.scheduledEvents.clear();
|
|
333
|
+
this.activeNotes.clear();
|
|
334
|
+
|
|
335
|
+
this.engine._unregisterChannel(this);
|
|
336
|
+
this.isDestroyed = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Validate that this channel is still active
|
|
342
|
+
* @protected
|
|
343
|
+
*/
|
|
344
|
+
_validateActive() {
|
|
345
|
+
if (this.isDestroyed) {
|
|
346
|
+
throw new Error('Channel has been destroyed');
|
|
347
|
+
}
|
|
348
|
+
if (!this.engine.isInitialized) {
|
|
349
|
+
throw new Error('AudioEngine is not initialized');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Utility class for common audio engine operations
|
|
356
|
+
*/
|
|
357
|
+
export class AudioEngineUtils {
|
|
358
|
+
/**
|
|
359
|
+
* Map common instrument names to MIDI program numbers
|
|
360
|
+
* @param {string|number} instrument - Instrument name or program number
|
|
361
|
+
* @returns {number} MIDI program number
|
|
362
|
+
*/
|
|
363
|
+
static getInstrumentProgram(instrument) {
|
|
364
|
+
if (typeof instrument === 'number') return instrument;
|
|
365
|
+
|
|
366
|
+
const instrumentMap = {
|
|
367
|
+
// Piano family (0-7)
|
|
368
|
+
'piano': 0, 'bright_piano': 1, 'electric_grand': 2, 'honky_tonk': 3,
|
|
369
|
+
'electric_piano_1': 4, 'electric_piano_2': 5, 'harpsichord': 6, 'clavinet': 7,
|
|
370
|
+
|
|
371
|
+
// Chromatic percussion (8-15)
|
|
372
|
+
'celesta': 8, 'glockenspiel': 9, 'music_box': 10, 'vibraphone': 11,
|
|
373
|
+
'marimba': 12, 'xylophone': 13, 'tubular_bells': 14, 'dulcimer': 15,
|
|
374
|
+
|
|
375
|
+
// Organ (16-23)
|
|
376
|
+
'drawbar_organ': 16, 'percussive_organ': 17, 'rock_organ': 18, 'church_organ': 19,
|
|
377
|
+
'reed_organ': 20, 'accordion': 21, 'harmonica': 22, 'tango_accordion': 23,
|
|
378
|
+
'organ': 19,
|
|
379
|
+
|
|
380
|
+
// Guitar (24-31)
|
|
381
|
+
'nylon_guitar': 24, 'steel_guitar': 25, 'electric_guitar_jazz': 26, 'electric_guitar_clean': 27,
|
|
382
|
+
'electric_guitar_muted': 28, 'overdriven_guitar': 29, 'distortion_guitar': 30, 'guitar_harmonics': 31,
|
|
383
|
+
'guitar': 24,
|
|
384
|
+
|
|
385
|
+
// Bass (32-39)
|
|
386
|
+
'acoustic_bass': 32, 'electric_bass_finger': 33, 'electric_bass_pick': 34, 'fretless_bass': 35,
|
|
387
|
+
'slap_bass_1': 36, 'slap_bass_2': 37, 'synth_bass_1': 38, 'synth_bass_2': 39,
|
|
388
|
+
'bass': 32,
|
|
389
|
+
|
|
390
|
+
// Strings (40-47)
|
|
391
|
+
'violin': 40, 'viola': 41, 'cello': 42, 'contrabass': 43,
|
|
392
|
+
'tremolo_strings': 44, 'pizzicato_strings': 45, 'orchestral_harp': 46, 'timpani': 47,
|
|
393
|
+
'strings': 48, 'strings_ensemble': 48,
|
|
394
|
+
|
|
395
|
+
// Ensemble (48-55)
|
|
396
|
+
'slow_strings': 49, 'synth_strings_1': 50, 'synth_strings_2': 51,
|
|
397
|
+
'choir_aahs': 52, 'voice_oohs': 53, 'synth_voice': 54, 'orchestra_hit': 55,
|
|
398
|
+
|
|
399
|
+
// Brass (56-63)
|
|
400
|
+
'trumpet': 56, 'trombone': 57, 'tuba': 58, 'muted_trumpet': 59,
|
|
401
|
+
'french_horn': 60, 'brass_section': 61, 'synth_brass_1': 62, 'synth_brass_2': 63,
|
|
402
|
+
|
|
403
|
+
// Reed (64-71)
|
|
404
|
+
'soprano_sax': 64, 'alto_sax': 65, 'tenor_sax': 66, 'baritone_sax': 67,
|
|
405
|
+
'oboe': 68, 'english_horn': 69, 'bassoon': 70, 'clarinet': 71,
|
|
406
|
+
'saxophone': 64,
|
|
407
|
+
|
|
408
|
+
// Pipe (72-79)
|
|
409
|
+
'piccolo': 72, 'flute': 73, 'recorder': 74, 'pan_flute': 75,
|
|
410
|
+
'blown_bottle': 76, 'shakuhachi': 77, 'whistle': 78, 'ocarina': 79,
|
|
411
|
+
|
|
412
|
+
// Synth lead (80-87)
|
|
413
|
+
'lead_1_square': 80, 'lead_2_sawtooth': 81, 'lead_3_calliope': 82, 'lead_4_chiff': 83,
|
|
414
|
+
'lead_5_charang': 84, 'lead_6_voice': 85, 'lead_7_fifths': 86, 'lead_8_bass': 87,
|
|
415
|
+
|
|
416
|
+
// Synth pad (88-95)
|
|
417
|
+
'pad_1_new_age': 88, 'pad_2_warm': 89, 'pad_3_polysynth': 90, 'pad_4_choir': 91,
|
|
418
|
+
'pad_5_bowed': 92, 'pad_6_metallic': 93, 'pad_7_halo': 94, 'pad_8_sweep': 95,
|
|
419
|
+
|
|
420
|
+
// Synth effects (96-103)
|
|
421
|
+
'fx_1_rain': 96, 'fx_2_soundtrack': 97, 'fx_3_crystal': 98, 'fx_4_atmosphere': 99,
|
|
422
|
+
'fx_5_brightness': 100, 'fx_6_goblins': 101, 'fx_7_echoes': 102, 'fx_8_sci_fi': 103,
|
|
423
|
+
|
|
424
|
+
// Ethnic (104-111)
|
|
425
|
+
'sitar': 104, 'banjo': 105, 'shamisen': 106, 'koto': 107,
|
|
426
|
+
'kalimba': 108, 'bag_pipe': 109, 'fiddle': 110, 'shanai': 111,
|
|
427
|
+
|
|
428
|
+
// Percussive (112-119)
|
|
429
|
+
'tinkle_bell': 112, 'agogo': 113, 'steel_drums': 114, 'woodblock': 115,
|
|
430
|
+
'taiko_drum': 116, 'melodic_tom': 117, 'synth_drum': 118, 'reverse_cymbal': 119,
|
|
431
|
+
|
|
432
|
+
// Sound effects (120-127)
|
|
433
|
+
'guitar_fret_noise': 120, 'breath_noise': 121, 'seashore': 122, 'bird_tweet': 123,
|
|
434
|
+
'telephone_ring': 124, 'helicopter': 125, 'applause': 126, 'gunshot': 127
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const program = instrumentMap[instrument.toLowerCase()];
|
|
438
|
+
return program !== undefined ? program : 0; // Default to Piano
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get instrument name from MIDI program number (for display purposes)
|
|
443
|
+
* @param {number} programNumber - MIDI program number (0-127)
|
|
444
|
+
* @returns {string} Instrument name or fallback
|
|
445
|
+
*/
|
|
446
|
+
static getProgramName(programNumber) {
|
|
447
|
+
const programNames = [
|
|
448
|
+
// Piano family (0-7)
|
|
449
|
+
'Piano', 'Bright Piano', 'Electric Grand', 'Honky-tonk Piano',
|
|
450
|
+
'Electric Piano 1', 'Electric Piano 2', 'Harpsichord', 'Clavinet',
|
|
451
|
+
|
|
452
|
+
// Chromatic percussion (8-15)
|
|
453
|
+
'Celesta', 'Glockenspiel', 'Music Box', 'Vibraphone',
|
|
454
|
+
'Marimba', 'Xylophone', 'Tubular Bells', 'Dulcimer',
|
|
455
|
+
|
|
456
|
+
// Organ (16-23)
|
|
457
|
+
'Drawbar Organ', 'Percussive Organ', 'Rock Organ', 'Church Organ',
|
|
458
|
+
'Reed Organ', 'Accordion', 'Harmonica', 'Tango Accordion',
|
|
459
|
+
|
|
460
|
+
// Guitar (24-31)
|
|
461
|
+
'Nylon Guitar', 'Steel Guitar', 'Electric Guitar (jazz)', 'Electric Guitar (clean)',
|
|
462
|
+
'Electric Guitar (muted)', 'Overdriven Guitar', 'Distortion Guitar', 'Guitar Harmonics',
|
|
463
|
+
|
|
464
|
+
// Bass (32-39)
|
|
465
|
+
'Acoustic Bass', 'Electric Bass (finger)', 'Electric Bass (pick)', 'Fretless Bass',
|
|
466
|
+
'Slap Bass 1', 'Slap Bass 2', 'Synth Bass 1', 'Synth Bass 2',
|
|
467
|
+
|
|
468
|
+
// Strings (40-47)
|
|
469
|
+
'Violin', 'Viola', 'Cello', 'Contrabass',
|
|
470
|
+
'Tremolo Strings', 'Pizzicato Strings', 'Orchestral Harp', 'Timpani',
|
|
471
|
+
|
|
472
|
+
// Ensemble (48-55)
|
|
473
|
+
'String Ensemble 1', 'String Ensemble 2', 'Synth Strings 1', 'Synth Strings 2',
|
|
474
|
+
'Choir Aahs', 'Voice Oohs', 'Synth Voice', 'Orchestra Hit',
|
|
475
|
+
|
|
476
|
+
// Brass (56-63)
|
|
477
|
+
'Trumpet', 'Trombone', 'Tuba', 'Muted Trumpet',
|
|
478
|
+
'French Horn', 'Brass Section', 'Synth Brass 1', 'Synth Brass 2',
|
|
479
|
+
|
|
480
|
+
// Reed (64-71)
|
|
481
|
+
'Soprano Sax', 'Alto Sax', 'Tenor Sax', 'Baritone Sax',
|
|
482
|
+
'Oboe', 'English Horn', 'Bassoon', 'Clarinet',
|
|
483
|
+
|
|
484
|
+
// Pipe (72-79)
|
|
485
|
+
'Piccolo', 'Flute', 'Recorder', 'Pan Flute',
|
|
486
|
+
'Blown Bottle', 'Shakuhachi', 'Whistle', 'Ocarina',
|
|
487
|
+
|
|
488
|
+
// Synth lead (80-87)
|
|
489
|
+
'Lead 1 (square)', 'Lead 2 (sawtooth)', 'Lead 3 (calliope)', 'Lead 4 (chiff)',
|
|
490
|
+
'Lead 5 (charang)', 'Lead 6 (voice)', 'Lead 7 (fifths)', 'Lead 8 (bass + lead)',
|
|
491
|
+
|
|
492
|
+
// Synth pad (88-95)
|
|
493
|
+
'Pad 1 (new age)', 'Pad 2 (warm)', 'Pad 3 (polysynth)', 'Pad 4 (choir)',
|
|
494
|
+
'Pad 5 (bowed)', 'Pad 6 (metallic)', 'Pad 7 (halo)', 'Pad 8 (sweep)',
|
|
495
|
+
|
|
496
|
+
// Synth effects (96-103)
|
|
497
|
+
'FX 1 (rain)', 'FX 2 (soundtrack)', 'FX 3 (crystal)', 'FX 4 (atmosphere)',
|
|
498
|
+
'FX 5 (brightness)', 'FX 6 (goblins)', 'FX 7 (echoes)', 'FX 8 (sci-fi)',
|
|
499
|
+
|
|
500
|
+
// Ethnic (104-111)
|
|
501
|
+
'Sitar', 'Banjo', 'Shamisen', 'Koto',
|
|
502
|
+
'Kalimba', 'Bag pipe', 'Fiddle', 'Shanai',
|
|
503
|
+
|
|
504
|
+
// Percussive (112-119)
|
|
505
|
+
'Tinkle Bell', 'Agogo', 'Steel Drums', 'Woodblock',
|
|
506
|
+
'Taiko Drum', 'Melodic Tom', 'Synth Drum', 'Reverse Cymbal',
|
|
507
|
+
|
|
508
|
+
// Sound effects (120-127)
|
|
509
|
+
'Guitar Fret Noise', 'Breath Noise', 'Seashore', 'Bird Tweet',
|
|
510
|
+
'Telephone Ring', 'Helicopter', 'Applause', 'Gunshot'
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
return programNames[programNumber] || `Program ${programNumber}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate a unique note ID
|
|
518
|
+
* @param {number} channel - MIDI channel
|
|
519
|
+
* @param {number} pitch - MIDI pitch
|
|
520
|
+
* @param {number} startTime - Start time
|
|
521
|
+
* @returns {string} Unique note ID
|
|
522
|
+
*/
|
|
523
|
+
static generateNoteId(channel, pitch, startTime) {
|
|
524
|
+
return `${channel}_${pitch}_${Math.round(startTime)}`;
|
|
525
|
+
}
|
|
526
|
+
}
|