feat: enhanced the note scheduling logic to support loop mode
pending cases: user disabled the loop mode or updated the loop range during playback.
This commit is contained in:
+61
-17
@@ -270,16 +270,36 @@ export class KGCore {
|
|||||||
|
|
||||||
// High-level playback control methods
|
// High-level playback control methods
|
||||||
public async startPlaying(): Promise<void> {
|
public async startPlaying(): Promise<void> {
|
||||||
|
// Handle loop mode initialization
|
||||||
|
if (this.currentProject.getIsLooping()) {
|
||||||
|
const [startBar, endBar] = this.currentProject.getLoopingRange();
|
||||||
|
|
||||||
|
// Handle [0, 0] case - set to full project
|
||||||
|
if (startBar === 0 && endBar === 0) {
|
||||||
|
const maxBars = this.currentProject.getMaxBars();
|
||||||
|
const { ChangeLoopSettingsCommand } = await import('./commands');
|
||||||
|
this.executeCommand(new ChangeLoopSettingsCommand({
|
||||||
|
loopingRange: [0, maxBars]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move playhead to loop start (use updated range if [0,0] was just set)
|
||||||
|
const updatedRange = this.currentProject.getLoopingRange();
|
||||||
|
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
||||||
|
const loopStartBeats = updatedRange[0] * beatsPerBar;
|
||||||
|
this.setPlayheadPosition(loopStartBeats);
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare playback first
|
// Prepare playback first
|
||||||
await this.preparePlay();
|
await this.preparePlay();
|
||||||
|
|
||||||
// Start playing (non-blocking)
|
// Start playing (non-blocking)
|
||||||
this.play(); // Don't await this
|
this.play(); // Don't await this
|
||||||
|
|
||||||
// Set up the regular playback update timer
|
// Set up the regular playback update timer
|
||||||
this.playbackStartTime = performance.now();
|
this.playbackStartTime = performance.now();
|
||||||
this.playbackStartPosition = this.playheadPosition;
|
this.playbackStartPosition = this.playheadPosition;
|
||||||
|
|
||||||
this.startPlaybackUpdates();
|
this.startPlaybackUpdates();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -336,28 +356,52 @@ export class KGCore {
|
|||||||
|
|
||||||
const bpm = this.currentProject.getBpm();
|
const bpm = this.currentProject.getBpm();
|
||||||
const beatsPerMs = bpm / (60 * 1000);
|
const beatsPerMs = bpm / (60 * 1000);
|
||||||
const newPosition = this.playbackStartPosition + (adjustedElapsedMs * beatsPerMs);
|
let newPosition = this.playbackStartPosition + (adjustedElapsedMs * beatsPerMs);
|
||||||
|
|
||||||
// Stop playback at the end of project (maxBars)
|
// Handle looping or end-of-project
|
||||||
const maxBars = this.currentProject.getMaxBars();
|
|
||||||
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
||||||
const maxBeats = maxBars * beatsPerBar;
|
|
||||||
if (newPosition >= maxBeats) {
|
if (this.currentProject.getIsLooping()) {
|
||||||
// Clamp to max and stop
|
// Loop mode: wrap playhead when it reaches loop end
|
||||||
this.setPlayheadPosition(maxBeats);
|
const [startBar, endBarOriginal] = this.currentProject.getLoopingRange();
|
||||||
// Stop playback (non-blocking)
|
const endBar = (startBar === 0 && endBarOriginal === 0) ? this.currentProject.getMaxBars() : endBarOriginal;
|
||||||
this.stopPlaying();
|
|
||||||
return;
|
const loopStartBeats = startBar * beatsPerBar;
|
||||||
|
const loopEndBeats = (endBar + 1) * beatsPerBar; // +1 because endBar is inclusive
|
||||||
|
const loopLengthBeats = loopEndBeats - loopStartBeats;
|
||||||
|
|
||||||
|
// Wrap playhead position within loop range
|
||||||
|
if (newPosition >= loopEndBeats) {
|
||||||
|
// Calculate how far we've overshot and wrap back
|
||||||
|
const overshot = newPosition - loopEndBeats;
|
||||||
|
newPosition = loopStartBeats + (overshot % loopLengthBeats);
|
||||||
|
|
||||||
|
// Reset timing reference to prevent drift accumulation
|
||||||
|
const newElapsedBeats = newPosition - loopStartBeats;
|
||||||
|
this.playbackStartTime = performance.now() - (newElapsedBeats / beatsPerMs) - playbackDelayMs;
|
||||||
|
this.playbackStartPosition = loopStartBeats;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-looping mode: stop at project end
|
||||||
|
const maxBars = this.currentProject.getMaxBars();
|
||||||
|
const maxBeats = maxBars * beatsPerBar;
|
||||||
|
|
||||||
|
if (newPosition >= maxBeats) {
|
||||||
|
// Clamp to max and stop
|
||||||
|
this.setPlayheadPosition(maxBeats);
|
||||||
|
// Stop playback (non-blocking)
|
||||||
|
this.stopPlaying();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update playhead position
|
// Update playhead position
|
||||||
this.setPlayheadPosition(newPosition);
|
this.setPlayheadPosition(newPosition);
|
||||||
|
|
||||||
// TODO: Future enhancements
|
// TODO: Future enhancements
|
||||||
// - Sync with Tone.Transport position for more accurate timing
|
// - Sync with Tone.Transport position for more accurate timing
|
||||||
// - Handle tempo changes mid-playback
|
// - Handle tempo changes mid-playback
|
||||||
// - Account for latency compensation
|
// - Account for latency compensation
|
||||||
// - Support for loop regions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// selected items
|
// selected items
|
||||||
|
|||||||
@@ -264,9 +264,41 @@ export class KGAudioInterface {
|
|||||||
Tone.Transport.bpm.value = project.getBpm();
|
Tone.Transport.bpm.value = project.getBpm();
|
||||||
const timeSignature = project.getTimeSignature();
|
const timeSignature = project.getTimeSignature();
|
||||||
Tone.Transport.timeSignature = [timeSignature.numerator, timeSignature.denominator];
|
Tone.Transport.timeSignature = [timeSignature.numerator, timeSignature.denominator];
|
||||||
|
|
||||||
console.log(`Setting Tone.js BPM to ${project.getBpm()}, actual value: ${Tone.Transport.bpm.value}`);
|
console.log(`Setting Tone.js BPM to ${project.getBpm()}, actual value: ${Tone.Transport.bpm.value}`);
|
||||||
|
|
||||||
|
// Configure loop settings
|
||||||
|
const isLooping = project.getIsLooping();
|
||||||
|
let scheduleStartBeat = 0;
|
||||||
|
let scheduleEndBeat = Infinity;
|
||||||
|
|
||||||
|
if (isLooping) {
|
||||||
|
const [startBar, endBarOriginal] = project.getLoopingRange();
|
||||||
|
const beatsPerBar = timeSignature.numerator;
|
||||||
|
|
||||||
|
// Handle [0, 0] case - use full project
|
||||||
|
const endBar = (startBar === 0 && endBarOriginal === 0) ? project.getMaxBars() : endBarOriginal;
|
||||||
|
|
||||||
|
scheduleStartBeat = startBar * beatsPerBar;
|
||||||
|
scheduleEndBeat = (endBar + 1) * beatsPerBar; // +1 because endBar is inclusive
|
||||||
|
|
||||||
|
// Configure Tone.Transport loop boundaries
|
||||||
|
const loopStartTime = this.beatsToToneTime(scheduleStartBeat);
|
||||||
|
const loopEndTime = this.beatsToToneTime(scheduleEndBeat);
|
||||||
|
Tone.Transport.setLoopPoints(loopStartTime, loopEndTime);
|
||||||
|
Tone.Transport.loop = true;
|
||||||
|
|
||||||
|
console.log(`Loop mode enabled: bars [${startBar}, ${endBar}], beats [${scheduleStartBeat}, ${scheduleEndBeat}]`);
|
||||||
|
|
||||||
|
// Adjust start position to loop start if before loop range
|
||||||
|
if (startPosition < scheduleStartBeat) {
|
||||||
|
startPosition = scheduleStartBeat;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Tone.Transport.loop = false;
|
||||||
|
console.log("Loop mode disabled");
|
||||||
|
}
|
||||||
|
|
||||||
// Set transport position (convert beats to Tone.js format)
|
// Set transport position (convert beats to Tone.js format)
|
||||||
this.setTransportPosition(startPosition);
|
this.setTransportPosition(startPosition);
|
||||||
|
|
||||||
@@ -290,8 +322,14 @@ export class KGAudioInterface {
|
|||||||
// Calculate absolute note timing in beats (note position + region start position)
|
// Calculate absolute note timing in beats (note position + region start position)
|
||||||
const regionStartBeat = region.getStartFromBeat();
|
const regionStartBeat = region.getStartFromBeat();
|
||||||
const noteStartBeat = note.getStartBeat() + regionStartBeat;
|
const noteStartBeat = note.getStartBeat() + regionStartBeat;
|
||||||
|
const noteEndBeat = note.getEndBeat() + regionStartBeat;
|
||||||
const noteDurationBeats = note.getEndBeat() - note.getStartBeat();
|
const noteDurationBeats = note.getEndBeat() - note.getStartBeat();
|
||||||
|
|
||||||
|
// Skip notes outside loop range when looping
|
||||||
|
if (noteStartBeat >= scheduleEndBeat || noteEndBeat <= scheduleStartBeat) {
|
||||||
|
return; // Skip notes outside the loop range
|
||||||
|
}
|
||||||
|
|
||||||
// Only schedule notes that start at or after the playback start position
|
// Only schedule notes that start at or after the playback start position
|
||||||
if (noteStartBeat < startPosition) {
|
if (noteStartBeat < startPosition) {
|
||||||
return; // Skip notes that would have already finished before playback starts
|
return; // Skip notes that would have already finished before playback starts
|
||||||
|
|||||||
Reference in New Issue
Block a user