Files
op-pedal/vite/src/pipedal/Pedalboard.tsx
T
2026-06-16 03:39:50 -04:00

892 lines
28 KiB
TypeScript

// Copyright (c) Robin E.R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { PiPedalArgumentError } from './PiPedalError';
import MidiBinding from './MidiBinding';
import MidiChannelBinding from './MidiChannelBinding';
const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Split";
const EMPTY_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Empty";
interface Deserializable<T> {
deserialize(input: any): T;
}
export class ControlValue implements Deserializable<ControlValue> {
constructor(key?: string, value?: number)
{
this.key = key??"";
this.value = value?? 0;
}
deserialize(input: any): ControlValue {
this.key = input.key;
this.value = input.value;
return this;
}
static EmptyArray: ControlValue[] = [];
static deserializeArray(input: any[]): ControlValue[] {
let result: ControlValue[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new ControlValue().deserialize(input[i]);
}
return result;
}
setValue(value: number) {
this.value = value;
}
key: string;
value: number;
}
export class PedalboardItem implements Deserializable<PedalboardItem> {
clone(): PedalboardItem {
if (this.isSplit())
{
let result = new PedalboardSplitItem();
result.deserialize(this);
return result as PedalboardItem;;
}
return new PedalboardItem().deserialize(this);
}
deserializePedalboardItem(input: any): PedalboardItem {
this.instanceId = input.instanceId ?? -1;
this.title = input.title ?? "";
this.uri = input.uri;
this.pluginName = input.pluginName;
this.isEnabled = input.isEnabled;
this.midiBindings = MidiBinding.deserialize_array(input.midiBindings);
if (input.midiChannelBinding)
{
this.midiChannelBinding = input.midiChannelBinding;
} else {
this.midiChannelBinding = null;
}
this.controlValues = ControlValue.deserializeArray(input.controlValues);
this.vstState = input.vstState ?? "";
this.stateUpdateCount = input.stateUpdateCount;
this.lv2State = input.lv2State;
this.lilvPresetUri = input.lilvPresetUri;
this.pathProperties = input.pathProperties;
this.useModUi = input.useModUi ?? false;
this.iconColor = input.iconColor??"";
this.sideChainInputId = input.sideChainInputId ?? -1;
return this;
}
deserialize(input: any): PedalboardItem {
return this.deserializePedalboardItem(input);
}
static deserializeArray(input: any): PedalboardItem[] {
let result: PedalboardItem[] = [];
for (let i = 0; i < input.length; ++i) {
let inputItem: any = input[i];
let uri: string = inputItem.uri as string;
let outputItem: PedalboardItem;
if (uri === SPLIT_PEDALBOARD_ITEM_URI) {
outputItem = new PedalboardSplitItem().deserialize(inputItem);
} else {
outputItem = new PedalboardItem().deserialize(inputItem);
}
result[i] = outputItem;
}
return result;
}
isSyntheticItem(): boolean {
return this.instanceId === Pedalboard.START_CONTROL_ID
|| this.instanceId === Pedalboard.END_CONTROL_ID;
}
getInstanceId() : number {
if (this.instanceId === undefined)
{
throw new PiPedalArgumentError("Item does not have an id.");
}
return this.instanceId;
}
isEmpty(): boolean {
return this.uri === EMPTY_PEDALBOARD_ITEM_URI;
}
isSplit(): boolean {
return this.uri === SPLIT_PEDALBOARD_ITEM_URI;
}
isStart(): boolean {
return this.uri === Pedalboard.START_PEDALBOARD_ITEM_URI;
}
isEnd(): boolean {
return this.uri === Pedalboard.END_PEDALBOARD_ITEM_URI;
}
getControl(key: string): ControlValue {
for (let i = 0; i < this.controlValues.length; ++i) {
let v = this.controlValues[i];
if (v.key === key) {
return v;
}
}
throw new PiPedalArgumentError("Invalid key.");
}
isChild(instanceId: number): boolean {
if (this.instanceId === instanceId) return true;
if (this.isSplit())
{
let splitItem = this as unknown as PedalboardSplitItem;
for (let topItem of splitItem.topChain)
{
if (topItem.isChild(instanceId)) return true;
}
for (let bottomItem of splitItem.bottomChain)
{
if (bottomItem.isChild(instanceId)) return true;
}
}
return false;
}
getControlValue(key: string): number {
for (let i = 0; i < this.controlValues.length; ++i) {
let v = this.controlValues[i];
if (v.key === key) {
return v.value;
}
}
return 0;
}
setControlValue(key: string, value: number): boolean {
for (let i = 0; i < this.controlValues.length; ++i) {
let v = this.controlValues[i];
if (v.key === key) {
if (v.value === value) return false;
v.setValue(value);
return true;
}
}
return false;
}
setMidiBinding(midiBinding: MidiBinding): boolean {
if (this.midiBindings)
{
for (let i = 0; i < this.midiBindings.length; ++i)
{
let binding = this.midiBindings[i];
if (midiBinding.symbol === binding.symbol)
{
if (binding.equals(midiBinding))
{
return false;
}
this.midiBindings.splice(i,1,midiBinding);
return true;
}
}
this.midiBindings.push(midiBinding);
return true;
}
this.midiBindings = [ midiBinding];
return true;
}
getMidiBinding(symbol: string): MidiBinding {
if (this.midiBindings)
{
for (let i = 0; i < this.midiBindings.length; ++i)
{
let midiBinding = this.midiBindings[i];
if (midiBinding.symbol === symbol)
{
return midiBinding;
}
}
}
let result = new MidiBinding();
result.symbol = symbol;
return result;
}
static EmptyArray: PedalboardItem[] = [];
instanceId: number = -1;
title: string = "";
isEnabled: boolean = false;
uri: string = "";
pluginName?: string;
controlValues: ControlValue[] = ControlValue.EmptyArray;
midiBindings: MidiBinding[] = [];
midiChannelBinding: MidiChannelBinding | null = null;
vstState: string = "";
stateUpdateCount: number = 0;
lv2State: [boolean,any] = [false,{}];
lilvPresetUri: string = "";
pathProperties: {[Name: string]: string} = {};
useModUi: boolean = false; // true if this item should use the mod-ui.
iconColor: string = "";
sideChainInputId: number = -1; // -1 means no sidechain input.
};
export class SnapshotValue {
deserialize(input: any): SnapshotValue {
this.isEnabled = input.isEnabled;
this.instanceId = input.instanceId;
this.controlValues = ControlValue.deserializeArray(input.controlValues);
this.lv2State = input.lv2State;
this.pathProperties = input.pathProperties;
return this;
}
static deserializeArray(input: any): SnapshotValue[] {
let result: SnapshotValue[] = [];
for (let i = 0; i < input.length; ++i) {
let inputItem: any = input[i];
let outputItem = new SnapshotValue().deserialize(inputItem);
result[i] = outputItem;
}
return result;
}
static createFromPedalboardItem(item: PedalboardItem)
{
let result = new SnapshotValue();
result.instanceId = item.instanceId;
result.isEnabled = item.isEnabled;
result.controlValues = ControlValue.deserializeArray(item.controlValues);
result.lv2State = item.lv2State; // we can do this, because lv2State is immutable.
result.pathProperties = {...item.pathProperties}; // clone the dictionary.
return result;
}
instanceId: number = -1;
isEnabled: boolean = true;
controlValues: ControlValue[] = ControlValue.EmptyArray;
lv2State: [boolean,any] = [false,{}];
pathProperties: {[Name: string]: string} = {};
}
export class Snapshot {
deserialize(input: any): Snapshot {
this.values = SnapshotValue.deserializeArray(input.values);
this.isModified = input.isModified;
this.name = input.name;
this.color = input.color;
return this;
}
static deserializeArray(input: any): (Snapshot| null)[] {
let result: (Snapshot|null)[] = [];
for (let i = 0; i < input.length; ++i) {
let inputItem: any = input[i];
let outputItem: (Snapshot|null) = null;
if (inputItem !== null)
{
outputItem = new Snapshot().deserialize(inputItem);
}
result[i] = outputItem;
}
return result;
}
static readonly MAX_SNAPSHOTS: number = 6;
static cloneSnapshots(snapshots: (Snapshot|null)[]): (Snapshot|null)[]
{
let result: (Snapshot|null)[] = [];
for (let i = 0; i < Snapshot.MAX_SNAPSHOTS; ++i)
{
if (i >= snapshots.length)
{
result.push(null);
} else {
result.push(snapshots[i]);
}
}
return result;
}
name: string = "";
isModified: boolean = false;
color: string = "";
values: SnapshotValue[] = [];
};
export enum SplitType {
Ab = 0,
Mix = 1,
Lr = 2
}
export class PedalboardSplitItem extends PedalboardItem {
static PANL_KEY: string = "panL";
static PANR_KEY: string = "panR";
static VOLL_KEY: string = "volL";
static VOLR_KEY: string = "volR";
static MIX_KEY: string = "mix";
static TYPE_KEY: string = "splitType";
static SELECT_KEY: string = "select";
deserialize(input: any): PedalboardSplitItem {
this.deserializePedalboardItem(input);
this.topChain = PedalboardItem.deserializeArray(input.topChain);
this.bottomChain = PedalboardItem.deserializeArray(input.bottomChain);
return this;
}
getSplitType(): SplitType {
let rawValue = this.getControlValue(PedalboardSplitItem.TYPE_KEY);
if (rawValue < 1) return SplitType.Ab;
if (rawValue < 2) return SplitType.Mix;
return SplitType.Lr;
}
getToggleAbControlValue(): ControlValue {
let cv = new ControlValue();
cv.key = "select";
cv.value = this.isASelected() ? 1 : 0;
return cv;
}
getMixControl(): ControlValue {
return this.getControl(PedalboardSplitItem.MIX_KEY);
}
getMix(): number {
return this.getControlValue(PedalboardSplitItem.MIX_KEY);
}
isASelected(): boolean {
return this.getSplitType() !== SplitType.Ab || this.getControlValue(PedalboardSplitItem.SELECT_KEY) === 0;
}
isBSelected(): boolean {
return this.getSplitType() !== SplitType.Ab || this.getControlValue(PedalboardSplitItem.SELECT_KEY) !== 0;
}
topChain: PedalboardItem[] = PedalboardItem.EmptyArray;
bottomChain: PedalboardItem[] = PedalboardItem.EmptyArray;
}
export enum InstanceType {
MainPedalboard,
MainInsert,
AuxInsert
}
export class Pedalboard implements Deserializable<Pedalboard> {
static readonly START_CONTROL_ID = -2; // synthetic PedalboardItem for input volume.
static readonly END_CONTROL_ID = -3; // synthetic PedalboardItem for output volume.
static readonly AUX_START_CONTROL_ID = -4;
static readonly AUX_END_CONTROL_ID = -5;
static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start";
static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End";
deserialize(input: any): Pedalboard {
this.name = input.name;
this.input_volume_db = input.input_volume_db;
this.output_volume_db = input.output_volume_db;
this.items = PedalboardItem.deserializeArray(input.items);
this.nextInstanceId = input.nextInstanceId ?? 0;
this.snapshots = input.snapshots ? Snapshot.deserializeArray(input.snapshots): [];
this.selectedSnapshot = input.selectedSnapshot;
this.pathProperties = input.pathProperties;
this.selectedPlugin = input.selectedPlugin??-1;
return this;
}
clone(): Pedalboard {
return new Pedalboard().deserialize(this);
}
name: string = "";
input_volume_db: number = 0;
output_volume_db: number = 0;
items: PedalboardItem[] = [];
nextInstanceId: number = -1;
snapshots: (Snapshot | null)[] = [];
selectedSnapshot: number = -1;
pathProperties: {[Name: string]: string} = {};
selectedPlugin: number = -1;
// yields all items in the pedalboard, including split items. Splits are yielded before their children.
*itemsGenerator(): Generator<PedalboardItem, void, undefined> {
let it = itemGenerator_(this.items);
while (true)
{
let v = it.next();
if (v.done) break;
yield v.value;
}
}
// same as itemsGenerator, but yields split items after their chains.
*itemsGeneratorSplitAfter(): Generator<PedalboardItem, void, undefined> {
let it = itemGeneratorSplitAfter_(this.items);
while (true)
{
let v = it.next();
if (v.done) break;
yield v.value;
}
}
makeSnapshot(): Snapshot {
let result = new Snapshot();
let it = this.itemsGenerator();
while (true)
{
let v = it.next();
if (v.done) break;
let pedalboardItem = v.value;
let snapshotValue = SnapshotValue.createFromPedalboardItem(pedalboardItem);
result.values.push(snapshotValue);
}
return result;
}
hasItem(instanceId: number): boolean
{
let it = this.itemsGenerator();
while (true)
{
let v = it.next();
if (v.done) break;
if (v.value.instanceId === instanceId)
{
return true;
}
}
return false;
}
getFirstSelectableItem(): number
{
if (this.items.length !== 0)
{
return this.items[0].instanceId;
}
return -1;
}
maybeGetItem(instanceId: number): PedalboardItem | null{
let it = this.itemsGenerator();
while (true)
{
let v = it.next();
if (v.done) break;
if (v.value.instanceId === instanceId)
{
return v.value;
}
}
return null;
}
makeStartItem(): PedalboardItem {
let result = new PedalboardItem();
result.pluginName = "Input";
result.instanceId = Pedalboard.START_CONTROL_ID;
result.uri = Pedalboard.START_PEDALBOARD_ITEM_URI;
result.isEnabled = true;
result.controlValues = [new ControlValue("volume_db",this.input_volume_db)];
return result;
}
makeEndItem(): PedalboardItem {
let result = new PedalboardItem();
result.pluginName = "Output";
result.instanceId = Pedalboard.END_CONTROL_ID;
result.uri = Pedalboard.END_PEDALBOARD_ITEM_URI;
result.isEnabled = true;
result.controlValues = [new ControlValue("volume_db",this.output_volume_db)];
return result;
}
getItem(instanceId: number): PedalboardItem {
let it = this.itemsGenerator();
while (true)
{
let v = it.next();
if (v.done) break;
if (v.value.instanceId === instanceId)
{
return v.value;
}
}
throw new PiPedalArgumentError("Item not found.");
}
tryGetItem(instanceId: number): PedalboardItem | null {
let it = this.itemsGenerator();
while (true)
{
let v = it.next();
if (v.done) break;
if (v.value.instanceId === instanceId)
{
return v.value;
}
}
return null;
}
private deleteItem_(instanceId: number,items: PedalboardItem[]): number | null
{
for (let i = 0; i < items.length; ++i)
{
let item = items[i];
if (item.instanceId === instanceId)
{
if (items.length > 1) {
items.splice(i,1);
let nextSelectedItem = i;
if (i >= items.length) --nextSelectedItem;
return items[nextSelectedItem].instanceId;
} else {
// replace with an empty item.
let newItem = this.createEmptyItem();
items[i] = newItem;
return newItem.instanceId;
}
} else {
if (item.isSplit())
{
let splitItem = item as PedalboardSplitItem;
let t = this.deleteItem_(instanceId,splitItem.topChain);
if (t != null) return t;
t = this.deleteItem_(instanceId,splitItem.bottomChain);
if (t != null) return t;
}
}
}
return null;
}
canDeleteItem_(instanceId: number,items: PedalboardItem[]): boolean
{
for (let i = 0; i < items.length; ++i)
{
let item = items[i];
if (item.instanceId === instanceId)
{
if (items.length > 1) return true;
return !item.isEmpty(); // can delete if there's one non-empty item.
}
if (item.isSplit())
{
let splitItem = item as PedalboardSplitItem;
if (this.canDeleteItem_(instanceId,splitItem.topChain))
{
return true;
}
if (this.canDeleteItem_(instanceId,splitItem.bottomChain))
{
return true;
}
}
}
return false;
}
canDeleteItem(instanceId: number): boolean
{
return this.canDeleteItem_(instanceId,this.items);
}
// Returns the next selected instanceId, or null if no deletion occurred.
deleteItem(instanceId: number): number | null {
return this.deleteItem_(instanceId,this.items);
}
setMidiBinding(instanceId: number, midiBinding: MidiBinding): boolean
{
let item = this.getItem(instanceId);
if (!item) return false;
return item.setMidiBinding(midiBinding);
}
addToStart(item: PedalboardItem)
{
this.items.splice(0,0,item);
}
addToEnd(item: PedalboardItem)
{
this.items.splice(this.items.length,0,item);
}
static _addRelative(items: PedalboardItem[],newItem: PedalboardItem, instanceId: number, addBefore: boolean): boolean
{
for (let i = 0; i < items.length; ++i)
{
let item = items[i];
if (item.instanceId === instanceId)
{
if (addBefore)
{
items.splice(i,0,newItem);
} else {
items.splice(i+1,0,newItem);
}
return true;
}
if (item.isSplit())
{
let split = item as PedalboardSplitItem;
if (this._addRelative(split.topChain,newItem,instanceId,addBefore))
{
return true;
}
if (this._addRelative(split.bottomChain,newItem,instanceId,addBefore))
{
return true;
}
}
}
return false;
}
addBefore(item: PedalboardItem, instanceId: number)
{
if (item.instanceId === instanceId) return;
let result = Pedalboard._addRelative(this.items,item, instanceId, true);
if (!result) {
throw new PiPedalArgumentError("instanceId not found.");
}
}
addAfter(item: PedalboardItem, instanceId: number)
{
if (item.instanceId === instanceId) return;
let result = Pedalboard._addRelative(this.items,item, instanceId, false);
if (!result) {
throw new PiPedalArgumentError("instanceId not found.");
}
}
ensurePedalboardIds() {
if (this.nextInstanceId === -1) {
let minId = 1;
let it = this.itemsGenerator();
while (true) {
let v = it.next();
if (v.done) break;
if (v.value.instanceId) {
let t = v.value.instanceId;
if (t+1 > minId) minId = t+1;
}
}
this.nextInstanceId = minId;
}
let it = this.itemsGenerator();
while (true) {
let v = it.next();
if (v.done) break;
if (v.value.instanceId === -1) {
v.value.instanceId = ++this.nextInstanceId;
}
}
}
createEmptySplit(): PedalboardSplitItem {
let result: PedalboardSplitItem = new PedalboardSplitItem();
result.uri = SPLIT_PEDALBOARD_ITEM_URI;
result.instanceId = ++this.nextInstanceId;
result.pluginName = "";
result.isEnabled = true;
result.topChain = [ this.createEmptyItem()];
result.bottomChain = [ this.createEmptyItem()];
result.controlValues = [
new ControlValue(PedalboardSplitItem.TYPE_KEY, 0),
new ControlValue(PedalboardSplitItem.SELECT_KEY,0),
new ControlValue(PedalboardSplitItem.MIX_KEY,0),
new ControlValue(PedalboardSplitItem.PANL_KEY,0),
new ControlValue(PedalboardSplitItem.VOLL_KEY,0),
new ControlValue(PedalboardSplitItem.PANR_KEY,0),
new ControlValue(PedalboardSplitItem.VOLR_KEY,0)
];
return result;
}
createEmptyItem(): PedalboardItem {
let result: PedalboardItem = new PedalboardItem();
result.uri = EMPTY_PEDALBOARD_ITEM_URI;
result.instanceId = ++this.nextInstanceId;
result.pluginName = "";
result.isEnabled = true;
return result;
}
setItemEmpty(item: PedalboardItem)
{
item.uri = EMPTY_PEDALBOARD_ITEM_URI;
item.instanceId = ++this.nextInstanceId;
item.pluginName = "";
item.isEnabled = true;
item.controlValues = [];
item.vstState = "";
item.lv2State = [false,{}];
}
_replaceItem(items: PedalboardItem[], instanceId: number, newItem: PedalboardItem): boolean {
for (let i = 0; i < items.length; ++i)
{
let item = items[i];
if (item.instanceId === instanceId)
{
items[i] = newItem;
return true;
}
if (items[i].isSplit())
{
let splitItem = item as PedalboardSplitItem;
if (this._replaceItem(splitItem.topChain,instanceId,newItem))
return true;
if (this._replaceItem(splitItem.bottomChain,instanceId,newItem))
{
return true;
}
}
}
return false;
}
assignNewInstanceIds(items: PedalboardItem[])
{
for (let i = 0; i < items.length; ++i)
{
items[i].instanceId = ++this.nextInstanceId;
if (items[i].isSplit())
{
let splitItem = items[i] as PedalboardSplitItem;
this.assignNewInstanceIds(splitItem.topChain);
this.assignNewInstanceIds(splitItem.bottomChain);
}
}
}
replaceItem(instanceId: number, newItem: PedalboardItem)
{
newItem.instanceId = ++this.nextInstanceId;
let result = this._replaceItem(this.items,instanceId,newItem);
if (!result)
{
throw new PiPedalArgumentError("instanceId not found.");
}
if (newItem.isSplit())
{
// Generate new instance ids for all children, to avoid conflicts.
let splitItem = newItem as PedalboardSplitItem;
this.assignNewInstanceIds(splitItem.topChain);
this.assignNewInstanceIds(splitItem.bottomChain);
}
return newItem.instanceId;
}
private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean)
{
for (let i = 0; i < items.length; ++i)
{
let item = items[i];
if (item.instanceId === instanceId)
{
if (append)
{
items.splice(i+1,0,newItem);
return true;
} else {
items.splice(i,0,newItem);
return true;
}
}
if (item.isSplit())
{
let splitItem = item as PedalboardSplitItem;
if (this._addItem(splitItem.topChain,newItem,instanceId,append)) return true;
if (this._addItem(splitItem.bottomChain,newItem,instanceId,append)) return true;
}
}
}
addItem(newItem: PedalboardItem, instanceId: number, append: boolean): void
{
this._addItem(this.items,newItem,instanceId,append);
}
}
function* itemGenerator_(items: PedalboardItem[]): Generator<PedalboardItem, void, undefined> {
for (let i = 0; i < items.length; ++i) {
let item = items[i];
yield item;
if (item.uri === SPLIT_PEDALBOARD_ITEM_URI) {
let splitItem = item as PedalboardSplitItem;
let it = itemGenerator_(splitItem.topChain);
while (true) {
let v = it.next();
if (v.done) break;
yield v.value;
}
it = itemGenerator_(splitItem.bottomChain);
while (true) {
let v = it.next();
if (v.done) break;
yield v.value;
}
}
}
}
function* itemGeneratorSplitAfter_(items: PedalboardItem[]): Generator<PedalboardItem, void, undefined> {
for (let i = 0; i < items.length; ++i) {
let item = items[i];
if (item.uri === SPLIT_PEDALBOARD_ITEM_URI) {
let splitItem = item as PedalboardSplitItem;
let it = itemGenerator_(splitItem.topChain);
while (true) {
let v = it.next();
if (v.done) break;
yield v.value;
}
it = itemGenerator_(splitItem.bottomChain);
while (true) {
let v = it.next();
if (v.done) break;
yield v.value;
}
}
yield item;
}
}