590 lines
18 KiB
TypeScript
590 lines
18 KiB
TypeScript
// Copyright (c) 2021 Robin 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';
|
|
|
|
|
|
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> {
|
|
deserializePedalBoardItem(input: any): PedalBoardItem {
|
|
this.instanceId = input.instanceId ?? -1;
|
|
this.uri = input.uri;
|
|
this.pluginName = input.pluginName;
|
|
this.isEnabled = input.isEnabled;
|
|
this.midiBindings = MidiBinding.deserialize_array(input.midiBindings);
|
|
|
|
this.controlValues = ControlValue.deserializeArray(input.controlValues);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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.");
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
throw new PiPedalArgumentError("Control not found.");
|
|
}
|
|
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;
|
|
isEnabled: boolean = false;
|
|
uri: string = "";
|
|
pluginName?: string;
|
|
controlValues: ControlValue[] = ControlValue.EmptyArray;
|
|
midiBindings: MidiBinding[] = [];
|
|
};
|
|
|
|
|
|
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 class PedalBoard implements Deserializable<PedalBoard> {
|
|
|
|
deserialize(input: any): PedalBoard {
|
|
this.name = input.name;
|
|
this.items = PedalBoardItem.deserializeArray(input.items);
|
|
this.nextInstanceId = input.nextInstanceId ?? -1;
|
|
return this;
|
|
}
|
|
|
|
clone(): PedalBoard {
|
|
return new PedalBoard().deserialize(this);
|
|
}
|
|
name: string = "";
|
|
items: PedalBoardItem[] = [];
|
|
nextInstanceId: number = -1;
|
|
|
|
*itemsGenerator(): Generator<PedalBoardItem, void, undefined> {
|
|
let it = itemGenerator_(this.items);
|
|
while (true)
|
|
{
|
|
let v = it.next();
|
|
if (v.done) break;
|
|
yield v.value;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
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.");
|
|
}
|
|
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,-3),
|
|
new ControlValue(PedalBoardSplitItem.PANR_KEY,0),
|
|
new ControlValue(PedalBoardSplitItem.VOLR_KEY,-3)
|
|
];
|
|
|
|
|
|
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 = [];
|
|
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
replaceItem(instanceId: number, newItem: PedalBoardItem)
|
|
{
|
|
let result = this._replaceItem(this.items,instanceId,newItem);
|
|
if (!result)
|
|
{
|
|
throw new PiPedalArgumentError("instanceId not found.");
|
|
}
|
|
}
|
|
_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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|