Skip to content

Commit

Permalink
Render MIDI metadata labels in MIDI editor
Browse files Browse the repository at this point in the history
 * Update sampler to set MIDI metadata labels for selections that have mapped MIDI numbers and set names
 * Create helper function to hook into Redux and subscribe to current connections for a given VC
   * Add some memoization and fast paths to avoid calling the callback if connections don't actually change
 * Render and clean up labels in MIDI editor note lines, changing them as connections/metadata change
  • Loading branch information
Ameobea committed Feb 1, 2024
1 parent 137c453 commit 5bf82ea
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 17 deletions.
2 changes: 1 addition & 1 deletion public/SamplerAWP.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class SamplerAWP extends AudioWorkletProcessor {
});
} else {
console.warn(
'SamplerAWP: SharedArrayBuffer not available, MIDI gate status will not be available'
'SamplerAWP: `SharedArrayBuffer`, `Atomics`, or `Atomics.waitAsync` not available, MIDI gate status will not be available'
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/init-composition.json

Large diffs are not rendered by default.

73 changes: 72 additions & 1 deletion src/midiEditor/MIDIEditorUIInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
import * as conf from './conf';
import type { FederatedPointerEvent } from '@pixi/events';
import { UnreachableError } from 'src/util';
import type { Unsubscribe } from 'redux';
import { subscribeToConnections, type ConnectionDescriptor } from 'src/redux/modules/vcmUtils';
import { MIDINode, type MIDINodeMetadata } from 'src/patchNetwork/midiNode';

export interface Note {
id: number;
Expand Down Expand Up @@ -94,8 +97,12 @@ export default class MIDIEditorUIInstance {
public loopCursor: LoopCursor | null;
private clipboard: { startPoint: number; length: number; lineIx: number }[] = [];
public noteMetadataByNoteID: Map<number, any> = new Map();
private vcId: string;
public vcId: string;
private isHidden: boolean;
private unsubscribeConnectablesUpdates: Unsubscribe;
private midiMetadataUnsubscribers: (() => void)[] = [];
private isUnsubscribingMIDIMetadataListeners = false;
private connectedOutputMIDINodeMetadataStores: { [outputName: string]: MIDINodeMetadata } = {};
private destroyed = false;
/**
* A cache used by note lines for storing line marker sprites keyed by `${pxPerBeat}-${beatsPerMeasure}`
Expand Down Expand Up @@ -181,6 +188,10 @@ export default class MIDIEditorUIInstance {
this.parentInstance.playbackHandler?.recordingCtx?.tick();
});

this.unsubscribeConnectablesUpdates = subscribeToConnections(this.vcId, newConnections =>
this.handleConnectionsChanged(newConnections)
);

this.init().then(() => {
if (this.destroyed) {
return;
Expand Down Expand Up @@ -1192,6 +1203,64 @@ export default class MIDIEditorUIInstance {
}
};

private handleConnectionsChanged = (
newConnections:
| {
inputs: ConnectionDescriptor[];
outputs: ConnectionDescriptor[];
}
| undefined
) => {
this.unsubMIDIMetadataListeners();

const connectedOutputs = newConnections?.outputs || [];
connectedOutputs.forEach((conn, outputName) => {
if (!(conn.rxNode instanceof MIDINode)) {
return;
}

const unsubInner = conn.rxNode.metadata.subscribe(metadata => {
this.connectedOutputMIDINodeMetadataStores[outputName] = metadata;
this.handleMIDIOutputMetadataChange();
});
const unsub = () => {
delete this.connectedOutputMIDINodeMetadataStores[outputName];
unsubInner();
this.handleMIDIOutputMetadataChange();
};
this.midiMetadataUnsubscribers.push(unsub);
});
};

private unsubMIDIMetadataListeners() {
this.isUnsubscribingMIDIMetadataListeners = true;
this.midiMetadataUnsubscribers.forEach(unsub => unsub());
this.midiMetadataUnsubscribers = [];
this.isUnsubscribingMIDIMetadataListeners = false;
}

private handleMIDIOutputMetadataChange() {
if (this.isUnsubscribingMIDIMetadataListeners) {
return;
}

const labelByLineIx: Map<number, string> = new Map();

for (const metadata of Object.values(this.connectedOutputMIDINodeMetadataStores)) {
for (const [midiNumber, noteMetadata] of metadata.noteMetadata) {
const lineIx = this.lines.length - midiNumber;
if (noteMetadata.name && !labelByLineIx.has(lineIx)) {
labelByLineIx.set(lineIx, noteMetadata.name);
}
}
}

this.lines.forEach((line, lineIx) => {
const label = labelByLineIx.get(lineIx);
line.setLabel(label);
});
}

public destroy() {
if (this.destroyed) {
console.error('MIDI editor already destroyed');
Expand All @@ -1200,6 +1269,8 @@ export default class MIDIEditorUIInstance {

this.destroyed = true;
this.cleanupEventHandlers();
this.unsubscribeConnectablesUpdates();
this.unsubMIDIMetadataListeners();
try {
destroyPIXIApp(this.app);
} catch (err) {
Expand Down
29 changes: 29 additions & 0 deletions src/midiEditor/NoteLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default class NoteLine {
private markers: PIXI.Sprite | undefined;
private noteCreationState: NoteCreationState | null = null;
private isCulled = true;
private isDestroyed = false;
private labelText: PIXI.Text | undefined;

/**
* Used for markings caching to determine whether we need to re-render markings or not
*/
Expand Down Expand Up @@ -265,7 +268,33 @@ export default class NoteLine {
this.markers.x = this.app.beatsToPx(xOffsetBeats);
}

public setLabel(text: string | undefined) {
if (text) {
if (!this.labelText) {
this.labelText = new PIXI.Text(text, {
fontSize: 12,
fill: conf.LINE_LABEL_COLOR,
fontFamily: 'Hack',
});
this.labelText.x = 4;
this.labelText.y = 1;
this.container.addChild(this.labelText);
} else {
this.labelText.text = text;
}
} else if (this.labelText) {
this.container.removeChild(this.labelText);
this.labelText.destroy();
this.labelText = undefined;
}
}

public destroy() {
if (this.isDestroyed) {
console.warn('Attempted to destroy a note line that was already destroyed');
}
this.isDestroyed = true;

for (const note of this.notesByID.values()) {
note.destroy();
}
Expand Down
1 change: 1 addition & 0 deletions src/midiEditor/conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export const SAMPLE_EDITOR_LABEL_BORDER_COLOR = 0x151515;
export const SAMPLE_EDITOR_LABEL_BACKGROUND_COLOR = 0xadadad;
export const SAMPLE_EDITOR_LABEL_TEXT_COLOR = 0x040404;
export const SAMPLE_EDITOR_LABEL_FONT_SIZE = 20;
export const LINE_LABEL_COLOR = 0xfa5fe6;
6 changes: 3 additions & 3 deletions src/patchNetwork/midiNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ export type MIDIEvent =
| { type: MIDIEventType.Attack; note: number; velocity: number }
| { type: MIDIEventType.Release; note: number; velocity: number };

interface MIDINoteMetadata {
export interface MIDINoteMetadata {
active: boolean;
name?: string;
}

interface MIDINodeMetadata {
export interface MIDINodeMetadata {
/**
* Sparse map of note numbers to metadata about that note.
* Sparse map of MIDI numbers to metadata about that note.
*/
noteMetadata: Map<number, MIDINoteMetadata>;
}
Expand Down
104 changes: 102 additions & 2 deletions src/redux/modules/vcmUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Option } from 'funfix-core';
import { Map } from 'immutable';
import * as R from 'ramda';
import { shallowEqual } from 'react-redux';
import type { Unsubscribe } from 'redux';

import { PlaceholderInput } from 'src/controlPanel/PlaceholderInput';
import { OverridableAudioNode, OverridableAudioParam } from 'src/graphEditor/nodes/util';
Expand All @@ -14,8 +16,8 @@ import type {
} from 'src/patchNetwork';
import type { MIDINode } from 'src/patchNetwork/midiNode';
import { reinitializeWithComposition } from 'src/persistance';
import { getState } from 'src/redux';
import { getEngine } from 'src/util';
import { getState, store } from 'src/redux';
import { filterNils, getEngine, UnreachableError } from 'src/util';
import { setGlobalVolume } from 'src/ViewContextManager/GlobalVolumeSlider';

/**
Expand Down Expand Up @@ -191,3 +193,101 @@ export const initializeDefaultVCMState = () => {
}
setGlobalVolume(20);
};

export interface ConnectionDescriptor {
txVcId: string;
rxVcId: string;
txPortName: string;
rxPortName: string;
txNode: AudioNode | MIDINode;
rxNode: AudioNode | MIDINode | AudioParam;
}

/**
* Subscribes to changes in the connections to/from the given VC ID.
*
* @returns An `Unsubscribe` function that can be called to unsubscribe from the store.
*/
export const subscribeToConnections = (
vcId: string,
cb: (
newConnections: { inputs: ConnectionDescriptor[]; outputs: ConnectionDescriptor[] } | undefined
) => void
): Unsubscribe => {
const buildConnectionDescriptor = ([from, to]: [
ConnectableDescriptor,
ConnectableDescriptor,
]): ConnectionDescriptor | null => {
const txNode = getState()
.viewContextManager.patchNetwork.connectables.get(from.vcId)
?.outputs.get(from.name)?.node;
if (!txNode) {
return null;
}
if (txNode instanceof AudioParam) {
throw new UnreachableError('`AudioParam`s cannot be source nodes');
}

const rxNode = getState()
.viewContextManager.patchNetwork.connectables.get(to.vcId)
?.inputs.get(to.name)?.node;
if (!rxNode) {
return null;
}

return {
txVcId: from.vcId,
rxVcId: to.vcId,
txPortName: from.name,
rxPortName: to.name,
txNode,
rxNode,
};
};

const getConnectionsForVc = () => {
const conns = getState().viewContextManager.patchNetwork.connections.filter(
([from, to]) => from.vcId === vcId || to.vcId === vcId
);
return filterNils(
R.sortWith(
[
R.ascend(([from, _to]) => from.vcId),
R.ascend(([from, _to]) => from.name),
R.ascend(([_from, to]) => to.vcId),
R.ascend(([_from, to]) => to.name),
],
conns
).map(buildConnectionDescriptor)
);
};

const connectionsEqual = (a: ConnectionDescriptor[], b: ConnectionDescriptor[]): boolean => {
if (a.length !== b.length) {
return false;
}
return a.every((conn, ix) => shallowEqual(conn, b[ix]));
};

let lastConnections: [ConnectableDescriptor, ConnectableDescriptor][] =
getState().viewContextManager.patchNetwork.connections;
let lastConnectables = getConnectionsForVc();

return store.subscribe(() => {
// Fast path if no connections have changed
const newConnections = getState().viewContextManager.patchNetwork.connections;
if (newConnections === lastConnections) {
return;
}
lastConnections = newConnections;

const connectables = getConnectionsForVc();

if (!connectionsEqual(connectables, lastConnectables)) {
const [inputs, outputs] = R.partition(conn => conn.rxVcId === vcId, connectables);

cb({ inputs, outputs });
lastConnectables = connectables;
}
});
};
27 changes: 26 additions & 1 deletion src/sampler/SamplerInstance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WaveformRenderer } from 'src/granulator/GranulatorUI/WaveformRenderer';
import { type MIDIInputCbs, MIDINode } from 'src/patchNetwork/midiNode';
import { type MIDIInputCbs, MIDINode, type MIDINoteMetadata } from 'src/patchNetwork/midiNode';
import { getSample, hashSampleDescriptor, type SampleDescriptor } from 'src/sampleLibrary';
import type { SamplerSelection, SerializedSampler } from 'src/sampler/sampler';
import { AsyncOnce, UnreachableError, delay, getEngine } from 'src/util';
Expand Down Expand Up @@ -78,6 +78,8 @@ export class SamplerInstance {
}
});

this.updateMIDINodeMetadata();

this.init();
}

Expand Down Expand Up @@ -225,11 +227,34 @@ export class SamplerInstance {
throw new Error(`Selection at index ${ix} does not exist`);
}

const nameChanged = selections[ix].name !== newSelection.name;
const midiNumberChanged = selections[ix].midiNumber !== newSelection.midiNumber;

const newSelections = [...selections];
newSelections[ix] = newSelection;
this.selections.set(newSelections);

this.commitSelection(newSelection);

if (nameChanged || midiNumberChanged) {
this.updateMIDINodeMetadata();
}
}

private updateMIDINodeMetadata() {
const selections = get(this.selections);
const newNoteMetadata: Map<number, MIDINoteMetadata> = new Map();

for (const selection of selections) {
if (typeof selection.midiNumber === 'number') {
newNoteMetadata.set(selection.midiNumber, {
active: true,
name: selection.name || undefined,
});
}
}

this.midiNode.metadata.update(metadata => ({ ...metadata, noteMetadata: newNoteMetadata }));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/sampler/SamplerUI/ConfigureSelection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import SvelteControlPanel, {
type ControlPanelSetting,
} from 'src/controls/SvelteControlPanel/SvelteControlPanel.svelte';
import { SamplerSelection } from 'src/sampler/sampler';
import type { SamplerSelection } from 'src/sampler/sampler';
import { mkMIDINumberDisplay } from './MIDINumberDisplay';
</script>

Expand Down
2 changes: 1 addition & 1 deletion src/sampler/SamplerUI/SamplerUI.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { SampleDescriptor } from 'src/sampleLibrary';
import type { SampleDescriptor } from 'src/sampleLibrary';
import type { SamplerInstance } from 'src/sampler/SamplerInstance';
import MainSamplerUI from 'src/sampler/SamplerUI/MainSamplerUI.svelte';
import PickSample from 'src/sampler/SamplerUI/PickSample.svelte';
Expand Down
Loading

0 comments on commit 5bf82ea

Please sign in to comment.