Skip to content

Commit

Permalink
Initial non-zero global beat counter start support
Browse files Browse the repository at this point in the history
 * Revamp + modernize MIDI editor scheduling
   * Delete local tempo scheduling mode.  Always start global beat counter when play button is pressed.
   * Add support for non-zero start time
 * Event scheduler/global beat counter changes to support non-zero start time
   * Update cbs to take start time
   * Update internal state to keep track of global beat counter position even when playback isn't active
   * Sync cursor position with global beat counter position, main thread -> audio thread
 * Existing MIDI editor scheduling seems to be working in both loop and oneshot mode
  • Loading branch information
Ameobea committed Jan 25, 2024
1 parent fa0f331 commit 59c0397
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 249 deletions.
8 changes: 5 additions & 3 deletions public/EventSchedulerWorkletProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class EventSchedulerWorkletProcessor extends AudioWorkletProcessor {
globalThis.curBeat = 0;
if (typeof SharedArrayBuffer !== 'undefined') {
this.beatManagerSABInner = new SharedArrayBuffer(1024);
this.beatManagerSAB = new Float32Array(this.beatManagerSABInner);
this.beatManagerSAB = new Float64Array(this.beatManagerSABInner);
}
this.port.postMessage({ type: 'beatManagerSAB', beatManagerSAB: this.beatManagerSAB });

Expand All @@ -108,7 +108,7 @@ class EventSchedulerWorkletProcessor extends AudioWorkletProcessor {
break;
}

globalThis.curBeat = 0;
globalThis.curBeat = event.data.startBeat;
this.lastRecordedTime = currentTime;
globalThis.globalBeatCounterStarted = true;
this.isStarted = true;
Expand All @@ -120,7 +120,6 @@ class EventSchedulerWorkletProcessor extends AudioWorkletProcessor {
break;
}

globalThis.curBeat = 0;
globalThis.globalBeatCounterStarted = false;
this.wasmInstance.exports.stop();
this.isStarted = false;
Expand Down Expand Up @@ -281,6 +280,9 @@ class EventSchedulerWorkletProcessor extends AudioWorkletProcessor {

updateGlobalBeats(globalTempoBPM) {
globalThis.globalTempoBPM = globalTempoBPM;
if (this.beatManagerSAB) {
globalThis.curBeat = this.beatManagerSAB[0];
}

if (this.isStarted) {
const passedTime = currentTime - this.lastRecordedTime;
Expand Down
41 changes: 31 additions & 10 deletions src/eventScheduler/eventScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const registerCb = (cb: () => void): number => {

export const cancelCb = (cbId: number) => RegisteredCbs.delete(cbId);

let StartCBs: (() => void)[] = [];
let StartCBs: ((startBeat: number) => void)[] = [];
let StopCBs: (() => void)[] = [];
let isStarted = false;
let lastStartTime = 0;
Expand All @@ -65,14 +65,14 @@ export const getIsGlobalBeatCounterStarted = (): boolean => isStarted;
/**
* Registers a callback to be called when the global beat counter is started
*/
export const registerGlobalStartCB = (cb: () => void) => StartCBs.push(cb);
export const registerGlobalStartCB = (cb: (startBeat: number) => void) => StartCBs.push(cb);

/**
* Registers a callback to be called when the global beat counter is stopped
*/
export const registerGlobalStopCB = (cb: () => void) => StopCBs.push(cb);

export const unregisterStartCB = (cb: () => void) => {
export const unregisterStartCB = (cb: (startBeat: number) => void) => {
StartCBs = StartCBs.filter(ocb => ocb !== cb);
};

Expand All @@ -99,12 +99,15 @@ export const useIsGlobalBeatCounterStarted = () => {
};

/**
* Starts the global beat counter loop, resetting the current beat to zero. Until this is called, no
* events will be processed and the global current beat will remain at zero.
* Starts the global beat counter loop, resetting the current beat to the specified beat or
* zero if not provided.
*
* Until this is called, no events will be processed and the global current beat will remain
* at zero (or the last beat it reached before being stopped).
*
* Triggers all callbacks registered with `addStartCB` to be called.
*/
export const startAll = () => {
export const startAll = (startBeat = 0) => {
if (isStarted) {
console.warn("Tried to start global beat counter, but it's already started");
return;
Expand All @@ -115,8 +118,8 @@ export const startAll = () => {

isStarted = true;
lastStartTime = ctx.currentTime;
SchedulerHandle.port.postMessage({ type: 'start' });
scheduleEventBeats(0, () => StartCBs.forEach(cb => cb()));
SchedulerHandle.port.postMessage({ type: 'start', startBeat });
scheduleEventBeats(0, () => StartCBs.forEach(cb => cb(startBeat)));
};

/**
Expand Down Expand Up @@ -152,7 +155,7 @@ const callCb = (cbId: number) => {
cb();
};

let beatManagerSAB: Float32Array | null = null;
let beatManagerSAB: Float64Array | null = null;

/**
* Returns the current beat of the global beat counter. This value is updated directly from the web audio rendering thread
Expand All @@ -165,6 +168,24 @@ export const getCurBeat = (): number => {
return beatManagerSAB[0];
};

/**
* Sets the current beat of the global beat counter. This is used to sync the global beat counter with the UI when the user
* drags the playhead around.
*/
export const setCurBeat = (beat: number) => {
if (!beatManagerSAB) {
console.error('Tried to set beat before beat manager initialized');
return;
}

if (getIsGlobalBeatCounterStarted()) {
console.warn('Tried to set beat while beat counter was running');
return;
}

beatManagerSAB[0] = beat;
};

export const getCurGlobalBPM = () => {
if (!beatManagerSAB) {
return 0;
Expand All @@ -173,7 +194,7 @@ export const getCurGlobalBPM = () => {
};

// Init the scheduler AWP instance
export const EventScheduleInitialized = Promise.all([
export const EventSchedulerInitialized = Promise.all([
retryAsync(() =>
fetch(
process.env.ASSET_PATH +
Expand Down
4 changes: 2 additions & 2 deletions src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { mkContainerCleanupHelper, mkContainerRenderHelper } from 'src/reactUtil
import { getSample, hashSampleDescriptor, type SampleDescriptor } from 'src/sampleLibrary';
import { getSentry } from 'src/sentry';
import { AsyncOnce, normalizeEnvelope } from 'src/util';
import { EventScheduleInitialized } from 'src/eventScheduler';
import { EventSchedulerInitialized } from 'src/eventScheduler';
import type { FilterParams } from 'src/redux/modules/synthDesigner';
import { buildDefaultFilter } from 'src/synthDesigner/filterHelpersLight';
import { FilterType } from 'src/synthDesigner/FilterType';
Expand Down Expand Up @@ -366,7 +366,7 @@ export default class FMSynth implements ForeignNode {
const [wasmBytes] = await Promise.all([
WavetableWasmBytes.get(),
RegisterFMSynthAWP.get(),
EventScheduleInitialized,
EventSchedulerInitialized,
] as const);
this.awpHandle = new AudioWorkletNode(this.ctx, 'fm-synth-audio-worklet-processor', {
numberOfInputs: 0,
Expand Down
12 changes: 2 additions & 10 deletions src/midiEditor/MIDIEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import FileUploader, { type FileUploaderValue } from 'src/controls/FileUploader'
import { renderGenericPresetSaverWithModal } from 'src/controls/GenericPresetPicker/GenericPresetSaver';
import { getMidiImportSettings, type MidiFileInfo } from 'src/controls/MidiImportDialog';
import { renderModalWithControls, type ModalCompProps } from 'src/controls/Modal';
import { useIsGlobalBeatCounterStarted } from 'src/eventScheduler';
import { getCurBeat, startAll } from 'src/eventScheduler';
import type { MIDIEditorInstance, SerializedMIDIEditorState } from 'src/midiEditor';
import { CVOutputTopControls } from 'src/midiEditor/CVOutput/CVOutputTopControls';
import { mkLoadMIDICompositionModal } from 'src/midiEditor/LoadMIDICompositionModal';
Expand All @@ -28,8 +28,6 @@ import type { ManagedInstance, MIDIEditorUIManager } from 'src/midiEditor/MIDIEd
import type MIDIEditorPlaybackHandler from 'src/midiEditor/PlaybackHandler';
import EditableInstanceName from './EditableInstanceName.svelte';

const ctx = new AudioContext();

const MIDIWasmModule = new AsyncOnce(() => import('src/midi'));

interface MIDIEditorControlsState {
Expand Down Expand Up @@ -227,7 +225,6 @@ const MIDIEditorControlsInner: React.FC<MIDIEditorControlsProps> = ({
initialState,
onChange: onChangeInner,
}) => {
const isGlobalBeatCounterStarted = useIsGlobalBeatCounterStarted();
const [state, setState] = useState(initialState);
const [isRecording, setIsRecording] = useState(false);
const [metronomeEnabled, setMetronomeEnabled] = useState(initialState.metronomeEnabled);
Expand All @@ -239,17 +236,12 @@ const MIDIEditorControlsInner: React.FC<MIDIEditorControlsProps> = ({
return (
<div className='midi-editor-controls'>
<MIDIEditorControlButton
disabled={isGlobalBeatCounterStarted}
title='Start/Stop Playback'
onClick={() => {
if (playbackHandler.isPlaying) {
playbackHandler.stopPlayback();
} else {
playbackHandler.startPlayback({
type: 'localTempo',
bpm: state.bpm,
startTime: ctx.currentTime,
});
startAll(getCurBeat());
}
}}
label='⏯'
Expand Down
Loading

0 comments on commit 59c0397

Please sign in to comment.