diff --git a/packages/alphatab/src/synth/AlphaSynth.ts b/packages/alphatab/src/synth/AlphaSynth.ts index 7746660e0..e20f07332 100644 --- a/packages/alphatab/src/synth/AlphaSynth.ts +++ b/packages/alphatab/src/synth/AlphaSynth.ts @@ -325,7 +325,7 @@ export class AlphaSynthBase implements IAlphaSynth { if (this._countInVolume > 0) { Logger.debug('AlphaSynth', 'Starting countin'); this.sequencer.startCountIn(); - this.synthesizer.setupMetronomeChannel(this._countInVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this._countInVolume); this.updateTimePosition(0, true); } @@ -340,7 +340,7 @@ export class AlphaSynthBase implements IAlphaSynth { } Logger.debug('AlphaSynth', 'Starting playback'); - this.synthesizer.setupMetronomeChannel(this.metronomeVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume); this._synthStopping = false; this.state = PlayerState.Playing; (this.stateChanged as EventEmitterOfT).trigger( @@ -442,7 +442,7 @@ export class AlphaSynthBase implements IAlphaSynth { private _checkReadyForPlayback(): void { if (this.isReadyForPlayback) { - this.synthesizer.setupMetronomeChannel(this.metronomeVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume); const programs = this.sequencer.instrumentPrograms; const percussionKeys = this.sequencer.percussionKeys; let append = false; @@ -875,7 +875,7 @@ export class AlphaSynthAudioExporter implements IAlphaSynthAudioExporter { private _generatedAudioEndTime: number = 0; public setup() { - this._synth.setupMetronomeChannel(this._synth.metronomeVolume); + this._synth.setupMetronomeChannel(this._sequencer.metronomeChannel, this._synth.metronomeVolume); const syncPoints = this._sequencer.currentSyncPoints; const alphaTabEndTime = this._sequencer.currentEndTime; diff --git a/packages/alphatab/src/synth/BackingTrackPlayer.ts b/packages/alphatab/src/synth/BackingTrackPlayer.ts index 730250a9c..fb813fe8f 100644 --- a/packages/alphatab/src/synth/BackingTrackPlayer.ts +++ b/packages/alphatab/src/synth/BackingTrackPlayer.ts @@ -81,7 +81,7 @@ class BackingTrackAudioSynthesizer implements IAudioSampleSynthesizer { // not supported, ignore } - public setupMetronomeChannel(_metronomeVolume: number): void { + public setupMetronomeChannel(_metronomeChannel: number, _metronomeVolume: number): void { // not supported, ignore } diff --git a/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts b/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts index 19d41847a..d8d1ce405 100644 --- a/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts +++ b/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts @@ -68,9 +68,10 @@ export interface IAudioSampleSynthesizer { /** * Configures the channel used to generate metronome sounds. + * @param metronomeChannel The midi hannel to use for playing the metronome (to avoid overlaps with instruments). * @param metronomeVolume The volume for the channel. */ - setupMetronomeChannel(metronomeVolume: number): void; + setupMetronomeChannel(metronomeChannel: number, metronomeVolume: number): void; /** * Synthesizes the given number of samples without producing an output (e.g. on seeking) diff --git a/packages/alphatab/src/synth/MidiFileSequencer.ts b/packages/alphatab/src/synth/MidiFileSequencer.ts index 1feac5b3e..f3073d26a 100644 --- a/packages/alphatab/src/synth/MidiFileSequencer.ts +++ b/packages/alphatab/src/synth/MidiFileSequencer.ts @@ -51,6 +51,7 @@ class MidiSequencerState { public endTime: number = 0; public currentTempo: number = 0; public syncPointTempo: number = 0; + public metronomeChannel: number = SynthConstants.DefaultChannelCount - 1; } /** @@ -65,6 +66,10 @@ export class MidiFileSequencer { private _oneTimeState: MidiSequencerState | null = null; private _countInState: MidiSequencerState | null = null; + public get metronomeChannel() { + return this._mainState.metronomeChannel; + } + public get isPlayingMain(): boolean { return this._currentState === this._mainState; } @@ -171,7 +176,7 @@ export class MidiFileSequencer { const metronomeVolume: number = this._synthesizer.metronomeVolume; this._synthesizer.noteOffAll(true); this._synthesizer.resetSoft(); - this._synthesizer.setupMetronomeChannel(metronomeVolume); + this._synthesizer.setupMetronomeChannel(this.metronomeChannel, metronomeVolume); } this._mainSilentProcess(timePosition); } @@ -239,6 +244,8 @@ export class MidiFileSequencer { let metronomeTick: number = midiFile.tickShift; // shift metronome to content let metronomeTime: number = 0.0; + let maxChannel = 0; + let previousTick: number = 0; for (const mEvent of midiFile.events) { const synthData: SynthEvent = new SynthEvent(state.synthData.length, mEvent); @@ -287,6 +294,9 @@ export class MidiFileSequencer { if (!state.firstProgramEventPerChannel.has(channel)) { state.firstProgramEventPerChannel.set(channel, synthData); } + if (channel > maxChannel) { + maxChannel = channel; + } const isPercussion = channel === SynthConstants.PercussionChannel; if (!isPercussion) { this.instrumentPrograms.add(programChange.program); @@ -297,6 +307,9 @@ export class MidiFileSequencer { if (isPercussion) { this.percussionKeys.add(noteOn.noteKey); } + if (noteOn.channel > maxChannel) { + maxChannel = noteOn.channel; + } } } @@ -314,6 +327,7 @@ export class MidiFileSequencer { }); state.endTime = absTime; state.endTick = absTick; + state.metronomeChannel = maxChannel + 1; return state; } diff --git a/packages/alphatab/src/synth/SynthConstants.ts b/packages/alphatab/src/synth/SynthConstants.ts index 366a259aa..b5082f0db 100644 --- a/packages/alphatab/src/synth/SynthConstants.ts +++ b/packages/alphatab/src/synth/SynthConstants.ts @@ -8,7 +8,6 @@ */ export class SynthConstants { public static readonly DefaultChannelCount: number = 16 + 1; - public static readonly MetronomeChannel: number = SynthConstants.DefaultChannelCount - 1; public static readonly MetronomeKey: number = 33; public static readonly AudioChannels: number = 2; public static readonly MinVolume: number = 0; diff --git a/packages/alphatab/src/synth/synthesis/TinySoundFont.ts b/packages/alphatab/src/synth/synthesis/TinySoundFont.ts index 49e107d05..b70b98a72 100644 --- a/packages/alphatab/src/synth/synthesis/TinySoundFont.ts +++ b/packages/alphatab/src/synth/synthesis/TinySoundFont.ts @@ -63,6 +63,7 @@ export class TinySoundFont implements IAudioSampleSynthesizer { public currentTempo: number = 0; public timeSignatureNumerator: number = 0; public timeSignatureDenominator: number = 0; + private _metronomeChannel: number = SynthConstants.DefaultChannelCount - 1; public constructor(sampleRate: number) { this.outSampleRate = sampleRate; @@ -188,8 +189,8 @@ export class TinySoundFont implements IAudioSampleSynthesizer { while (!this._midiEventQueue.isEmpty) { const m: SynthEvent = this._midiEventQueue.dequeue()!; if (m.isMetronome && this.metronomeVolume > 0) { - this.channelNoteOff(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey); - this.channelNoteOn(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey, 95 / 127); + this.channelNoteOff(this._metronomeChannel, SynthConstants.MetronomeKey); + this.channelNoteOn(this._metronomeChannel, SynthConstants.MetronomeKey, 95 / 127); } else if (m.event) { this.processMidiMessage(m.event); } @@ -204,7 +205,7 @@ export class TinySoundFont implements IAudioSampleSynthesizer { // exception. metronome is implicitly added in solo const isChannelMuted: boolean = this._mutedChannels.has(channel) || - (anySolo && channel !== SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); + (anySolo && channel !== this._metronomeChannel && !this._soloChannels.has(channel)); if (!buffer) { voice.kill(); @@ -261,18 +262,19 @@ export class TinySoundFont implements IAudioSampleSynthesizer { } public get metronomeVolume(): number { - return this.channelGetMixVolume(SynthConstants.MetronomeChannel); + return this.channelGetMixVolume(this._metronomeChannel); } public set metronomeVolume(value: number) { - this.setupMetronomeChannel(value); + this.setupMetronomeChannel(this._metronomeChannel, value); } - public setupMetronomeChannel(volume: number): void { - this.channelSetMixVolume(SynthConstants.MetronomeChannel, volume); + public setupMetronomeChannel(channel:number, volume: number): void { + this._metronomeChannel = channel; + this.channelSetMixVolume(channel, volume); if (volume > 0) { - this.channelSetVolume(SynthConstants.MetronomeChannel, 1); - this.channelSetPresetNumber(SynthConstants.MetronomeChannel, 0, true); + this.channelSetVolume(channel, 1); + this.channelSetPresetNumber(channel, 0, true); } } diff --git a/packages/alphatab/test/audio/SyncPoint.test.ts b/packages/alphatab/test/audio/SyncPoint.test.ts index 81cb80cce..22f657cb9 100644 --- a/packages/alphatab/test/audio/SyncPoint.test.ts +++ b/packages/alphatab/test/audio/SyncPoint.test.ts @@ -1,4 +1,9 @@ -import { type IEventEmitterOfT, type IEventEmitter, EventEmitterOfT, EventEmitter } from '@coderline/alphatab/EventEmitter'; +import { + type IEventEmitterOfT, + type IEventEmitter, + EventEmitterOfT, + EventEmitter +} from '@coderline/alphatab/EventEmitter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; import { MidiFile } from '@coderline/alphatab/midi/MidiFile'; @@ -464,7 +469,7 @@ class EmptyAudioSynthesizer implements IAudioSampleSynthesizer { _percussionKeys: Set, _append: boolean ): void {} - public setupMetronomeChannel(_metronomeVolume: number): void {} + public setupMetronomeChannel(_metronomeChannel: number, _metronomeVolume: number): void {} public synthesizeSilent(_sampleCount: number): void {} public dispatchEvent(_synthEvent: SynthEvent): void {} public synthesize(_buffer: Float32Array, _bufferPos: number, _ampleCount: number): SynthEvent[] {