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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-mixer-engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Audio engine library for audio mixer applications with MIDI parsing, playback, and synthesis",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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('sounds/click-regular.mp3'),
128
- fetch('sounds/click-accent.mp3')
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
- const programNames = [
558
- // Piano family (0-7)
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
  /**
@@ -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];
@@ -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, instrumentMap, parsedMidiData, structureMetadata = null) {
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: numBars } // Single section covering all bars
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
- // Audio setup
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 startBeat = beats[0], beatPeriod = 0.5;
685
- for (let i = beats.length - 1; i >= 0; i--) {
686
- if (beats[i].time <= startTime) {
687
- startBeat = beats[i];
688
- if (i + 1 < beats.length) beatPeriod = beats[i+1].time - startBeat.time;
689
- else if (i>0) beatPeriod = startBeat.time - beats[i-1].time;
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
- // Extract tempo and time signature from bar structure at start position
695
- const barIndex = Math.max(0, startBeat.bar - 1);
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 || !this.metronomeOutput) {
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 || !this.metronomeOutput) {
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();