diff --git a/engine/note_container/src/exports.rs b/engine/note_container/src/exports.rs index 6e9d42f9..2d0e5ac0 100644 --- a/engine/note_container/src/exports.rs +++ b/engine/note_container/src/exports.rs @@ -132,12 +132,16 @@ pub fn iter_notes( /// 1. an `is_attack` flag which is true if the note is starting and false if the note is ending /// 2. line index /// 3. beat +/// +/// If `include_partial_notes` is true, then notes that intersect the start or end of the selection +/// will be included but truncated to the bounds of the selection. #[wasm_bindgen] pub fn iter_notes_with_cb( lines: *const NoteLines, start_beat_inclusive: f64, end_beat_exclusive: f64, cb: Function, + include_partial_notes: bool, ) { let notes = unsafe { &*lines }; @@ -147,8 +151,14 @@ pub fn iter_notes_with_cb( note: Note, } + struct NoteEvent { + is_attack: bool, + line_ix: usize, + beat: f64, + } + let mut unreleased_notes: HashMap = HashMap::default(); - let mut events: Vec<(bool, usize, f64)> = Vec::default(); + let mut events: Vec = Vec::default(); let iter = notes.lines.iter().enumerate().flat_map(|(line_ix, line)| { line .inner @@ -165,7 +175,11 @@ pub fn iter_notes_with_cb( for (line_ix, pos, entry) in iter { match entry { NoteEntry::NoteStart { note } => { - events.push((true, line_ix, pos)); + events.push(NoteEvent { + is_attack: true, + line_ix, + beat: pos, + }); let existing = unreleased_notes.insert(note.id, UnreleasedNote { line_ix, start_point: pos, @@ -178,8 +192,21 @@ pub fn iter_notes_with_cb( }, NoteEntry::NoteEnd { note_id } => { let existing = unreleased_notes.remove(¬e_id); - if existing.is_some() { - events.push((false, line_ix, pos)); + + if existing.is_none() && include_partial_notes { + events.push(NoteEvent { + is_attack: true, + line_ix, + beat: start_beat_inclusive, + }); + } + + if existing.is_some() || include_partial_notes { + events.push(NoteEvent { + is_attack: false, + line_ix, + beat: pos, + }); } }, NoteEntry::StartAndEnd { @@ -189,10 +216,18 @@ pub fn iter_notes_with_cb( // release before attack let existing = unreleased_notes.remove(&end_note_id); if existing.is_some() { - events.push((false, line_ix, pos)); + events.push(NoteEvent { + is_attack: false, + line_ix, + beat: pos, + }); } - events.push((true, line_ix, pos)); + events.push(NoteEvent { + is_attack: true, + line_ix, + beat: pos, + }); let existing = unreleased_notes.insert(start_note.id, UnreleasedNote { line_ix, start_point: pos, @@ -212,10 +247,27 @@ pub fn iter_notes_with_cb( } for note in unreleased_notes.values() { let release_time = note.start_point + note.note.length; - events.push((false, note.line_ix, release_time)); + events.push(NoteEvent { + is_attack: false, + line_ix: note.line_ix, + beat: release_time, + }); } - for (is_attack, line_ix, beat) in events { + events.sort_unstable_by(|a, b| { + FloatOrd(a.beat) + .cmp(&FloatOrd(b.beat)) + .then_with(|| a.line_ix.cmp(&b.line_ix)) + // attacks before releases + .then_with(|| a.is_attack.cmp(&b.is_attack).reverse()) + }); + + for NoteEvent { + is_attack, + line_ix, + beat, + } in events + { let _ = cb.call3( &JsValue::NULL, &JsValue::from(is_attack), diff --git a/public/SequencerWorkletProcessor.js b/public/SequencerWorkletProcessor.js index 23d0a4a8..62404983 100644 --- a/public/SequencerWorkletProcessor.js +++ b/public/SequencerWorkletProcessor.js @@ -16,8 +16,8 @@ class SequencerWorkletProcessor extends AudioWorkletProcessor { break; } case 'start': { - this.startBeat = globalThis.curBeat; - this.lastBeat = -1; + this.startBeat = evt.data.startBeat ?? globalThis.curBeat; + this.lastBeat = Math.trunc(evt.data.startBeat / this.config.beatRatio); break; } case 'stop': { @@ -44,7 +44,7 @@ class SequencerWorkletProcessor extends AudioWorkletProcessor { return true; } - const beatsSinceStart = globalThis.curBeat - this.startBeat; + const beatsSinceStart = globalThis.curBeat; const curQuantizedBeat = Math.trunc(beatsSinceStart / this.config.beatRatio); if (curQuantizedBeat !== this.lastBeat) { diff --git a/src/eventScheduler/eventScheduler.ts b/src/eventScheduler/eventScheduler.ts index b92ab4b3..eb864782 100644 --- a/src/eventScheduler/eventScheduler.ts +++ b/src/eventScheduler/eventScheduler.ts @@ -107,7 +107,7 @@ export const useIsGlobalBeatCounterStarted = () => { * * Triggers all callbacks registered with `addStartCB` to be called. */ -export const startAll = (startBeat = 0) => { +export const startAll = (startBeat = getCurBeat()) => { if (isStarted) { console.warn("Tried to start global beat counter, but it's already started"); return; diff --git a/src/midiEditor/MIDIEditorUIManager.tsx b/src/midiEditor/MIDIEditorUIManager.tsx index cf2a53a5..03eda199 100644 --- a/src/midiEditor/MIDIEditorUIManager.tsx +++ b/src/midiEditor/MIDIEditorUIManager.tsx @@ -157,7 +157,8 @@ export class ManagedMIDIEditorUIInstance { noteLinesCtxPtr, startBeatInclusive ?? 0, endBeatExclusive ?? -1, - cb + cb, + true ); }; diff --git a/src/midiEditor/PlaybackHandler.ts b/src/midiEditor/PlaybackHandler.ts index 78347f5f..aae064be 100644 --- a/src/midiEditor/PlaybackHandler.ts +++ b/src/midiEditor/PlaybackHandler.ts @@ -135,7 +135,6 @@ export default class MIDIEditorPlaybackHandler { * to determine if a given playback session has ended or not. */ private playbackGeneration: number | null = null; - private lastPlaybackStartBeat = 0; private cbs: { start: (startBeat: number) => void; stop: () => void; @@ -343,6 +342,11 @@ export default class MIDIEditorPlaybackHandler { return; } + const firstLoopRemainder = loopLengthBeats - (startBeat % loopLengthBeats); + const curSegmentAbsoluteStartBeat = + startBeat - (loopLengthBeats - firstLoopRemainder) + loopIx * loopLengthBeats; + const curSegmentAbsoluteEndBeat = curSegmentAbsoluteStartBeat + loopLengthBeats; + const insts = get(this.inst.uiManager.instances); for (const inst of insts) { if (inst.type !== 'midiEditor') { @@ -350,21 +354,20 @@ export default class MIDIEditorPlaybackHandler { } const instance = inst.instance; - const offset = loopLengthBeats * loopIx; // If we're starting in the middle of a loop on the first loop iteration, filter out notes that // start before the starting cursor position const notesInRange = this.getNotesInRange( instance, - loopIx === 0 ? startBeat : 0, + loopIx === 0 ? startBeat % loopLengthBeats : 0, loopPoint, - offset + curSegmentAbsoluteStartBeat ); this.scheduleNotes(instance, notesInRange); } // Schedule an event before the loop ends to recursively schedule another. - const curSegmentAbsoluteEndBeat = loopLengthBeats * (loopIx + 1); + scheduleEventBeats(curSegmentAbsoluteEndBeat - Math.min(1, loopLengthBeats / 2), () => scheduleAnother(loopIx + 1) ); @@ -408,7 +411,6 @@ export default class MIDIEditorPlaybackHandler { } } - this.lastPlaybackStartBeat = startBeat; this.playbackGeneration = Math.random(); if (this.loopPoint === null) { this.scheduleOneshot(startBeat); diff --git a/src/sequencer/redux.ts b/src/sequencer/redux.ts index ad9c4c1e..3c4b6e11 100644 --- a/src/sequencer/redux.ts +++ b/src/sequencer/redux.ts @@ -104,7 +104,7 @@ const reschedule = (state: SequencerReduxState): SequencerReduxState => { }; interface SequencerInst extends SequencerReduxInfra { - onGlobalStart: () => void; + onGlobalStart: (startBeat: number) => void; onGlobalStop: () => void; } @@ -173,8 +173,12 @@ const actionGroups = { }), }), TOGGLE_IS_PLAYING: buildActionGroup({ - actionCreator: (vcId: string) => ({ type: 'TOGGLE_IS_PLAYING', vcId }), - subReducer: (state: SequencerReduxState, { vcId }) => { + actionCreator: (vcId: string, startBeat?: number) => ({ + type: 'TOGGLE_IS_PLAYING', + vcId, + startBeat, + }), + subReducer: (state: SequencerReduxState, { vcId, startBeat }) => { if (!state.awpHandle) { return state; } @@ -185,7 +189,7 @@ const actionGroups = { state.awpHandle.port.postMessage({ type: 'stop' }); return { ...state, isPlaying: false, curActiveMarkIx: null }; } else { - state.awpHandle.port.postMessage({ type: 'start' }); + state.awpHandle.port.postMessage({ type: 'start', startBeat: startBeat ?? 0 }); return reschedule({ ...state, isPlaying: true, diff --git a/src/sequencer/sequencer.tsx b/src/sequencer/sequencer.tsx index 7f0ab66d..ee0e8be1 100644 --- a/src/sequencer/sequencer.tsx +++ b/src/sequencer/sequencer.tsx @@ -329,12 +329,12 @@ export const init_sequencer = (stateKey: string) => { console.error(`Existing entry in sequencer redux infra map for vcId ${vcId}; overwriting...`); } - const onGlobalStart = () => { + const onGlobalStart = (startBeat: number) => { const isPlaying = reduxInfra.getState().sequencer.isPlaying; if (isPlaying) { reduxInfra.dispatch(reduxInfra.actionCreators.sequencer.TOGGLE_IS_PLAYING(vcId)); } - reduxInfra.dispatch(reduxInfra.actionCreators.sequencer.TOGGLE_IS_PLAYING(vcId)); + reduxInfra.dispatch(reduxInfra.actionCreators.sequencer.TOGGLE_IS_PLAYING(vcId, startBeat)); }; registerGlobalStartCB(onGlobalStart); const onGlobalStop = () => {