fix: missing count-in when recording with loop mode on
This commit is contained in:
+13
-5
@@ -10,6 +10,10 @@ import { KGRegion } from './region/KGRegion';
|
|||||||
import { generateUniqueId } from '../util/miscUtil';
|
import { generateUniqueId } from '../util/miscUtil';
|
||||||
import { KGCommand, KGCommandHistory } from './commands';
|
import { KGCommand, KGCommandHistory } from './commands';
|
||||||
|
|
||||||
|
interface PlaybackStartOptions {
|
||||||
|
preserveLoopPreroll?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KGCore - Main application class for the DAW
|
* KGCore - Main application class for the DAW
|
||||||
* Implements the singleton pattern for global access
|
* Implements the singleton pattern for global access
|
||||||
@@ -231,7 +235,7 @@ export class KGCore {
|
|||||||
return this.isPlaying;
|
return this.isPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async preparePlay(): Promise<void> {
|
public async preparePlay(options?: PlaybackStartOptions): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const audioInterface = KGAudioInterface.instance();
|
const audioInterface = KGAudioInterface.instance();
|
||||||
|
|
||||||
@@ -239,7 +243,9 @@ export class KGCore {
|
|||||||
await audioInterface.startAudioContext();
|
await audioInterface.startAudioContext();
|
||||||
|
|
||||||
// Prepare playback with current project and playhead position
|
// Prepare playback with current project and playhead position
|
||||||
audioInterface.preparePlayback(this.currentProject, this.playheadPosition);
|
audioInterface.preparePlayback(this.currentProject, this.playheadPosition, {
|
||||||
|
allowStartBeforeLoopStart: options?.preserveLoopPreroll ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
// Sync BPM and transport settings
|
// Sync BPM and transport settings
|
||||||
audioInterface.setBpm(this.currentProject.getBpm());
|
audioInterface.setBpm(this.currentProject.getBpm());
|
||||||
@@ -301,7 +307,7 @@ export class KGCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// High-level playback control methods
|
// High-level playback control methods
|
||||||
public async startPlaying(): Promise<void> {
|
public async startPlaying(options?: PlaybackStartOptions): Promise<void> {
|
||||||
// Handle loop mode initialization
|
// Handle loop mode initialization
|
||||||
if (this.currentProject.getIsLooping()) {
|
if (this.currentProject.getIsLooping()) {
|
||||||
const [startBar, endBar] = this.currentProject.getLoopingRange();
|
const [startBar, endBar] = this.currentProject.getLoopingRange();
|
||||||
@@ -319,11 +325,13 @@ export class KGCore {
|
|||||||
const updatedRange = this.currentProject.getLoopingRange();
|
const updatedRange = this.currentProject.getLoopingRange();
|
||||||
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
||||||
const loopStartBeats = updatedRange[0] * beatsPerBar;
|
const loopStartBeats = updatedRange[0] * beatsPerBar;
|
||||||
this.setPlayheadPosition(loopStartBeats);
|
if (!options?.preserveLoopPreroll) {
|
||||||
|
this.setPlayheadPosition(loopStartBeats);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare playback first
|
// Prepare playback first
|
||||||
await this.preparePlay();
|
await this.preparePlay(options);
|
||||||
|
|
||||||
// Start playing (non-blocking)
|
// Start playing (non-blocking)
|
||||||
this.play(); // Don't await this
|
this.play(); // Don't await this
|
||||||
|
|||||||
@@ -97,4 +97,19 @@ describe('KGAudioInterface preroll playback', () => {
|
|||||||
expect(MockTransport.stop).toHaveBeenCalledTimes(1)
|
expect(MockTransport.stop).toHaveBeenCalledTimes(1)
|
||||||
expect(audio.getTransportPosition()).toBe(0)
|
expect(audio.getTransportPosition()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('allows a first-pass start before the loop start when explicitly requested', () => {
|
||||||
|
const project = createMockProject({
|
||||||
|
bpm: 120,
|
||||||
|
timeSignature: { numerator: 4, denominator: 4 },
|
||||||
|
tracks: [],
|
||||||
|
})
|
||||||
|
project.setIsLooping(true)
|
||||||
|
project.setLoopingRange([4, 7])
|
||||||
|
|
||||||
|
const audio = KGAudioInterface.instance()
|
||||||
|
audio.preparePlayback(project, 12, { allowStartBeforeLoopStart: true })
|
||||||
|
|
||||||
|
expect(MockTransport.position).toBe(6)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import { KGCore } from '../KGCore';
|
|||||||
import { ConfigManager } from '../config/ConfigManager';
|
import { ConfigManager } from '../config/ConfigManager';
|
||||||
import { KGMetronome } from './KGMetronome';
|
import { KGMetronome } from './KGMetronome';
|
||||||
|
|
||||||
|
interface PreparePlaybackOptions {
|
||||||
|
allowStartBeforeLoopStart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KGAudioInterface - Audio engine interface for the DAW
|
* KGAudioInterface - Audio engine interface for the DAW
|
||||||
* Implements the singleton pattern for global audio management
|
* Implements the singleton pattern for global audio management
|
||||||
@@ -391,7 +395,7 @@ export class KGAudioInterface {
|
|||||||
/**
|
/**
|
||||||
* Prepare playback by scheduling all MIDI events
|
* Prepare playback by scheduling all MIDI events
|
||||||
*/
|
*/
|
||||||
public preparePlayback(project: KGProject, startPosition: number): void {
|
public preparePlayback(project: KGProject, startPosition: number, options?: PreparePlaybackOptions): void {
|
||||||
// Clear any existing scheduled events
|
// Clear any existing scheduled events
|
||||||
this.clearScheduledEvents();
|
this.clearScheduledEvents();
|
||||||
this.clearDelayedTransportStart();
|
this.clearDelayedTransportStart();
|
||||||
@@ -437,7 +441,7 @@ export class KGAudioInterface {
|
|||||||
console.log(`Loop mode enabled: bars [${startBar}, ${endBar}], beats [${scheduleStartBeat}, ${scheduleEndBeat}]`);
|
console.log(`Loop mode enabled: bars [${startBar}, ${endBar}], beats [${scheduleStartBeat}, ${scheduleEndBeat}]`);
|
||||||
|
|
||||||
// Adjust start position to loop start if before loop range
|
// Adjust start position to loop start if before loop range
|
||||||
if (startPosition < scheduleStartBeat) {
|
if (startPosition < scheduleStartBeat && !options?.allowStartBeforeLoopStart) {
|
||||||
startPosition = scheduleStartBeat;
|
startPosition = scheduleStartBeat;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
startRecording: async () => {
|
startRecording: async () => {
|
||||||
const { activeRegionId, timeSignature, playheadPosition, startPlaying, setPlayheadPosition } = get();
|
const { activeRegionId, timeSignature, playheadPosition, setPlayheadPosition } = get();
|
||||||
|
|
||||||
const project = KGCore.instance().getCurrentProject();
|
const project = KGCore.instance().getCurrentProject();
|
||||||
let targetRegion: KGMidiRegion | null = null;
|
let targetRegion: KGMidiRegion | null = null;
|
||||||
@@ -842,8 +842,18 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setPlayheadPosition(playheadPosition - timeSignature.numerator);
|
const projectLooping = project.getIsLooping();
|
||||||
await startPlaying();
|
const [loopStartBar] = project.getLoopingRange();
|
||||||
|
const loopStartBeat = loopStartBar * timeSignature.numerator;
|
||||||
|
const recordingStartBeat = projectLooping
|
||||||
|
? loopStartBeat - timeSignature.numerator
|
||||||
|
: playheadPosition - timeSignature.numerator;
|
||||||
|
|
||||||
|
setPlayheadPosition(recordingStartBeat);
|
||||||
|
await KGCore.instance().startPlaying({
|
||||||
|
preserveLoopPreroll: projectLooping,
|
||||||
|
});
|
||||||
|
set({ isPlaying: true, autoScrollEnabled: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
stopRecording: async () => {
|
stopRecording: async () => {
|
||||||
@@ -1196,4 +1206,3 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { KGProject } from '../../../core/KGProject'
|
|||||||
import { KGMidiTrack, type InstrumentType } from '../../../core/track/KGMidiTrack'
|
import { KGMidiTrack, type InstrumentType } from '../../../core/track/KGMidiTrack'
|
||||||
import { KGMidiRegion } from '../../../core/region/KGMidiRegion'
|
import { KGMidiRegion } from '../../../core/region/KGMidiRegion'
|
||||||
import { KGMidiNote } from '../../../core/midi/KGMidiNote'
|
import { KGMidiNote } from '../../../core/midi/KGMidiNote'
|
||||||
|
import { KGMidiInput } from '../../../core/midi-input/KGMidiInput'
|
||||||
|
|
||||||
// Import store
|
// Import store
|
||||||
import { useProjectStore } from '../../../stores/projectStore'
|
import { useProjectStore } from '../../../stores/projectStore'
|
||||||
@@ -279,6 +280,41 @@ describe('Project Store Synchronization Integration Tests', () => {
|
|||||||
// Verify audio interface was called
|
// Verify audio interface was called
|
||||||
expect(mockAudioInterface.stopPlayback).toHaveBeenCalled()
|
expect(mockAudioInterface.stopPlayback).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should start looped recording one bar before the loop start on the first pass', async () => {
|
||||||
|
const testTrack = new KGMidiTrack('Recording Track', 0, 'acoustic_grand_piano')
|
||||||
|
const testRegion = new KGMidiRegion('record-region', 'track-0', 0, 'Recording Region', 16, 16)
|
||||||
|
testTrack.addRegion(testRegion)
|
||||||
|
testProject.setTracks([testTrack])
|
||||||
|
testProject.setIsLooping(true)
|
||||||
|
testProject.setLoopingRange([4, 7])
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await useProjectStore.getState().loadProject(testProject)
|
||||||
|
})
|
||||||
|
|
||||||
|
const core = KGCore.instance()
|
||||||
|
const startPlayingSpy = vi.spyOn(core, 'startPlaying').mockResolvedValue(undefined)
|
||||||
|
const recordingCallbacksSpy = vi.spyOn(KGMidiInput.instance(), 'setRecordingCallbacks')
|
||||||
|
const { setActiveRegionId, setPlayheadPosition, startRecording } = useProjectStore.getState()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setActiveRegionId(testRegion.getId())
|
||||||
|
setPlayheadPosition(18)
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await startRecording()
|
||||||
|
})
|
||||||
|
|
||||||
|
const storeState = useProjectStore.getState()
|
||||||
|
expect(storeState.playheadPosition).toBe(12)
|
||||||
|
expect(storeState.recordingOriginalPlayhead).toBe(18)
|
||||||
|
expect(storeState.isRecording).toBe(true)
|
||||||
|
expect(storeState.isPlaying).toBe(true)
|
||||||
|
expect(recordingCallbacksSpy).toHaveBeenCalled()
|
||||||
|
expect(startPlayingSpy).toHaveBeenCalledWith({ preserveLoopPreroll: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Selection State Synchronization', () => {
|
describe('Selection State Synchronization', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user