audio-mixer-engine 0.2.0 → 0.3.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/README.md +50 -45
- package/dist/audio-mixer-engine.cjs.js +1 -1
- package/dist/audio-mixer-engine.es.js +380 -623
- package/package.json +1 -1
- package/src/lib/audio-engine.js +85 -142
- package/src/lib/beat-mapper.js +1 -84
- package/src/lib/midi-player.js +4 -33
- package/src/lib/playback-manager.js +12 -114
package/package.json
CHANGED
package/src/lib/audio-engine.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import stick4csUrl from '../assets/stick-4cs.mp3';
|
|
2
|
+
import stick4dUrl from '../assets/stick-4d.mp3';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Abstract AudioEngine - Part-centric audio synthesis for audio mixers
|
|
3
6
|
* This class provides the contract for creating and managing musical parts
|
|
@@ -124,8 +127,8 @@ export default class AudioEngine {
|
|
|
124
127
|
// Try to load tick sound files if available
|
|
125
128
|
if (typeof fetch !== 'undefined') {
|
|
126
129
|
const [regularResponse, accentResponse] = await Promise.all([
|
|
127
|
-
fetch(
|
|
128
|
-
fetch(
|
|
130
|
+
fetch(stick4csUrl),
|
|
131
|
+
fetch(stick4dUrl)
|
|
129
132
|
]);
|
|
130
133
|
|
|
131
134
|
const [regularBuffer, accentBuffer] = await Promise.all([
|
|
@@ -461,6 +464,83 @@ export class ChannelHandle {
|
|
|
461
464
|
}
|
|
462
465
|
}
|
|
463
466
|
|
|
467
|
+
const MidiInstrumentNumbers = {
|
|
468
|
+
// Piano family (0-7)
|
|
469
|
+
'piano': 0, 'bright_piano': 1, 'electric_grand': 2, 'honky_tonk': 3,
|
|
470
|
+
'electric_piano_1': 4, 'electric_piano_2': 5, 'harpsichord': 6, 'clavinet': 7,
|
|
471
|
+
|
|
472
|
+
// Chromatic percussion (8-15)
|
|
473
|
+
'celesta': 8, 'glockenspiel': 9, 'music_box': 10, 'vibraphone': 11,
|
|
474
|
+
'marimba': 12, 'xylophone': 13, 'tubular_bells': 14, 'dulcimer': 15,
|
|
475
|
+
|
|
476
|
+
// Organ (16-23)
|
|
477
|
+
'drawbar_organ': 16, 'percussive_organ': 17, 'rock_organ': 18, 'church_organ': 19,
|
|
478
|
+
'reed_organ': 20, 'accordion': 21, 'harmonica': 22, 'tango_accordion': 23,
|
|
479
|
+
'organ': 19,
|
|
480
|
+
|
|
481
|
+
// Guitar (24-31)
|
|
482
|
+
'nylon_guitar': 24, 'steel_guitar': 25, 'electric_guitar_jazz': 26, 'electric_guitar_clean': 27,
|
|
483
|
+
'electric_guitar_muted': 28, 'overdriven_guitar': 29, 'distortion_guitar': 30, 'guitar_harmonics': 31,
|
|
484
|
+
'guitar': 24,
|
|
485
|
+
|
|
486
|
+
// Bass (32-39)
|
|
487
|
+
'acoustic_bass': 32, 'electric_bass_finger': 33, 'electric_bass_pick': 34, 'fretless_bass': 35,
|
|
488
|
+
'slap_bass_1': 36, 'slap_bass_2': 37, 'synth_bass_1': 38, 'synth_bass_2': 39,
|
|
489
|
+
'bass': 32,
|
|
490
|
+
|
|
491
|
+
// Strings (40-47)
|
|
492
|
+
'violin': 40, 'viola': 41, 'cello': 42, 'contrabass': 43,
|
|
493
|
+
'tremolo_strings': 44, 'pizzicato_strings': 45, 'orchestral_harp': 46, 'timpani': 47,
|
|
494
|
+
'strings': 48, 'strings_ensemble': 48,
|
|
495
|
+
|
|
496
|
+
// Ensemble (48-55)
|
|
497
|
+
'slow_strings': 49, 'synth_strings_1': 50, 'synth_strings_2': 51,
|
|
498
|
+
'choir_aahs': 52, 'voice_oohs': 53, 'synth_voice': 54, 'orchestra_hit': 55,
|
|
499
|
+
|
|
500
|
+
// Brass (56-63)
|
|
501
|
+
'trumpet': 56, 'trombone': 57, 'tuba': 58, 'muted_trumpet': 59,
|
|
502
|
+
'french_horn': 60, 'brass_section': 61, 'synth_brass_1': 62, 'synth_brass_2': 63,
|
|
503
|
+
|
|
504
|
+
// Reed (64-71)
|
|
505
|
+
'soprano_sax': 64, 'alto_sax': 65, 'tenor_sax': 66, 'baritone_sax': 67,
|
|
506
|
+
'oboe': 68, 'english_horn': 69, 'bassoon': 70, 'clarinet': 71,
|
|
507
|
+
'saxophone': 64,
|
|
508
|
+
|
|
509
|
+
// Pipe (72-79)
|
|
510
|
+
'piccolo': 72, 'flute': 73, 'recorder': 74, 'pan_flute': 75,
|
|
511
|
+
'blown_bottle': 76, 'shakuhachi': 77, 'whistle': 78, 'ocarina': 79,
|
|
512
|
+
|
|
513
|
+
// Synth lead (80-87)
|
|
514
|
+
'lead_1_square': 80, 'lead_2_sawtooth': 81, 'lead_3_calliope': 82, 'lead_4_chiff': 83,
|
|
515
|
+
'lead_5_charang': 84, 'lead_6_voice': 85, 'lead_7_fifths': 86, 'lead_8_bass': 87,
|
|
516
|
+
|
|
517
|
+
// Synth pad (88-95)
|
|
518
|
+
'pad_1_new_age': 88, 'pad_2_warm': 89, 'pad_3_polysynth': 90, 'pad_4_choir': 91,
|
|
519
|
+
'pad_5_bowed': 92, 'pad_6_metallic': 93, 'pad_7_halo': 94, 'pad_8_sweep': 95,
|
|
520
|
+
|
|
521
|
+
// Synth effects (96-103)
|
|
522
|
+
'fx_1_rain': 96, 'fx_2_soundtrack': 97, 'fx_3_crystal': 98, 'fx_4_atmosphere': 99,
|
|
523
|
+
'fx_5_brightness': 100, 'fx_6_goblins': 101, 'fx_7_echoes': 102, 'fx_8_sci_fi': 103,
|
|
524
|
+
|
|
525
|
+
// Ethnic (104-111)
|
|
526
|
+
'sitar': 104, 'banjo': 105, 'shamisen': 106, 'koto': 107,
|
|
527
|
+
'kalimba': 108, 'bag_pipe': 109, 'fiddle': 110, 'shanai': 111,
|
|
528
|
+
|
|
529
|
+
// Percussive (112-119)
|
|
530
|
+
'tinkle_bell': 112, 'agogo': 113, 'steel_drums': 114, 'woodblock': 115,
|
|
531
|
+
'taiko_drum': 116, 'melodic_tom': 117, 'synth_drum': 118, 'reverse_cymbal': 119,
|
|
532
|
+
|
|
533
|
+
// Sound effects (120-127)
|
|
534
|
+
'guitar_fret_noise': 120, 'breath_noise': 121, 'seashore': 122, 'bird_tweet': 123,
|
|
535
|
+
'telephone_ring': 124, 'helicopter': 125, 'applause': 126, 'gunshot': 127
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const MidiInstrumentNames = Object.entries(MidiInstrumentNumbers)
|
|
539
|
+
.reduce((o,[k,v])=>{
|
|
540
|
+
o[v]=k;
|
|
541
|
+
return o;
|
|
542
|
+
},{})
|
|
543
|
+
|
|
464
544
|
/**
|
|
465
545
|
* Utility class for common audio engine operations
|
|
466
546
|
*/
|
|
@@ -472,79 +552,7 @@ export class AudioEngineUtils {
|
|
|
472
552
|
*/
|
|
473
553
|
static getInstrumentProgram(instrument) {
|
|
474
554
|
if (typeof instrument === 'number') return instrument;
|
|
475
|
-
|
|
476
|
-
const instrumentMap = {
|
|
477
|
-
// Piano family (0-7)
|
|
478
|
-
'piano': 0, 'bright_piano': 1, 'electric_grand': 2, 'honky_tonk': 3,
|
|
479
|
-
'electric_piano_1': 4, 'electric_piano_2': 5, 'harpsichord': 6, 'clavinet': 7,
|
|
480
|
-
|
|
481
|
-
// Chromatic percussion (8-15)
|
|
482
|
-
'celesta': 8, 'glockenspiel': 9, 'music_box': 10, 'vibraphone': 11,
|
|
483
|
-
'marimba': 12, 'xylophone': 13, 'tubular_bells': 14, 'dulcimer': 15,
|
|
484
|
-
|
|
485
|
-
// Organ (16-23)
|
|
486
|
-
'drawbar_organ': 16, 'percussive_organ': 17, 'rock_organ': 18, 'church_organ': 19,
|
|
487
|
-
'reed_organ': 20, 'accordion': 21, 'harmonica': 22, 'tango_accordion': 23,
|
|
488
|
-
'organ': 19,
|
|
489
|
-
|
|
490
|
-
// Guitar (24-31)
|
|
491
|
-
'nylon_guitar': 24, 'steel_guitar': 25, 'electric_guitar_jazz': 26, 'electric_guitar_clean': 27,
|
|
492
|
-
'electric_guitar_muted': 28, 'overdriven_guitar': 29, 'distortion_guitar': 30, 'guitar_harmonics': 31,
|
|
493
|
-
'guitar': 24,
|
|
494
|
-
|
|
495
|
-
// Bass (32-39)
|
|
496
|
-
'acoustic_bass': 32, 'electric_bass_finger': 33, 'electric_bass_pick': 34, 'fretless_bass': 35,
|
|
497
|
-
'slap_bass_1': 36, 'slap_bass_2': 37, 'synth_bass_1': 38, 'synth_bass_2': 39,
|
|
498
|
-
'bass': 32,
|
|
499
|
-
|
|
500
|
-
// Strings (40-47)
|
|
501
|
-
'violin': 40, 'viola': 41, 'cello': 42, 'contrabass': 43,
|
|
502
|
-
'tremolo_strings': 44, 'pizzicato_strings': 45, 'orchestral_harp': 46, 'timpani': 47,
|
|
503
|
-
'strings': 48, 'strings_ensemble': 48,
|
|
504
|
-
|
|
505
|
-
// Ensemble (48-55)
|
|
506
|
-
'slow_strings': 49, 'synth_strings_1': 50, 'synth_strings_2': 51,
|
|
507
|
-
'choir_aahs': 52, 'voice_oohs': 53, 'synth_voice': 54, 'orchestra_hit': 55,
|
|
508
|
-
|
|
509
|
-
// Brass (56-63)
|
|
510
|
-
'trumpet': 56, 'trombone': 57, 'tuba': 58, 'muted_trumpet': 59,
|
|
511
|
-
'french_horn': 60, 'brass_section': 61, 'synth_brass_1': 62, 'synth_brass_2': 63,
|
|
512
|
-
|
|
513
|
-
// Reed (64-71)
|
|
514
|
-
'soprano_sax': 64, 'alto_sax': 65, 'tenor_sax': 66, 'baritone_sax': 67,
|
|
515
|
-
'oboe': 68, 'english_horn': 69, 'bassoon': 70, 'clarinet': 71,
|
|
516
|
-
'saxophone': 64,
|
|
517
|
-
|
|
518
|
-
// Pipe (72-79)
|
|
519
|
-
'piccolo': 72, 'flute': 73, 'recorder': 74, 'pan_flute': 75,
|
|
520
|
-
'blown_bottle': 76, 'shakuhachi': 77, 'whistle': 78, 'ocarina': 79,
|
|
521
|
-
|
|
522
|
-
// Synth lead (80-87)
|
|
523
|
-
'lead_1_square': 80, 'lead_2_sawtooth': 81, 'lead_3_calliope': 82, 'lead_4_chiff': 83,
|
|
524
|
-
'lead_5_charang': 84, 'lead_6_voice': 85, 'lead_7_fifths': 86, 'lead_8_bass': 87,
|
|
525
|
-
|
|
526
|
-
// Synth pad (88-95)
|
|
527
|
-
'pad_1_new_age': 88, 'pad_2_warm': 89, 'pad_3_polysynth': 90, 'pad_4_choir': 91,
|
|
528
|
-
'pad_5_bowed': 92, 'pad_6_metallic': 93, 'pad_7_halo': 94, 'pad_8_sweep': 95,
|
|
529
|
-
|
|
530
|
-
// Synth effects (96-103)
|
|
531
|
-
'fx_1_rain': 96, 'fx_2_soundtrack': 97, 'fx_3_crystal': 98, 'fx_4_atmosphere': 99,
|
|
532
|
-
'fx_5_brightness': 100, 'fx_6_goblins': 101, 'fx_7_echoes': 102, 'fx_8_sci_fi': 103,
|
|
533
|
-
|
|
534
|
-
// Ethnic (104-111)
|
|
535
|
-
'sitar': 104, 'banjo': 105, 'shamisen': 106, 'koto': 107,
|
|
536
|
-
'kalimba': 108, 'bag_pipe': 109, 'fiddle': 110, 'shanai': 111,
|
|
537
|
-
|
|
538
|
-
// Percussive (112-119)
|
|
539
|
-
'tinkle_bell': 112, 'agogo': 113, 'steel_drums': 114, 'woodblock': 115,
|
|
540
|
-
'taiko_drum': 116, 'melodic_tom': 117, 'synth_drum': 118, 'reverse_cymbal': 119,
|
|
541
|
-
|
|
542
|
-
// Sound effects (120-127)
|
|
543
|
-
'guitar_fret_noise': 120, 'breath_noise': 121, 'seashore': 122, 'bird_tweet': 123,
|
|
544
|
-
'telephone_ring': 124, 'helicopter': 125, 'applause': 126, 'gunshot': 127
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
const program = instrumentMap[instrument.toLowerCase()];
|
|
555
|
+
const program = MidiInstrumentNumbers[instrument.toLowerCase()];
|
|
548
556
|
return program !== undefined ? program : 0; // Default to Piano
|
|
549
557
|
}
|
|
550
558
|
|
|
@@ -554,73 +562,8 @@ export class AudioEngineUtils {
|
|
|
554
562
|
* @returns {string} Instrument name or fallback
|
|
555
563
|
*/
|
|
556
564
|
static getProgramName(programNumber) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
'Piano', 'Bright Piano', 'Electric Grand', 'Honky-tonk Piano',
|
|
560
|
-
'Electric Piano 1', 'Electric Piano 2', 'Harpsichord', 'Clavinet',
|
|
561
|
-
|
|
562
|
-
// Chromatic percussion (8-15)
|
|
563
|
-
'Celesta', 'Glockenspiel', 'Music Box', 'Vibraphone',
|
|
564
|
-
'Marimba', 'Xylophone', 'Tubular Bells', 'Dulcimer',
|
|
565
|
-
|
|
566
|
-
// Organ (16-23)
|
|
567
|
-
'Drawbar Organ', 'Percussive Organ', 'Rock Organ', 'Church Organ',
|
|
568
|
-
'Reed Organ', 'Accordion', 'Harmonica', 'Tango Accordion',
|
|
569
|
-
|
|
570
|
-
// Guitar (24-31)
|
|
571
|
-
'Nylon Guitar', 'Steel Guitar', 'Electric Guitar (jazz)', 'Electric Guitar (clean)',
|
|
572
|
-
'Electric Guitar (muted)', 'Overdriven Guitar', 'Distortion Guitar', 'Guitar Harmonics',
|
|
573
|
-
|
|
574
|
-
// Bass (32-39)
|
|
575
|
-
'Acoustic Bass', 'Electric Bass (finger)', 'Electric Bass (pick)', 'Fretless Bass',
|
|
576
|
-
'Slap Bass 1', 'Slap Bass 2', 'Synth Bass 1', 'Synth Bass 2',
|
|
577
|
-
|
|
578
|
-
// Strings (40-47)
|
|
579
|
-
'Violin', 'Viola', 'Cello', 'Contrabass',
|
|
580
|
-
'Tremolo Strings', 'Pizzicato Strings', 'Orchestral Harp', 'Timpani',
|
|
581
|
-
|
|
582
|
-
// Ensemble (48-55)
|
|
583
|
-
'String Ensemble 1', 'String Ensemble 2', 'Synth Strings 1', 'Synth Strings 2',
|
|
584
|
-
'Choir Aahs', 'Voice Oohs', 'Synth Voice', 'Orchestra Hit',
|
|
585
|
-
|
|
586
|
-
// Brass (56-63)
|
|
587
|
-
'Trumpet', 'Trombone', 'Tuba', 'Muted Trumpet',
|
|
588
|
-
'French Horn', 'Brass Section', 'Synth Brass 1', 'Synth Brass 2',
|
|
589
|
-
|
|
590
|
-
// Reed (64-71)
|
|
591
|
-
'Soprano Sax', 'Alto Sax', 'Tenor Sax', 'Baritone Sax',
|
|
592
|
-
'Oboe', 'English Horn', 'Bassoon', 'Clarinet',
|
|
593
|
-
|
|
594
|
-
// Pipe (72-79)
|
|
595
|
-
'Piccolo', 'Flute', 'Recorder', 'Pan Flute',
|
|
596
|
-
'Blown Bottle', 'Shakuhachi', 'Whistle', 'Ocarina',
|
|
597
|
-
|
|
598
|
-
// Synth lead (80-87)
|
|
599
|
-
'Lead 1 (square)', 'Lead 2 (sawtooth)', 'Lead 3 (calliope)', 'Lead 4 (chiff)',
|
|
600
|
-
'Lead 5 (charang)', 'Lead 6 (voice)', 'Lead 7 (fifths)', 'Lead 8 (bass + lead)',
|
|
601
|
-
|
|
602
|
-
// Synth pad (88-95)
|
|
603
|
-
'Pad 1 (new age)', 'Pad 2 (warm)', 'Pad 3 (polysynth)', 'Pad 4 (choir)',
|
|
604
|
-
'Pad 5 (bowed)', 'Pad 6 (metallic)', 'Pad 7 (halo)', 'Pad 8 (sweep)',
|
|
605
|
-
|
|
606
|
-
// Synth effects (96-103)
|
|
607
|
-
'FX 1 (rain)', 'FX 2 (soundtrack)', 'FX 3 (crystal)', 'FX 4 (atmosphere)',
|
|
608
|
-
'FX 5 (brightness)', 'FX 6 (goblins)', 'FX 7 (echoes)', 'FX 8 (sci-fi)',
|
|
609
|
-
|
|
610
|
-
// Ethnic (104-111)
|
|
611
|
-
'Sitar', 'Banjo', 'Shamisen', 'Koto',
|
|
612
|
-
'Kalimba', 'Bag pipe', 'Fiddle', 'Shanai',
|
|
613
|
-
|
|
614
|
-
// Percussive (112-119)
|
|
615
|
-
'Tinkle Bell', 'Agogo', 'Steel Drums', 'Woodblock',
|
|
616
|
-
'Taiko Drum', 'Melodic Tom', 'Synth Drum', 'Reverse Cymbal',
|
|
617
|
-
|
|
618
|
-
// Sound effects (120-127)
|
|
619
|
-
'Guitar Fret Noise', 'Breath Noise', 'Seashore', 'Bird Tweet',
|
|
620
|
-
'Telephone Ring', 'Helicopter', 'Applause', 'Gunshot'
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
return programNames[programNumber] || `Program ${programNumber}`;
|
|
565
|
+
|
|
566
|
+
return MidiInstrumentNames[programNumber] || `Program ${programNumber}`;
|
|
624
567
|
}
|
|
625
568
|
|
|
626
569
|
/**
|
package/src/lib/beat-mapper.js
CHANGED
|
@@ -18,7 +18,6 @@ class BeatMapper {
|
|
|
18
18
|
try {
|
|
19
19
|
// Step 1: Generate the bar order from sections and order
|
|
20
20
|
this.barOrder = this.generateBarOrder(structureMetadata.sections, structureMetadata.order);
|
|
21
|
-
|
|
22
21
|
// Step 2: Generate beat table from bar order and MIDI bar structure
|
|
23
22
|
this.beatTable = this.generateBeatTable(this.barOrder, parsedMidiData.barStructure);
|
|
24
23
|
|
|
@@ -85,95 +84,13 @@ class BeatMapper {
|
|
|
85
84
|
*/
|
|
86
85
|
generateBeatTable(barOrder, barStructure) {
|
|
87
86
|
const beatTable = [];
|
|
88
|
-
|
|
89
|
-
// Detect if we need the complex BEATMAPPING.md algorithm
|
|
90
|
-
const needsComplexAlgorithm = this._needsComplexAlgorithm(barOrder, barStructure);
|
|
91
|
-
|
|
92
|
-
if (needsComplexAlgorithm) {
|
|
93
|
-
return this._generateBeatTableComplex(barOrder, barStructure);
|
|
94
|
-
} else {
|
|
95
|
-
return this._generateBeatTableSimple(barOrder, barStructure);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Determine if we need the complex barBeats tracking algorithm
|
|
101
|
-
* @private
|
|
102
|
-
*/
|
|
103
|
-
_needsComplexAlgorithm(barOrder, barStructure) {
|
|
104
|
-
// Check for scenarios that require barBeats tracking:
|
|
105
|
-
// 1. Bar numbers repeat (same bar appears multiple times)
|
|
106
|
-
// 2. MIDI structure doesn't match bar order length (mismatches) - BUT not for simple offset cases
|
|
107
|
-
|
|
108
|
-
const barNumbers = barOrder.map(b => b.barNumber);
|
|
109
|
-
const uniqueBars = new Set(barNumbers);
|
|
110
|
-
const hasRepeatedBars = uniqueBars.size < barNumbers.length;
|
|
111
|
-
|
|
112
|
-
// Only treat as mismatch if it's a significant difference (not just pickup offset)
|
|
113
|
-
const significantMismatch = Math.abs(barOrder.length - barStructure.length) > 1;
|
|
114
|
-
|
|
115
|
-
return hasRepeatedBars || significantMismatch;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Simple direct mapping for basic cases
|
|
120
|
-
* @private
|
|
121
|
-
*/
|
|
122
|
-
_generateBeatTableSimple(barOrder, barStructure) {
|
|
123
|
-
const beatTable = [];
|
|
124
|
-
|
|
125
|
-
// Handle both direct mapping and sequential mapping
|
|
126
|
-
const hasPickupBar = barStructure[0] && barStructure[0].sig && barStructure[0].sig[0] === 1;
|
|
127
|
-
const startsWithBarZero = barOrder.length > 0 && barOrder[0].barNumber === 0;
|
|
128
|
-
const isDirectMapping = hasPickupBar && startsWithBarZero;
|
|
129
|
-
|
|
130
|
-
for (let i = 0; i < barOrder.length; i++) {
|
|
131
|
-
const barInfo = barOrder[i];
|
|
132
|
-
let midiBar;
|
|
133
|
-
|
|
134
|
-
if (isDirectMapping) {
|
|
135
|
-
// Direct mapping: bar number = MIDI index
|
|
136
|
-
midiBar = barStructure[barInfo.barNumber];
|
|
137
|
-
} else {
|
|
138
|
-
// Sequential mapping: i-th score bar = i-th MIDI bar
|
|
139
|
-
// If there's a pickup bar and we're not starting from bar 0, offset by 1
|
|
140
|
-
const offset = (hasPickupBar && !startsWithBarZero) ? 1 : 0;
|
|
141
|
-
midiBar = barStructure[i + offset];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!midiBar) {
|
|
145
|
-
throw new Error(`No MIDI bar data for bar ${barInfo.barNumber}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this._generateBeatsForBar(beatTable, barInfo, midiBar, midiBar.sig[0]);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return beatTable;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Complex algorithm from BEATMAPPING.md for handling mismatches and repeats
|
|
156
|
-
* @private
|
|
157
|
-
*/
|
|
158
|
-
_generateBeatTableComplex(barOrder, barStructure) {
|
|
159
|
-
const beatTable = [];
|
|
160
87
|
const barBeats = {}; // Track beat count for each bar number (step 3)
|
|
161
88
|
let midiBarI = 0; // Index into MIDI bar array (step 1)
|
|
162
89
|
let scoreBarI = 0; // Index into score bar order array (step 1)
|
|
163
90
|
|
|
164
91
|
// Create a working copy of barStructure to allow modifications
|
|
165
92
|
const workingBarStructure = [...barStructure];
|
|
166
|
-
|
|
167
|
-
// Special handling: if we have a pickup bar but don't start from bar 0,
|
|
168
|
-
// we need to find the correct starting MIDI bar
|
|
169
|
-
const hasPickupBar = workingBarStructure[0] && workingBarStructure[0].sig && workingBarStructure[0].sig[0] === 1;
|
|
170
|
-
const firstBarNumber = barOrder[0]?.barNumber || 0;
|
|
171
|
-
|
|
172
|
-
if (hasPickupBar && firstBarNumber > 0) {
|
|
173
|
-
// Skip to the MIDI bar that corresponds to the first requested bar
|
|
174
|
-
midiBarI = firstBarNumber;
|
|
175
|
-
}
|
|
176
|
-
|
|
93
|
+
|
|
177
94
|
// Algorithm from BEATMAPPING.md steps 1-10
|
|
178
95
|
while (scoreBarI < barOrder.length && midiBarI < workingBarStructure.length) {
|
|
179
96
|
const barInfo = barOrder[scoreBarI];
|
package/src/lib/midi-player.js
CHANGED
|
@@ -29,11 +29,11 @@ export default class MidiPlayer {
|
|
|
29
29
|
/**
|
|
30
30
|
* Create a new MidiPlayer instance
|
|
31
31
|
* @param {AudioEngine} audioEngine - Initialized audio engine instance
|
|
32
|
-
* @param {Object} instrumentMap - Mapping of part names to instrument configurations
|
|
33
32
|
* @param {Object} parsedMidiData - Output from MidiParser
|
|
33
|
+
* @param {Object} instrumentMap - Mapping of part names to instrument configurations
|
|
34
34
|
* @param {Object} [structureMetadata] - Optional score structure for beat mapping
|
|
35
35
|
*/
|
|
36
|
-
constructor(audioEngine,
|
|
36
|
+
constructor(audioEngine, parsedMidiData, instrumentMap = null, structureMetadata = null) {
|
|
37
37
|
// Validate required parameters
|
|
38
38
|
if (!audioEngine || !audioEngine.isInitialized) {
|
|
39
39
|
throw new Error('Initialized AudioEngine is required');
|
|
@@ -594,40 +594,11 @@ export default class MidiPlayer {
|
|
|
594
594
|
* @private
|
|
595
595
|
*/
|
|
596
596
|
_createDefaultStructureMetadata() {
|
|
597
|
-
const barStructure = this.parsedData.barStructure
|
|
598
|
-
|
|
599
|
-
// Determine number of bars from barStructure (index 0 is null/unused)
|
|
600
|
-
let numBars = 1; // Default fallback
|
|
601
|
-
if (barStructure.length > 1) {
|
|
602
|
-
numBars = barStructure.length;
|
|
603
|
-
} else {
|
|
604
|
-
// No barStructure exists - we need to create default barStructure
|
|
605
|
-
// Estimate from duration using default bar timing
|
|
606
|
-
const defaultTempo = 120;
|
|
607
|
-
const defaultBeatsPerBar = 4;
|
|
608
|
-
const secondsPerBeat = 60 / defaultTempo;
|
|
609
|
-
const secondsPerBar = secondsPerBeat * defaultBeatsPerBar;
|
|
610
|
-
|
|
611
|
-
if (this._totalDuration > 0) {
|
|
612
|
-
numBars = Math.max(1, Math.ceil(this._totalDuration / secondsPerBar));
|
|
613
|
-
} else {
|
|
614
|
-
numBars = 1; // Fallback for zero duration
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Create default barStructure for BeatMapper to use
|
|
618
|
-
this.parsedData.barStructure = [null]; // Index 0 is unused
|
|
619
|
-
for (let i = 1; i <= numBars; i++) {
|
|
620
|
-
const barStartTime = (i - 1) * 2; // Each bar is 2 seconds (4 beats * 0.5s/beat at 120 BPM)
|
|
621
|
-
this.parsedData.barStructure[i] = {
|
|
622
|
-
sig: [4, 4],
|
|
623
|
-
beats: [barStartTime, barStartTime + 0.5, barStartTime + 1, barStartTime + 1.5]
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
}
|
|
597
|
+
const barStructure = this.parsedData.barStructure;
|
|
627
598
|
|
|
628
599
|
return {
|
|
629
600
|
sections: [
|
|
630
|
-
{ from: 1, to:
|
|
601
|
+
{ from: 1, to: barStructure.length } // Single section covering all bars
|
|
631
602
|
],
|
|
632
603
|
order: [
|
|
633
604
|
{ section: 0 } // Play section 0 once (no repeats)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import mitt from 'mitt';
|
|
2
|
-
import stick4csUrl from '../assets/stick-4cs.mp3';
|
|
3
|
-
import stick4dUrl from '../assets/stick-4d.mp3';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* PlaybackManager - High-level orchestration for MIDI playback, metronome, and lead-in
|
|
@@ -69,8 +67,7 @@ export default class PlaybackManager {
|
|
|
69
67
|
// Beat-based metronome scheduling
|
|
70
68
|
this.nextBeatIndex = 0; // Index of next beat to play
|
|
71
69
|
|
|
72
|
-
//
|
|
73
|
-
this._setupMetronomeAudio();
|
|
70
|
+
// Event setup
|
|
74
71
|
this._setupEventDelegation();
|
|
75
72
|
|
|
76
73
|
// Input validation
|
|
@@ -459,17 +456,6 @@ export default class PlaybackManager {
|
|
|
459
456
|
*/
|
|
460
457
|
allSoundsOff() {
|
|
461
458
|
this.midiPlayer.allSoundsOff();
|
|
462
|
-
|
|
463
|
-
// Stop metronome sounds
|
|
464
|
-
if (this.currentMetronomeSource) {
|
|
465
|
-
try {
|
|
466
|
-
this.currentMetronomeSource.disconnect();
|
|
467
|
-
this.currentMetronomeSource.stop();
|
|
468
|
-
this.currentMetronomeSource = null;
|
|
469
|
-
} catch (error) {
|
|
470
|
-
// Ignore errors from already stopped sources
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
459
|
}
|
|
474
460
|
|
|
475
461
|
// ========================================
|
|
@@ -510,57 +496,7 @@ export default class PlaybackManager {
|
|
|
510
496
|
// PRIVATE IMPLEMENTATION METHODS
|
|
511
497
|
// ========================================
|
|
512
498
|
|
|
513
|
-
/**
|
|
514
|
-
* Set up metronome audio using direct audio files
|
|
515
|
-
* @private
|
|
516
|
-
*/
|
|
517
|
-
async _setupMetronomeAudio() {
|
|
518
|
-
try {
|
|
519
|
-
// Create output gain node for mixer control
|
|
520
|
-
this.metronomeOutput = this.audioEngine.audioContext.createGain();
|
|
521
|
-
this.metronomeOutput.gain.value = this.metronomeConfig.volume;
|
|
522
|
-
|
|
523
|
-
// Load metronome sound files
|
|
524
|
-
this.metronomeBuffers = null; // Will be loaded lazily
|
|
525
|
-
this.currentMetronomeSource = null;
|
|
526
|
-
|
|
527
|
-
} catch (error) {
|
|
528
|
-
console.error('Failed to setup metronome audio:', error);
|
|
529
|
-
throw new Error('Failed to initialize metronome audio: ' + error.message);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Ensure metronome audio buffers are loaded
|
|
535
|
-
* @private
|
|
536
|
-
*/
|
|
537
|
-
async _ensureMetronomeBuffersLoaded() {
|
|
538
|
-
if (this.metronomeBuffers) {
|
|
539
|
-
return; // Already loaded
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
// Check if we're in a test environment or asset URLs are undefined
|
|
544
|
-
if (typeof window === 'undefined' || typeof fetch === 'undefined' || !stick4csUrl || !stick4dUrl) {
|
|
545
|
-
// Mock buffers for testing environment
|
|
546
|
-
console.log('Test environment detected, using mock metronome buffers');
|
|
547
|
-
this.metronomeBuffers = [null, null];
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Load both sound files from bundled assets
|
|
552
|
-
this.metronomeBuffers = await Promise.all([
|
|
553
|
-
fetch(stick4csUrl).then(r => r.arrayBuffer()).then(b => this.audioEngine.audioContext.decodeAudioData(b)),
|
|
554
|
-
fetch(stick4dUrl).then(r => r.arrayBuffer()).then(b => this.audioEngine.audioContext.decodeAudioData(b))
|
|
555
|
-
]);
|
|
556
499
|
|
|
557
|
-
} catch (error) {
|
|
558
|
-
console.error('Failed to load metronome sound buffers:', error);
|
|
559
|
-
// Set empty buffers to prevent repeated attempts
|
|
560
|
-
this.metronomeBuffers = [null, null];
|
|
561
|
-
throw error;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
500
|
|
|
565
501
|
/**
|
|
566
502
|
* Set up event delegation from MidiPlayer
|
|
@@ -665,44 +601,21 @@ export default class PlaybackManager {
|
|
|
665
601
|
const startTime = this.frozenTime;
|
|
666
602
|
const leadInBars = this.leadInConfig.bars;
|
|
667
603
|
|
|
668
|
-
if (!beats || beats.length === 0) {
|
|
669
|
-
// Fallback to simple calculation
|
|
670
|
-
const defaultPeriod = 0.5;
|
|
671
|
-
const defaultBeatsPerBar = 4;
|
|
672
|
-
const totalBeats = leadInBars * defaultBeatsPerBar ;
|
|
673
|
-
|
|
674
|
-
return {
|
|
675
|
-
totalBeats,
|
|
676
|
-
duration: totalBeats * defaultPeriod,
|
|
677
|
-
beatSequence: this._generateBeatSequence(totalBeats, defaultPeriod, defaultBeatsPerBar),
|
|
678
|
-
beatsPerBar: defaultBeatsPerBar
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
604
|
// Find the last beat at or before start time
|
|
683
605
|
// And, the time between this beat and the next, or the one before
|
|
684
|
-
let
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
606
|
+
let startIndex = beats.length-1, beatPeriod = 0.5;
|
|
607
|
+
while (beats[startIndex].time > startTime) startIndex--;
|
|
608
|
+
const startBeat = beats[startIndex];
|
|
609
|
+
const nextBeat = beats[startIndex + 1];
|
|
610
|
+
if (nextBeat) beatPeriod = nextBeat.time - startBeat.time;
|
|
611
|
+
else if (startIndex>0) beatPeriod = startBeat.time - beats[startIndex-1].time;
|
|
693
612
|
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
const barStructure = this.midiPlayer.parsedData.barStructure || [];
|
|
697
|
-
const currentBarData = barStructure[barIndex] || { sig: [4, 4], bpm: 120 };
|
|
698
|
-
|
|
699
|
-
const beatsPerBar = startBeat.timeSig;
|
|
700
|
-
const tempo = Array.isArray(currentBarData.bpm) ? currentBarData.bpm[0] : currentBarData.bpm;
|
|
701
|
-
|
|
613
|
+
const isPickup = startBeat.timeSig === 1
|
|
614
|
+
const beatsPerBar = isPickup && nextBeat? nextBeat.timeSig : startBeat.timeSig;
|
|
702
615
|
|
|
703
616
|
// Calculate beats needed: full lead-in bars + beats from current position to reach start position
|
|
704
617
|
// If we're at beat 2 of 4, we need 1 more beat to complete current bar (just the difference to next downbeat)
|
|
705
|
-
const beatsToStart = startBeat.beat > 1 ? startBeat.beat - 1 : 0;
|
|
618
|
+
const beatsToStart = isPickup ? beatsPerBar - 1 : startBeat.beat > 1 ? startBeat.beat - 1 : 0;
|
|
706
619
|
const totalBeats = leadInBars * beatsPerBar + beatsToStart;
|
|
707
620
|
|
|
708
621
|
return {
|
|
@@ -966,7 +879,7 @@ export default class PlaybackManager {
|
|
|
966
879
|
* @private
|
|
967
880
|
*/
|
|
968
881
|
_scheduleMetronomeTicksAt(startTime) {
|
|
969
|
-
if (!this.metronomeConfig.enabled
|
|
882
|
+
if (!this.metronomeConfig.enabled) {
|
|
970
883
|
return;
|
|
971
884
|
}
|
|
972
885
|
|
|
@@ -1004,7 +917,7 @@ export default class PlaybackManager {
|
|
|
1004
917
|
* @private
|
|
1005
918
|
*/
|
|
1006
919
|
_scheduleMetronomeTicks() {
|
|
1007
|
-
if (!this.metronomeConfig.enabled
|
|
920
|
+
if (!this.metronomeConfig.enabled) {
|
|
1008
921
|
return;
|
|
1009
922
|
}
|
|
1010
923
|
|
|
@@ -1202,21 +1115,6 @@ export default class PlaybackManager {
|
|
|
1202
1115
|
this._stopMetronome();
|
|
1203
1116
|
this._stopTimeUpdateLoop();
|
|
1204
1117
|
|
|
1205
|
-
// Stop and cleanup metronome audio
|
|
1206
|
-
if (this.currentMetronomeSource) {
|
|
1207
|
-
try {
|
|
1208
|
-
this.currentMetronomeSource.disconnect();
|
|
1209
|
-
this.currentMetronomeSource.stop();
|
|
1210
|
-
} catch (error) {
|
|
1211
|
-
// Ignore errors from already stopped sources
|
|
1212
|
-
}
|
|
1213
|
-
this.currentMetronomeSource = null;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Disconnect metronome output
|
|
1217
|
-
if (this.metronomeOutput) {
|
|
1218
|
-
this.metronomeOutput.disconnect();
|
|
1219
|
-
}
|
|
1220
1118
|
|
|
1221
1119
|
// Clear event bus
|
|
1222
1120
|
this.eventBus.all.clear();
|