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:
+48
-4
@@ -270,6 +270,26 @@ export class KGCore {
|
||||
|
||||
// High-level playback control methods
|
||||
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
|
||||
await this.preparePlay();
|
||||
|
||||
@@ -336,12 +356,36 @@ export class KGCore {
|
||||
|
||||
const bpm = this.currentProject.getBpm();
|
||||
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)
|
||||
const maxBars = this.currentProject.getMaxBars();
|
||||
// Handle looping or end-of-project
|
||||
const beatsPerBar = this.currentProject.getTimeSignature().numerator;
|
||||
|
||||
if (this.currentProject.getIsLooping()) {
|
||||
// Loop mode: wrap playhead when it reaches loop end
|
||||
const [startBar, endBarOriginal] = this.currentProject.getLoopingRange();
|
||||
const endBar = (startBar === 0 && endBarOriginal === 0) ? this.currentProject.getMaxBars() : endBarOriginal;
|
||||
|
||||
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);
|
||||
@@ -349,6 +393,7 @@ export class KGCore {
|
||||
this.stopPlaying();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update playhead position
|
||||
this.setPlayheadPosition(newPosition);
|
||||
@@ -357,7 +402,6 @@ export class KGCore {
|
||||
// - Sync with Tone.Transport position for more accurate timing
|
||||
// - Handle tempo changes mid-playback
|
||||
// - Account for latency compensation
|
||||
// - Support for loop regions
|
||||
}
|
||||
|
||||
// selected items
|
||||
|
||||
@@ -267,6 +267,38 @@ export class KGAudioInterface {
|
||||
|
||||
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)
|
||||
this.setTransportPosition(startPosition);
|
||||
|
||||
@@ -290,8 +322,14 @@ export class KGAudioInterface {
|
||||
// Calculate absolute note timing in beats (note position + region start position)
|
||||
const regionStartBeat = region.getStartFromBeat();
|
||||
const noteStartBeat = note.getStartBeat() + regionStartBeat;
|
||||
const noteEndBeat = note.getEndBeat() + regionStartBeat;
|
||||
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
|
||||
if (noteStartBeat < startPosition) {
|
||||
return; // Skip notes that would have already finished before playback starts
|
||||
|
||||
Reference in New Issue
Block a user