Files
op-pedal/react/src/PiPedalModel.tsx
T
2023-07-03 07:02:59 -04:00

2590 lines
89 KiB
TypeScript

// Copyright (c) 2022 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 { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
import { ObservableProperty } from './ObservableProperty';
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
import PluginClass from './PluginClass';
import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket';
import { nullCast } from './Utility'
import { JackConfiguration, JackChannelSelection } from './Jack';
import { BankIndex } from './Banks';
import JackHostStatus from './JackHostStatus';
import JackServerSettings from './JackServerSettings';
import MidiBinding from './MidiBinding';
import { PluginUiPresets } from './PluginPreset';
import WifiConfigSettings from './WifiConfigSettings';
import WifiDirectConfigSettings from './WifiDirectConfigSettings';
import GovernorSettings from './GovernorSettings';
import WifiChannel from './WifiChannel';
import AlsaDeviceInfo from './AlsaDeviceInfo';
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
import isDarkMode, {setDarkMode} from './DarkMode';
export enum ColorTheme {
Light,
Dark
};
export enum State {
Loading,
Ready,
Error,
Background,
Reconnecting,
ApplyingChanges
};
export type ControlValueChangedHandler = (key: string, value: number) => void;
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
export interface PluginPresetsChangedHandle {
_id: number;
_handler: PluginPresetsChangedHandler;
};
export type StateChangedHandler = (instanceId: number) => void;
interface StateChangedEntry {
handle: number;
instanceId: number;
onStateChanged: StateChangedHandler;
};
export interface StateChangedHandle {
_handle: number;
}
export interface ZoomedControlInfo {
source: HTMLElement;
name: string;
instanceId: number;
uiControl: UiControl;
}
export interface VuUpdateInfo {
instanceId: number;
sampleTime: number; // in samples.
isStereoInput: boolean;
isStereoOutput: boolean;
inputMaxValueL: number;
inputMaxValueR: number;
outputMaxValueL: number;
outputMaxValueR: number;
};
export interface MonitorPortHandle {
};
export interface ControlValueChangedHandle {
_ControlValueChangedHandle: number;
};
interface ControlValueChangeItem {
handle: number;
instanceId: number;
onValueChanged: ControlValueChangedHandler;
};
class MidiEventListener {
constructor(handle: number, callback: (isNote: boolean, noteOrControl: number) => void) {
this.handle = handle;
this.callback = callback;
}
handle: number;
callback: (isNote: boolean, noteOrControl: number) => void;
};
export type PatchPropertyListener = (instanceId: number, propertyUri: string, atomObject: any) => void;
class PatchPropertyListenerItem {
constructor(handle: number, instanceId: number, callback: PatchPropertyListener) {
this.handle = handle;
this.instanceId = instanceId;
this.callback = callback;
}
handle: number;
instanceId: number;
callback: PatchPropertyListener;
};
export interface ListenHandle {
_handle: number;
}
class MonitorPortHandleImpl implements MonitorPortHandle {
constructor(instanceId: number, key: string, onUpdated: (value: number) => void) {
this.instanceId = instanceId;
this.key = key;
this.onUpdated = onUpdated;
}
valid: boolean = true;
subscriptionHandle?: number;
instanceId: number;
key: string;
onUpdated: (value: number) => void
};
export interface VuSubscriptionHandle {
};
class VuSubscriptionHandleImpl implements VuSubscriptionHandle {
constructor(instanceId_: number, callback_: VuChangedHandler) {
this.instanceId = instanceId_;
this.callback = callback_;
}
instanceId: number;
callback: VuChangedHandler;
};
export type VuChangedHandler = (vuInfo: VuUpdateInfo) => void;
class VuSubscriptionTarget {
serverSubscriptionHandle?: number;
subscribers: VuSubscriptionHandleImpl[] = [];
};
export type PiPedalVersion = {
server: string;
serverVersion: string;
operatingSystem: string;
osVersion: string;
debug: boolean;
webAddresses: string[];
};
export class PresetIndexEntry {
deserialize(input: any): PresetIndexEntry {
this.instanceId = input.instanceId;
this.name = input.name;
return this;
}
static deserialize_array(input: any): PresetIndexEntry[] {
let result: PresetIndexEntry[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new PresetIndexEntry().deserialize(input[i]);
}
return result;
}
instanceId: number = 0;
name: string = "";
areEqual(other: PresetIndexEntry): boolean {
return (
(this.instanceId === other.instanceId)
&& this.name === other.name
);
}
};
export class PresetIndex {
deserialize(input: any): PresetIndex {
this.selectedInstanceId = input.selectedInstanceId;
this.presetChanged = input.presetChanged;
this.presets = PresetIndexEntry.deserialize_array(input.presets);
return this;
}
selectedInstanceId: number = 0;
presetChanged: boolean = false;
presets: PresetIndexEntry[] = [];
clone(): PresetIndex {
return new PresetIndex().deserialize(this);
}
areEqual(other: PresetIndex, includeSelectedInstance: boolean): boolean {
if (includeSelectedInstance && this.selectedInstanceId !== other.selectedInstanceId) return false;
if (this.presets.length !== other.presets.length) return false;
for (let i = 0; i < this.presets.length; ++i) {
if (!this.presets[i].areEqual(other.presets[i])) return false;
}
return true;
}
getItem(instanceId: number): PresetIndexEntry | null {
for (let i = 0; i < this.presets.length; ++i) {
if (this.presets[i].instanceId === instanceId) {
return this.presets[i];
}
}
return null;
}
getSelectedText(): string {
let item = this.getItem(this.selectedInstanceId);
if (item === null) return "";
if (this.presetChanged) {
return item.name + " *";
} else {
return item.name;
}
}
addItem(instanceId: number, name: string): number {
let newItem: PresetIndexEntry = new PresetIndexEntry();
newItem.instanceId = instanceId;
newItem.name = name;
this.presets.push(newItem);
return newItem.instanceId;
}
movePreset(from: number, to: number) {
let newElements = this.presets;
let t = newElements[from];
newElements.splice(from, 1);
newElements.splice(to, 0, t);
}
deleteItem(instanceId: number): number {
for (let i = 0; i < this.presets.length; ++i) {
let preset = this.presets[i];
if (preset.instanceId === instanceId) {
this.presets.splice(i, 1);
if (i < this.presets.length) {
return this.presets[i].instanceId;
} else {
if (i - 1 > 0) {
return this.presets[i - 1].instanceId;
} else {
return -1;
}
}
}
}
return -1;
}
}
enum VisibilityState {
Visible,
Hidden
};
interface MonitorPortOutputBody {
subscriptionHandle: number;
value: number;
}
interface ChannelSelectionChangedBody {
clientId: number;
jackChannelSelection: JackChannelSelection;
}
interface RenamePresetBody {
clientId: number;
instanceId: number;
name: string;
}
interface PresetsChangedBody {
clientId: number,
presets: PresetIndex
}
interface PedalboardItemEnableBody {
clientId: number,
instanceId: number,
enabled: boolean
}
interface PedalboardChangedBody {
clientId: number;
pedalboard: Pedalboard;
}
interface ControlChangedBody {
clientId: number;
instanceId: number;
symbol: string;
value: number;
};
interface PatchPropertyChangedBody {
clientId: number;
instanceId: number;
propertyUri: string;
value: any;
};
interface Vst3ControlChangedBody {
clientId: number;
instanceId: number;
symbol: string;
value: number;
state: string;
};
export interface FavoritesList {
[url: string]: boolean;
}
export class PiPedalModel //implements PiPedalModel
{
clientId: number = -1;
serverVersion?: PiPedalVersion;
countryCodes: Object = {};
socketServerUrl: string = "";
varServerUrl: string = "";
lv2Path: string = "";
webSocket?: PiPedalSocket;
ui_plugins: ObservableProperty<UiPlugin[]>
= new ObservableProperty<UiPlugin[]>([]);
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
showStatusMonitor: ObservableProperty<boolean> = new ObservableProperty<boolean>(true);
pedalboard: ObservableProperty<Pedalboard> = new ObservableProperty<Pedalboard>(new Pedalboard());
plugin_classes: ObservableProperty<PluginClass> = new ObservableProperty<PluginClass>(new PluginClass());
jackConfiguration: ObservableProperty<JackConfiguration> = new ObservableProperty<JackConfiguration>(new JackConfiguration());
jackSettings: ObservableProperty<JackChannelSelection> = new ObservableProperty<JackChannelSelection>(new JackChannelSelection());
banks: ObservableProperty<BankIndex>
= new ObservableProperty<BankIndex>(new BankIndex());
jackServerSettings: ObservableProperty<JackServerSettings>
= new ObservableProperty<JackServerSettings>(new JackServerSettings());
wifiConfigSettings: ObservableProperty<WifiConfigSettings> = new ObservableProperty<WifiConfigSettings>(new WifiConfigSettings());
wifiDirectConfigSettings: ObservableProperty<WifiDirectConfigSettings> = new ObservableProperty<WifiDirectConfigSettings>(new WifiDirectConfigSettings());
governorSettings: ObservableProperty<GovernorSettings> = new ObservableProperty<GovernorSettings>(new GovernorSettings());
favorites: ObservableProperty<FavoritesList> = new ObservableProperty<FavoritesList>({});
presets: ObservableProperty<PresetIndex> = new ObservableProperty<PresetIndex>
(
new PresetIndex()
);
systemMidiBindings: ObservableProperty<MidiBinding[]> = new ObservableProperty<MidiBinding[]>
(
[
MidiBinding.systemBinding("prevProgram"),
MidiBinding.systemBinding("nextProgram")
]
);
zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined> = new ObservableProperty<ZoomedControlInfo | undefined>(undefined);
svgImgUrl(svgImage: string): string {
//return this.varServerUrl + "img/" + svgImage;
return "img/" + svgImage;
}
getUiPlugin(uri: string): UiPlugin | null {
let plugins: UiPlugin[] = this.ui_plugins.get();
for (let plugin of plugins) {
if (plugin.uri === uri) return plugin;
}
return null;
}
androidHost?: AndroidHostInterface;
constructor() {
this.androidHost = (window as any).AndroidHost as AndroidHostInterface;
this.onSocketError = this.onSocketError.bind(this);
this.onSocketMessage = this.onSocketMessage.bind(this);
this.onSocketReconnecting = this.onSocketReconnecting.bind(this);
this.onSocketReconnected = this.onSocketReconnected.bind(this);
this.onVisibilityChanged = this.onVisibilityChanged.bind(this);
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
}
onSocketReconnecting(retry: number, maxRetries: number): void {
if (this.visibilityState.get() === VisibilityState.Hidden) return;
//if (retry !== 0) {
if (this.restartExpected) {
this.setState(State.ApplyingChanges);
} else {
this.setState(State.Reconnecting);
}
//}
}
onSocketError(errorMessage: string, exception: any) {
if (this.visibilityState.get() === VisibilityState.Hidden) return;
this.onError(errorMessage);
}
compareFavorites(left: FavoritesList, right: FavoritesList): boolean {
for (let key of Object.keys(left)) {
if (!right[key]) {
return false;
}
}
for (let key of Object.keys(right)) {
if (!left[key]) {
return false;
}
}
return true;
}
onSocketMessage(header: PiPedalMessageHeader, body?: any) {
if (this.visibilityState.get() === VisibilityState.Hidden) return;
let message = header.message;
if (message === "onControlChanged") {
let controlChangedBody = body as ControlChangedBody;
this._setPedalboardControlValue(
controlChangedBody.instanceId,
controlChangedBody.symbol,
controlChangedBody.value,
false // do NOT notify the server of the change.
);
}
else if (message === "onOutputVolumeChanged") {
let value = body as number;
this._setOutputVolume(value,false);
}
else if (message === "onInputVolumeChanged") {
let value = body as number;
this._setInputVolume(value,false);
} else if (message === "onLv2StateChanged") {
let instanceId = body.instanceId as number;
let state = body.state as [boolean,any];
this.onLv2StateChanged(instanceId,state);
} else if (message === "onVst3ControlChanged") {
let controlChangedBody = body as Vst3ControlChangedBody;
this._setVst3PedalboardControlValue(
controlChangedBody.instanceId,
controlChangedBody.symbol,
controlChangedBody.value,
controlChangedBody.state,
false // do NOT notify the server of the change.
);
} else if (message === "onMonitorPortOutput") {
let monitorPortOutputBody = body as MonitorPortOutputBody;
for (let i = 0; i < this.monitorPortSubscriptions.length; ++i) {
let subscription = this.monitorPortSubscriptions[i];
if (subscription.subscriptionHandle === monitorPortOutputBody.subscriptionHandle) {
subscription.onUpdated(body.value);
break;
}
}
if (header.replyTo) {
this.webSocket?.reply(header.replyTo, "onMonitorPortOutput", true);
}
} else if (message === "onFavoritesChanged") {
let favorites = body as FavoritesList;
if (!this.compareFavorites(favorites, this.favorites.get())) {
this.favorites.set(favorites);
}
} else if (message === "onShowStatusMonitorChanged") {
let value = body as boolean;
this.showStatusMonitor.set(value);
} else if (message === "onChannelSelectionChanged") {
let channelSelectionBody = body as ChannelSelectionChangedBody;
let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection);
this.jackSettings.set(channelSelection);
} else if (message === "onPresetsChanged") {
let presetsChangedBody = body as PresetsChangedBody;
let presets = new PresetIndex().deserialize(presetsChangedBody.presets);
this.presets.set(presets);
} else if (message === "onPluginPresetsChanged") {
let pluginUri = body as string;
this.handlePluginPresetsChanged(pluginUri);
} else if (message === "onJackConfigurationChanged") {
this.jackConfiguration.set(new JackConfiguration().deserialize(body));
} else if (message === "onLoadPluginPreset") {
let instanceId = body.instanceId as number;
let controlValues = ControlValue.deserializeArray(body.controlValues);
this.handleOnLoadPluginPreset(instanceId, controlValues);
} else if (message === "onItemEnabledChanged") {
let itemEnabledBody = body as PedalboardItemEnableBody;
this._setPedalboardItemEnabled(
itemEnabledBody.instanceId,
itemEnabledBody.enabled,
false // No server notification.
);
} else if (message === "onPedalboardChanged") {
let pedalChangedBody = body as PedalboardChangedBody;
this.pedalboard.set(new Pedalboard().deserialize(pedalChangedBody.pedalboard));
} else if (message === "onMidiValueChanged") {
let controlChangedBody = body as ControlChangedBody;
this._setPedalboardControlValue(
controlChangedBody.instanceId,
controlChangedBody.symbol,
controlChangedBody.value,
false // do NOT notify the server of the change.
);
if (header.replyTo) {
this.webSocket?.reply(header.replyTo, "onMidiValueChanged", true);
}
} else if (message === "onNotifyMidiListener") {
let clientHandle = body.clientHandle as number;
let isNote = body.isNote as boolean;
let noteOrControl = body.noteOrControl as number;
this.handleNotifyMidiListener(clientHandle, isNote, noteOrControl);
} else if (message === "onNotifyPatchProperty") {
let clientHandle = body.clientHandle as number;
let instanceId = body.instanceId as number;
let propertyUri = body.propertyUri as string;
let atomJson = body.atomJson as any;
this.handleNotifyPatchProperty(clientHandle, instanceId, propertyUri, atomJson);
if (header.replyTo) {
this.webSocket?.reply(header.replyTo, "onNotifyPatchProperty", true);
}
} else if (message === "onJackServerSettingsChanged") {
let jackServerSettings = new JackServerSettings().deserialize(body);
this.jackServerSettings.set(jackServerSettings);
} else if (message === "onWifiConfigSettingsChanged") {
let wifiConfigSettings = new WifiConfigSettings().deserialize(body);
this.wifiConfigSettings.set(wifiConfigSettings);
} else if (message === "onWifiDirectConfigSettingsChanged") {
let wifiDirectConfigSettings = new WifiDirectConfigSettings().deserialize(body);
this.wifiDirectConfigSettings.set(wifiDirectConfigSettings);
}
else if (message === "onGovernorSettingsChanged") {
let governor = body as string;
let newSettings = this.governorSettings.get().clone();
newSettings.governor = governor;
this.governorSettings.set(newSettings);
} else if (message === "onBanksChanged") {
let banks = new BankIndex().deserialize(body);
this.banks.set(banks);
} else if (message === "onVuUpdate") {
let vuUpdate = body as VuUpdateInfo;
let item = this.vuSubscriptions[vuUpdate.instanceId];
if (item) {
for (let i = 0; i < item.subscribers.length; ++i) {
item.subscribers[i].callback(vuUpdate);
}
}
if (header.replyTo) {
this.webSocket?.reply(header.replyTo, "onVuUpdate", true);
}
} else if (message === "onSystemMidiBindingsChanged") {
let bindings = MidiBinding.deserialize_array(body);
this.systemMidiBindings.set(bindings);
} else if (message === "onErrorMessage")
{
this.showAlert(body as string);
} else {
throw new PiPedalStateError("Unrecognized message received from server: " + message);
}
}
setError(message: string): void {
this.errorMessage.set(message);
this.setState(State.Error);
}
varRequest(url: string): string {
return "var/" + url;
}
setState(state: State) {
if (this.state.get() !== state) {
this.state.set(state);
}
}
validatePlugins(plugin: PluginClass) {
if (plugin.plugin_type === PluginType.None) {
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
}
for (let i = 0; i < plugin.children.length; ++i) {
this.validatePlugins(plugin.children[i]);
}
}
requestPluginClasses(): Promise<boolean> {
const myRequest = new Request(this.varRequest('plugin_classes.json'));
return fetch(myRequest)
.then(
response => response.json()
)
.then(data => {
let plugins = new PluginClass().deserialize(data);
this.validatePlugins(plugins);
this.plugin_classes.set(plugins);
return true;
})
.catch((error) => {
this.setError("Can't contact server.\n\n" + error);
return false;
});
}
getWebSocket(): PiPedalSocket {
if (this.webSocket === undefined) {
throw new PiPedalStateError("Attempt to access web socket before it's connected.");
}
return this.webSocket;
}
onSocketConnectionLost() {
if (this.isAndroidHosted()) {
this.androidHost?.setDisconnected(true);
}
}
onSocketReconnected() {
if (this.isAndroidHosted()) {
this.androidHost?.setDisconnected(false);
}
this.restartExpected = false;
if (this.visibilityState.get() === VisibilityState.Hidden) return;
// reload state, but not configuration.
this.getWebSocket().request<number>("hello")
.then(clientId => {
this.clientId = clientId;
return this.getWebSocket().request<any>("plugins");
})
.then(data => {
this.ui_plugins.set(UiPlugin.deserialize_array(data));
return this.getWebSocket().request<Pedalboard>("currentPedalboard");
})
.then(data => {
this.pedalboard.set(new Pedalboard().deserialize(data));
return this.getWebSocket().request<boolean>("getShowStatusMonitor");
})
.then(data => {
this.showStatusMonitor.set(data);
return this.getWebSocket().request<any>("getWifiConfigSettings");
})
.then(data => {
this.wifiConfigSettings.set(new WifiConfigSettings().deserialize(data));
return this.getWebSocket().request<any>("getWifiDirectConfigSettings");
})
.then(data => {
this.wifiDirectConfigSettings.set(new WifiDirectConfigSettings().deserialize(data));
return this.getWebSocket().request<any>("getGovernorSettings");
})
.then(data => {
this.governorSettings.set(new GovernorSettings().deserialize(data));
return this.getWebSocket().request<any>("getJackServerSettings");
})
.then(data => {
this.jackServerSettings.set(new JackServerSettings().deserialize(data));
return this.getWebSocket().request<PresetIndex>("getPresets");
})
.then((data) => {
this.presets.set(new PresetIndex().deserialize(data));
return this.getWebSocket().request<JackConfiguration>("getJackConfiguration");
})
.then((data) => {
// data.isValid = false;
// data.errorState = "Jack Audio server not running."
this.jackConfiguration.set(new JackConfiguration().deserialize(data));
return this.getWebSocket().request<any>("getJackSettings");
})
.then((data) => {
this.jackSettings.set(new JackChannelSelection().deserialize(data));
return this.getWebSocket().request<any>("getBankIndex");
})
.then((data) => {
this.banks.set(new BankIndex().deserialize(data));
return this.getWebSocket().request<FavoritesList>("getFavorites");
})
.then((data) => {
this.favorites.set(data);
return this.getWebSocket().request<MidiBinding[]>("getSystemMidiBindings");
})
.then((data) => {
let bindings = MidiBinding.deserialize_array(data);
this.systemMidiBindings.set(bindings);
this.setState(State.Ready);
})
.catch((what) => {
this.onError(what);
})
}
makeSocketServerUrl(hostName: string, port: number): string {
return "ws://" + hostName + ":" + port + "/pipedal";
}
makeVarServerUrl(protocol: string, hostName: string, port: number): string {
return protocol + "://" + hostName + ":" + port + "/var/";
}
maxFileUploadSize: number = 100000000;
maxPresetUploadSize: number = 512 * 1024;
debug: boolean = false;
requestConfig(): Promise<boolean> {
const myRequest = new Request(this.varRequest('config.json'));
return fetch(myRequest)
.then(
(response) => {
return response.json();
}
)
.then(data => {
if (data.max_upload_size) {
this.maxPresetUploadSize = data.max_upload_size;
}
if (data.fakeAndroid) {
this.androidHost = new FakeAndroidHost();
}
this.debug = !!data.debug;
let { socket_server_port, socket_server_address } = data;
if ((!socket_server_address) || socket_server_address === "*") {
socket_server_address = window.location.hostname;
}
if (!socket_server_port) socket_server_port = 8080;
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
this.socketServerUrl = socket_server;
this.varServerUrl = var_server_url;
this.webSocket = new PiPedalSocket(
this.socketServerUrl,
{
onMessageReceived: this.onSocketMessage,
onError: this.onSocketError,
onConnectionLost: this.onSocketConnectionLost,
onReconnect: this.onSocketReconnected,
onReconnecting: this.onSocketReconnecting
}
);
return this.webSocket.connect();
})
.catch((error) => {
this.setError("Failed to connect to server.");
return false;
})
.then(() => {
const isoRequest = new Request('iso_codes.json');
return fetch(isoRequest);
})
.then((response) => {
return response.json();
})
.then((countryCodes) => {
this.countryCodes = countryCodes as Object;
return this.getWebSocket().request<number>("hello");
})
.then((clientId) => {
this.clientId = clientId;
return this.getWebSocket().request<string>("imageList");
})
.then((data) => {
this.preloadImages(data);
return true;
})
.catch((error) => {
this.setError("Failed to connect to server. " + error);
return false;
})
.then((succeeded) => {
if (!succeeded) return false;
return this.getWebSocket().request<PiPedalVersion>("version").then(data => {
this.serverVersion = data;
return this.getWebSocket().request<any>("plugins");
})
.then(data => {
this.ui_plugins.set(UiPlugin.deserialize_array(data));
return this.getWebSocket().request<Pedalboard>("currentPedalboard");
})
.then(data => {
this.pedalboard.set(new Pedalboard().deserialize(data));
return this.getWebSocket().request<any>("pluginClasses");
})
.then((data) => {
this.plugin_classes.set(new PluginClass().deserialize(data));
this.validatePlugins(this.plugin_classes.get());
return this.getWebSocket().request<PresetIndex>("getPresets");
})
.then((data) => {
this.presets.set(new PresetIndex().deserialize(data));
return this.getWebSocket().request<any>("getWifiConfigSettings");
})
.then(data => {
this.wifiConfigSettings.set(new WifiConfigSettings().deserialize(data));
return this.getWebSocket().request<any>("getWifiDirectConfigSettings");
})
.then(data => {
this.wifiDirectConfigSettings.set(new WifiDirectConfigSettings().deserialize(data));
return this.getWebSocket().request<any>("getGovernorSettings");
})
.then(data => {
this.governorSettings.set(new GovernorSettings().deserialize(data));
return this.getWebSocket().request<boolean>("getShowStatusMonitor");
})
.then(data => {
this.showStatusMonitor.set(data);
return this.getWebSocket().request<any>("getJackServerSettings");
})
.then(data => {
this.jackServerSettings.set(new JackServerSettings().deserialize(data));
return this.getWebSocket().request<JackConfiguration>("getJackConfiguration");
})
.then((data) => {
// data.isValid = false;
// data.errorState = "Jack Audio server not running."
this.jackConfiguration.set(new JackConfiguration().deserialize(data));
return this.getWebSocket().request<any>("getJackSettings");
})
.then((data) => {
this.jackSettings.set(new JackChannelSelection().deserialize(data));
return this.getWebSocket().request<any>("getBankIndex");
})
.then((data) => {
this.banks.set(new BankIndex().deserialize(data));
if (this.webSocket) {
// MUST not allow reconnect until at least one complete load has finished.
this.webSocket.canReconnect = true;
}
return this.getWebSocket().request<FavoritesList>("getFavorites");
})
.then((data) => {
this.favorites.set(data);
return this.getWebSocket().request<MidiBinding[]>("getSystemMidiBindings");
})
.then((data) => {
let bindings = MidiBinding.deserialize_array(data);
this.systemMidiBindings.set(bindings);
if (this.webSocket) {
// MUST not allow reconnect until at least one complete load has finished.
this.webSocket.canReconnect = true;
}
return true;
})
.catch((error) => {
this.setError("Failed to fetch server state.\n\n" + error);
return false;
});
})
;
}
requestCurrentPedalboard(): Promise<void> {
const myRequest = new Request(this.varRequest('current_pedalboard.json'));
return fetch(myRequest)
.then(
(response) => {
return response.json();
}
)
.then(data => {
let pedalboard = new Pedalboard().deserialize(data);
pedalboard.ensurePedalboardIds();
this.pedalboard.set(pedalboard);
});
}
onError(msg: string): void {
this.errorMessage.set(msg);
this.state.set(State.Error);
}
backgroundStateTimeout?: NodeJS.Timeout = undefined;
exitBackgroundState() {
if (this.backgroundStateTimeout) {
clearTimeout(this.backgroundStateTimeout);
this.backgroundStateTimeout = undefined;
return;
}
if (this.state.get() === State.Background) {
console.log("Exiting background state.");
this.visibilityState.set(VisibilityState.Visible);
this.webSocket?.exitBackgroundState();
}
}
enterBackgroundState() {
// on Android, delay entering background state by 180 seconds,
// since background management is more complicated. e.g. screen flips, and system upload dialogs.
if (this.isAndroidHosted()) {
if (this.backgroundStateTimeout) {
clearTimeout(this.backgroundStateTimeout);
}
this.backgroundStateTimeout = setTimeout(() => {
this.backgroundStateTimeout = undefined;
this.enterBackgroundState_();
}, 180000);
} else {
this.enterBackgroundState_();
}
}
enterBackgroundState_() {
if (this.state.get() !== State.Background) {
console.log("Entering background state.");
this.visibilityState.set(VisibilityState.Hidden);
this.setState(State.Background);
this.webSocket?.enterBackgroundState();
}
}
onVisibilityChanged(doc: Document, event: Event): any {
if (document.visibilityState) {
switch (document.visibilityState) {
case "visible":
this.visibilityState.set(VisibilityState.Visible);
this.exitBackgroundState();
break;
case "hidden":
this.visibilityState.set(VisibilityState.Hidden);
this.enterBackgroundState();
break;
}
}
return false;
}
initialize(): void {
this.setError("");
this.setState(State.Loading);
this.requestConfig()
.then((succeeded) => {
if (succeeded) {
this.state.set(State.Ready);
}
})
.catch((error) => {
this.setError("Failed to get server state. \n\n" + error);
});
let t = this.onVisibilityChanged;
(document as any).addEventListener("visibilitychange", (doc: Document, event: Event) => {
return t(doc, event);
});
}
getControl(uiPlugin: UiPlugin, portSymbol: string): UiControl | null {
for (let i = 0; i < uiPlugin.controls.length; ++i) {
let control = uiPlugin.controls[i];
if (control.symbol === portSymbol) {
return control;
}
}
return null;
}
getDefaultValues(uri: string): ControlValue[] {
let uiPlugin = this.getUiPlugin(uri);
if (uiPlugin === null) throw new PiPedalArgumentError("Pedal uri not found.");
let result: ControlValue[] = [];
let controls = uiPlugin.controls;
for (let i = 0; i < controls.length; ++i) {
let control = controls[i];
let uiControl = this.getControl(uiPlugin, control.symbol);
if (uiControl === null) {
// this is default values, so a mis-match is in fact a problem.
throw new PiPedalStateError("ui and pedealboard ports don't match.");
}
let cv = new ControlValue();
cv.key = control.symbol;
cv.value = control.default_value;
result.push(cv);
}
return result;
}
handleOnLoadPluginPreset(instanceId: number, controlValues: ControlValue[]) {
// note that plugins with state are dealt with server-side.
// if we made it here, we can just load the controls.
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
let item = newPedalboard.getItem(instanceId);
let changed = false;
for (let i = 0; i < controlValues.length; ++i) {
let controlValue = controlValues[i];
changed = item.setControlValue(controlValue.key, controlValue.value) || changed;
}
if (changed) {
this.pedalboard.set(newPedalboard);
}
}
private _controlValueChangeItems: ControlValueChangeItem[] = [];
addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle {
let handle = ++this.nextListenHandle;
this._controlValueChangeItems.push({ handle: handle, instanceId: instanceId, onValueChanged: onValueChanged });
return { _ControlValueChangedHandle: handle };
}
removeControlValueChangeListener(handle: ControlValueChangedHandle) {
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
if (this._controlValueChangeItems[i].handle === handle._ControlValueChangedHandle) {
this._controlValueChangeItems.splice(i, 1);
return;
}
}
}
private stateChangedListeners: StateChangedEntry[] = [];
addLv2StateChangedListener(instanceId: number, onStateChanged: StateChangedHandler): StateChangedHandle {
let handle = ++this.nextListenHandle;
let item: StateChangedEntry = {
handle: handle,
instanceId: instanceId,
onStateChanged: onStateChanged
};
this.stateChangedListeners.push(item);
return { _handle: handle };
}
removeLv2StateChangedListener(handle: StateChangedHandle): void {
let h = handle._handle;
for (let i = 0; i < this.stateChangedListeners.length; ++i) {
if (this.stateChangedListeners[i].handle === h) {
this.stateChangedListeners.splice(i, 1);
}
}
}
private onLv2StateChanged(instanceId: number, state: [boolean,any]): void {
let item = this.pedalboard.get().getItem(instanceId);
item.lv2State = state;
for (let item of this.stateChangedListeners) {
if (item.instanceId === instanceId) {
item.onStateChanged(instanceId);
}
}
}
private _pluginPresetsChangedHandles: PluginPresetsChangedHandle[] = [];
addPluginPresetsChangedListener(onPluginPresetsChanged: PluginPresetsChangedHandler): PluginPresetsChangedHandle {
let handle = ++this.nextListenHandle;
let t: PluginPresetsChangedHandle = {
_id: handle,
_handler: onPluginPresetsChanged
};
this._pluginPresetsChangedHandles.push(t);
return t;
}
removePluginPresetsChangedListener(handle: PluginPresetsChangedHandle): void {
let pos = -1;
for (let i = 0; i < this._pluginPresetsChangedHandles.length; ++i) {
if (this._pluginPresetsChangedHandles[i]._id === handle._id) {
pos = i;
break;
}
}
if (pos !== -1) {
this._pluginPresetsChangedHandles.splice(pos, 1);
}
}
firePluginPresetsChanged(pluginUri: string): void {
for (let i = 0; i < this._pluginPresetsChangedHandles.length; ++i) {
this._pluginPresetsChangedHandles[i]._handler(pluginUri);
}
}
private _setPedalboardPropertyValue(instanceId: number, propertyUri: string, value: any): void {
let body: PatchPropertyChangedBody = {
clientId: this.clientId,
instanceId: instanceId,
propertyUri: propertyUri,
value: value
};
this.webSocket?.send("setPatchProperty", body);
}
setInputVolume(volume_db: number) : void {
this._setInputVolume(volume_db,true);
}
previewInputVolume(volume_db: number) : void {
nullCast(this.webSocket).send("previewInputVolume",volume_db);
}
previewOutputVolume(volume_db: number) : void {
nullCast(this.webSocket).send("previewOutputVolume",volume_db);
}
private _setInputVolume(volume_db: number, notifyServer: boolean) : void {
let changed: boolean = false;
let pedalboard = this.pedalboard.get();
if (pedalboard.input_volume_db !== volume_db)
{
let newPedalboard = pedalboard.clone();
newPedalboard.input_volume_db = volume_db;
this.pedalboard.set(newPedalboard);
changed = true;
}
if (changed)
{
if (notifyServer) {
nullCast(this.webSocket).send("setInputVolume",volume_db);
}
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (Pedalboard.START_CONTROL === item.instanceId) {
item.onValueChanged("volume_db", volume_db);
}
}
}
}
setOutputVolume(volume_db: number, notifyServer: boolean) : void
{
this._setOutputVolume(volume_db,true);
}
private _setOutputVolume(volume_db: number, notifyServer: boolean) : void {
let changed: boolean = false;
let pedalboard = this.pedalboard.get();
if (pedalboard.output_volume_db !== volume_db)
{
let newPedalboard = pedalboard.clone();
newPedalboard.output_volume_db = volume_db;
this.pedalboard.set(newPedalboard);
changed = true;
}
if (changed)
{
if (notifyServer) {
nullCast(this.webSocket).send("setOutputVolume",volume_db);
}
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (Pedalboard.END_CONTROL === item.instanceId) {
item.onValueChanged("volume_db", volume_db);
}
}
}
}
private _setPedalboardControlValue(instanceId: number, key: string, value: number, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
{
this._setInputVolume(value,notifyServer);
return;
} else if (instanceId === Pedalboard.END_CONTROL)
{
this._setOutputVolume(value,notifyServer);
return;
}
let item = newPedalboard.getItem(instanceId);
let changed = item.setControlValue(key, value);
if (changed) {
this.pedalboard.set(newPedalboard);
if (notifyServer) {
this._setServerControl("setControl", instanceId, key, value);
}
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (instanceId === item.instanceId) {
item.onValueChanged(key, value);
}
}
}
}
private _setVst3PedalboardControlValue(instanceId: number, key: string, value: number, state: string, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
let item: PedalboardItem = newPedalboard.getItem(instanceId);
let changed = item.setControlValue(key, value);
item.vstState = state;
if (changed) {
this.pedalboard.set(newPedalboard);
if (notifyServer) {
this._setServerControl("setControl", instanceId, key, value);
}
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (instanceId === item.instanceId) {
item.onValueChanged(key, value);
}
}
}
}
setPedalboardControl(instanceId: number, key: string, value: number): void {
this._setPedalboardControlValue(instanceId, key, value, true);
}
setPedalboardItemEnabled(instanceId: number, value: boolean): void {
this._setPedalboardItemEnabled(instanceId, value, true);
}
private _setPedalboardItemEnabled(instanceId: number, value: boolean, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
let item = newPedalboard.getItem(instanceId);
let changed = value !== item.isEnabled;
if (changed) {
item.isEnabled = value;
this.pedalboard.set(newPedalboard);
if (notifyServer) {
let body: PedalboardItemEnableBody = {
clientId: this.clientId,
instanceId: instanceId,
enabled: value
}
this.webSocket?.send("setPedalboardItemEnable", body);
}
}
}
updateServerPedalboard(): void {
let body: PedalboardChangedBody = {
clientId: this.clientId,
pedalboard: this.pedalboard.get()
};
this.webSocket?.send("updateCurrentPedalboard", body);
}
setShowStatusMonitor(show: boolean): void {
this.webSocket?.send("setShowStatusMonitor", show);
}
loadPedalboardPlugin(itemId: number, selectedUri: string): number {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalArgumentError("Can't clone an undefined object.");
let newPedalboard = pedalboard.clone();
let plugin = this.getUiPlugin(selectedUri);
if (plugin === null) {
throw new PiPedalArgumentError("Plugin not found.");
}
let it = newPedalboard.itemsGenerator();
while (true) {
let v = it.next();
if (v.done) break;
let item = v.value;
if (item.instanceId === itemId) {
item.deserialize(new PedalboardItem()); // skeezy way to re-initialize.
item.instanceId = ++newPedalboard.nextInstanceId;
item.uri = selectedUri;
item.pluginName = plugin.name;
item.controlValues = this.getDefaultValues(item.uri);
item.isEnabled = true;
// lv2State: not valid. vstState : not valid.
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard()
return item.instanceId;
}
}
throw new PiPedalArgumentError("Pedalboard item not found.");
}
private _setServerControl(message: string, instanceId: number, key: string, value: number) {
let body: ControlChangedBody = {
clientId: this.clientId,
instanceId: instanceId,
symbol: key,
value: value
};
this.webSocket?.send(message, body);
}
previewPedalboardValue(instanceId: number, key: string, value: number): void {
// mouse is down. Don't update EVERYBODY, but we must change
// the control on the running audio plugin.
// TODO: respect "expensive" port attribute.
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
{
this.previewInputVolume(value);
return;
} else if (instanceId === Pedalboard.END_CONTROL)
{
this.previewOutputVolume(value);
return;
}
this._setServerControl("previewControl", instanceId, key, value);
}
// returns the next selected instanceId, or null,if no item was deleted.
deletePedalboardPedal(instanceId: number): number | null {
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let result = newPedalboard.deleteItem(instanceId);
if (result !== null) {
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
return result;
}
movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void {
if (fromInstanceId === toInstanceId) return;
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let fromItem = newPedalboard.getItem(fromInstanceId);
if (fromItem === null) {
throw new PiPedalArgumentError("fromInstanceId not found.");
}
newPedalboard.deleteItem(fromInstanceId);
newPedalboard.addBefore(fromItem, toInstanceId);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
movePedalboardItemAfter(fromInstanceId: number, toInstanceId: number): void {
if (fromInstanceId === toInstanceId) return;
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let fromItem = newPedalboard.getItem(fromInstanceId);
if (fromItem === null) {
throw new PiPedalArgumentError("fromInstanceId not found.");
}
newPedalboard.deleteItem(fromInstanceId);
newPedalboard.addAfter(fromItem, toInstanceId);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
movePedalboardItemToStart(instanceId: number): void {
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let fromItem = newPedalboard.getItem(instanceId);
if (fromItem === null) {
throw new PiPedalArgumentError("fromInstanceId not found.");
}
newPedalboard.deleteItem(instanceId);
newPedalboard.addToStart(fromItem);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
movePedalboardItemToEnd(instanceId: number): void {
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let fromItem = newPedalboard.getItem(instanceId);
if (fromItem === null) {
throw new PiPedalArgumentError("fromInstanceId not found.");
}
newPedalboard.deleteItem(instanceId);
newPedalboard.addToEnd(fromItem);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
movePedalboardItem(fromInstanceId: number, toInstanceId: number): void {
if (fromInstanceId === toInstanceId) return;
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let fromItem = newPedalboard.getItem(fromInstanceId);
let toItem = newPedalboard.getItem(toInstanceId);
if (fromItem === null) {
throw new PiPedalArgumentError("fromInstanceId not found.");
}
if (toItem === null) {
throw new PiPedalArgumentError("toInstanceId not found.");
}
let emptyItem = newPedalboard.createEmptyItem();
newPedalboard.replaceItem(fromInstanceId, emptyItem);
newPedalboard.replaceItem(toInstanceId, fromItem);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
}
addPedalboardItem(instanceId: number, append: boolean): number {
let pedalboard = this.pedalboard.get();
if (instanceId === Pedalboard.START_CONTROL && append)
{
instanceId = pedalboard.items[0].instanceId;
append = false;
} else if (instanceId === Pedalboard.END_CONTROL && !append)
{
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
append = true;
}
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let item = newPedalboard.getItem(instanceId);
if (item === null) {
throw new PiPedalArgumentError("instanceId not found.");
}
let newItem = newPedalboard.createEmptyItem();
newPedalboard.addItem(newItem, instanceId, append);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
return newItem.instanceId;
}
addPedalboardSplitItem(instanceId: number, append: boolean): number {
let pedalboard = this.pedalboard.get();
if (instanceId === Pedalboard.START_CONTROL && append)
{
instanceId = pedalboard.items[0].instanceId;
append = false;
} else if (instanceId === Pedalboard.END_CONTROL && !append)
{
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
append = true;
}
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let item = newPedalboard.getItem(instanceId);
if (item === null) {
throw new PiPedalArgumentError("instanceId not found.");
}
let newItem = newPedalboard.createEmptySplit();
newPedalboard.addItem(newItem, instanceId, append);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
return newItem.instanceId;
}
setPedalboardItemEmpty(instanceId: number): number {
let pedalboard = this.pedalboard.get();
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
let item = newPedalboard.getItem(instanceId);
if (item === null) {
throw new PiPedalArgumentError("instanceId not found.");
}
newPedalboard.setItemEmpty(item);
this.pedalboard.set(newPedalboard);
this.updateServerPedalboard();
return item.instanceId;
}
saveCurrentPreset(): void {
this.webSocket?.send("saveCurrentPreset", this.clientId);
}
isOnboarding(): boolean {
var settings = this.jackServerSettings.get();
return settings.isOnboarding;
}
setOnboarding(value: boolean): void {
nullCast(this.webSocket)
.request("setOnboarding", value);
}
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
// default behaviour is to save after the currently selected preset.
if (saveAfterInstanceId === -1) {
saveAfterInstanceId = this.presets.get().selectedInstanceId;
}
let request: any = {
clientId: this.clientId,
name: newName,
saveAfterInstanceId: saveAfterInstanceId
};
return nullCast(this.webSocket)
.request<number>("saveCurrentPresetAs", request)
.then((newPresetId) => {
this.loadPreset(newPresetId);
return newPresetId;
});
}
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
return nullCast(this.webSocket)
.request<string[]>('requestFileList', piPedalFileProperty);
}
deleteUserFile(fileName: string) : Promise<boolean>
{
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName)
}
saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): Promise<number> {
// default behaviour is to save after the currently selected preset.
let request: any = {
instanceId: pluginInstanceId,
name: newName,
};
return nullCast(this.webSocket)
.request<number>("savePluginPresetAs", request)
.then((data) => {
return data;
});
}
loadPreset(instanceId: number): void {
this.webSocket?.send("loadPreset", instanceId);
}
updatePresets(presets: PresetIndex): Promise<void> {
return nullCast(this.webSocket).request<void>("updatePresets", presets);
}
moveBank(from: number, to: number): Promise<void> {
return nullCast(this.webSocket).request<void>("moveBank", { from: from, to: to });
}
deletePresetItem(instanceId: number): Promise<number> {
return nullCast(this.webSocket).request<number>("deletePresetItem", instanceId);
}
deleteBankItem(instanceId: number): Promise<number> {
return nullCast(this.webSocket).request<number>("deleteBankItem", instanceId);
}
renamePresetItem(instanceId: number, name: string): Promise<void> {
let body: RenamePresetBody = {
clientId: this.clientId,
instanceId: instanceId,
name: name
};
return nullCast(this.webSocket).request<void>("renamePresetItem", body);
}
copyPreset(fromId: number, toId: number = -1): Promise<number> {
let body: any = {
clientId: this.clientId,
fromId: fromId,
toId: toId
};
return nullCast(this.webSocket).request<number>("copyPreset", body);
}
duplicatePreset(instanceId: number): Promise<number> {
return this.copyPreset(instanceId, -1);
}
showAlert(message: string): void {
this.alertMessage.set(message);
}
restartExpected: boolean = false;
expectRestart() {
this.restartExpected = true;
}
setJackSettings(jackSettings: JackChannelSelection): void {
this.expectRestart();
this.webSocket?.send("setJackSettings", jackSettings);
}
monitorPortSubscriptions: MonitorPortHandleImpl[] = [];
monitorPort(instanceId: number, key: string, updateRateSeconds: number, onUpdated: (value: number) => void): MonitorPortHandle {
let result = new MonitorPortHandleImpl(instanceId, key, onUpdated);
this.monitorPortSubscriptions.push(result);
if (!this.webSocket) return result;
this.webSocket.request<number>("monitorPort",
{
instanceId: instanceId,
key: key,
updateRate: updateRateSeconds
})
.then((handle) => {
if (result.valid) {
result.subscriptionHandle = handle;
} else {
try {
this.webSocket?.send("unmonitorPort", handle);
} catch (err) {
}
}
});
return result;
}
unmonitorPort(handle: MonitorPortHandle): void {
let t = handle as MonitorPortHandleImpl;
for (let i = 0; i < this.monitorPortSubscriptions.length; ++i) {
let item = this.monitorPortSubscriptions[i];
if (item === t) {
this.monitorPortSubscriptions.splice(i, 1);
item.valid = false;
if (item.subscriptionHandle) {
try {
this.webSocket?.send("unmonitorPort", item.subscriptionHandle);
} catch (err) {
}
}
break;
}
}
}
vuSubscriptions: (VuSubscriptionTarget | undefined)[] = [];
addVuSubscription(instanceId: number, vuChangedHandler: VuChangedHandler): VuSubscriptionHandle {
let result = new VuSubscriptionHandleImpl(instanceId, vuChangedHandler);
if (!this.webSocket) return result; // racing to death. don't subscribe.
let item = this.vuSubscriptions[instanceId];
if (item) {
item.subscribers.push(result);
} else {
let newTarget = new VuSubscriptionTarget();
newTarget.subscribers.push(result);
this.webSocket.request<number>("addVuSubscription", instanceId)
.then((subscriptionHandle) => {
newTarget.serverSubscriptionHandle = subscriptionHandle;
if (newTarget.subscribers.length === 0) {
this.webSocket?.send("removeVuSubscription", subscriptionHandle);
} else {
newTarget.serverSubscriptionHandle = subscriptionHandle;
}
});
this.vuSubscriptions[instanceId] = newTarget;
}
return result;
}
removeVuSubscription(handle: VuSubscriptionHandle): void {
let handleImpl = handle as VuSubscriptionHandleImpl;
let item = this.vuSubscriptions[handleImpl.instanceId];
if (item) {
for (let i = 0; i < item.subscribers.length; ++i) {
if (item.subscribers[i] === handleImpl) {
item.subscribers.splice(i, 1);
if (item.subscribers.length === 0) {
this.vuSubscriptions[handleImpl.instanceId] = undefined;
if (item.serverSubscriptionHandle) {
this.webSocket?.send("removeVuSubscription", item.serverSubscriptionHandle);
item.serverSubscriptionHandle = undefined;
}
}
}
}
}
}
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
let result = new Promise<boolean>((resolve, reject) => {
if (this.webSocket) {
this.webSocket.request<boolean>(
"setPatchProperty",
{ instanceId: instanceId, propertyUri: uri, value: value }
).then((result) => {
resolve(true);
}).catch((message) => {
reject(message);
});
}
else {
reject("Socket closed.");
}
});
return result;
}
getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type> {
let result = new Promise<Type>((resolve, reject) => {
if (!this.webSocket) {
reject("Socket closed.");
} else {
this.webSocket.request<Type>("getPatchProperty", { instanceId: instanceId, propertyUri: uri })
.then((data) => {
resolve(data);
})
.catch((error) => {
reject(error + " (" + uri + ")");
});
}
});
return result;
}
openBank(bankId: number): Promise<void> {
let result = new Promise<void>((resolve, reject) => {
if (this.webSocket) {
this.webSocket.request<void>("openBank", bankId)
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
}
});
return result;
}
renameBank(bankId: number, newName: string): Promise<void> {
let result = new Promise<void>((resolve, reject) => {
if (!this.webSocket) {
reject("No server connection.");
} else {
this.webSocket.request<void>("renameBank", { bankId: bankId, newName: newName })
.then(() => {
resolve();
})
.catch(error => reject(error));
}
});
return result;
}
saveBankAs(bankId: number, newName: string): Promise<number> {
let result = new Promise<number>((resolve, reject) => {
if (!this.webSocket) {
reject("No server connection.");
} else {
this.webSocket.request<number>("saveBankAs", { bankId: bankId, newName: newName })
.then((newInstanceId) => {
resolve(newInstanceId);
})
.catch(error => reject(error));
}
});
return result;
}
getJackStatus(): Promise<JackHostStatus> {
let result = new Promise<JackHostStatus>((resolve, reject) => {
if (!this.webSocket) {
reject("No connection to server.");
} else {
this.webSocket.request<any>("getJackStatus")
.then((data) => {
resolve(new JackHostStatus().deserialize(data));
})
.catch(error => reject(error));
}
});
return result;
}
presetCache: { [uri: string]: PluginUiPresets } = {};
uncachePluginPreset(pluginUri: string): void {
delete this.presetCache[pluginUri];
}
getPluginPresets(uri: string): Promise<PluginUiPresets> {
return new Promise<PluginUiPresets>((resolve, reject) => {
if (this.presetCache[uri] !== undefined) {
resolve(this.presetCache[uri]);
} else {
this.webSocket!.request<any>("getPluginPresets", uri)
.then((result) => {
let presets = new PluginUiPresets().deserialize(result);
this.presetCache[uri] = presets;
resolve(presets);
})
.catch((error) => {
reject(error);
})
}
});
}
loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void {
this.webSocket?.send("loadPluginPreset", { pluginInstanceId: pluginInstanceId, presetInstanceId: presetInstanceId });
}
handlePluginPresetsChanged(pluginUri: string): void {
this.uncachePluginPreset(pluginUri);
this.firePluginPresetsChanged(pluginUri);
}
updatePluginPresets(pluginUri: string, presets: PluginUiPresets): Promise<void> {
this.presetCache[pluginUri] = presets;
this.firePluginPresetsChanged(pluginUri);
return nullCast(this.webSocket).request<void>("updatePluginPresets", presets);
}
duplicatePluginPreset(pluginUri: string, instanceId: number): Promise<number> {
return nullCast(this.webSocket).request<number>("copyPluginPreset", {
pluginUri: pluginUri, instanceId: instanceId
});
}
shutdown(): Promise<void> {
return this.webSocket!.request<void>("shutdown");
}
restart(): Promise<void> {
return this.webSocket!.request<void>("restart");
}
getJackServerSettings(): Promise<JackServerSettings> {
return this.webSocket!.request<any>("getJackServerSettings")
.then((data) => {
return new JackServerSettings().deserialize(data);
});
}
setJackServerSettings(jackServerSettings: JackServerSettings): void {
this.webSocket?.request<void>("setJackServerSettings", jackServerSettings)
.catch((error) => {
this.showAlert(error);
});
}
updateVst3State(pedalboard: Pedalboard) {
// let it = pedalboard.itemsGenerator();
// while (true) {
// let v = it.next();
// if (v.done) break;
// let item = v.value;
// }
}
setMidiBinding(instanceId: number, midiBinding: MidiBinding): void {
let pedalboard = this.pedalboard.get();
if (!pedalboard) {
throw new PiPedalStateError("Pedalboard not loaded.");
}
let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard);
if (newPedalboard.setMidiBinding(instanceId, midiBinding)) {
this.pedalboard.set(newPedalboard);
// notify the server.
// TODO: a more efficient notification.
this.updateServerPedalboard()
}
}
setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void {
let currentBindings = this.systemMidiBindings.get();
let result: MidiBinding[] = [];
for (var binding of currentBindings) {
if (binding.symbol === midiBinding.symbol) {
result.push(midiBinding);
} else {
result.push(binding);
}
}
this.systemMidiBindings.set(result);
this.webSocket?.send("setSystemMidiBindings", result);
}
private midiListeners: MidiEventListener[] = [];
private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = [];
nextListenHandle = 1;
listenForMidiEvent(listenForControlsOnly: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle {
let handle = this.nextListenHandle++;
this.midiListeners.push(new MidiEventListener(handle, onComplete));
this.webSocket?.send("listenForMidiEvent", { listenForControlsOnly: listenForControlsOnly, handle: handle });
return {
_handle: handle
};
}
monitorPatchProperty(
instanceId: number,
propertyUri: string,
onReceived: PatchPropertyListener
): ListenHandle {
let handle = this.nextListenHandle++;
this.monitorPatchPropertyListeners.push(new PatchPropertyListenerItem(handle, instanceId, onReceived));
this.webSocket?.send("monitorPatchProperty", { clientHandle: handle, instanceId: instanceId, propertyUri: propertyUri, });
return {
_handle: handle
};
}
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
if (this.monitorPatchPropertyListeners[i].handle === listenHandle._handle) {
this.monitorPatchPropertyListeners.splice(i, 1);
break;
}
}
this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle);
}
private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) {
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
let listener = this.monitorPatchPropertyListeners[i];
if (listener.handle === clientHandle && listener.instanceId === instanceId) {
listener.callback(instanceId, propertyUri, jsonObject);
}
}
}
handleNotifyMidiListener(clientHandle: number, isNote: boolean, noteOrControl: number) {
for (let i = 0; i < this.midiListeners.length; ++i) {
let listener = this.midiListeners[i];
if (listener.handle === clientHandle) {
listener.callback(isNote, noteOrControl);
}
}
}
cancelListenForMidiEvent(listenHandle: ListenHandle): void {
let found = false;
for (let i = 0; i < this.midiListeners.length; ++i) {
if (this.midiListeners[i].handle === listenHandle._handle) {
this.midiListeners.splice(i, 1);
found = true;
break;
}
}
if (!found) {
console.log('cancelListenForMidiEvent: event not found.');
}
this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle);
}
download(targetType: string, instanceId: number | string): void {
if (instanceId === -1) return;
let url = this.varServerUrl + targetType + "?id=" + instanceId;
window.open(url, "_blank");
}
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
let result = new Promise<string>((resolve, reject) => {
try {
if (file.size > this.maxFileUploadSize) {
reject("File is too large.");
}
let url = this.varServerUrl + uploadPage;
let parsedUrl = new URL(url);
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
let query = parsedUrl.search;
if (query.length === 0)
{
query += '?';
} else {
query += '&';
}
if (!fileNameOnly)
{
reject("Invalid filename.");
}
query += "filename=" + encodeURIComponent(fileNameOnly??"");
parsedUrl.search = query;
url = parsedUrl.toString();
fetch(
url,
{
method: "POST",
body: file,
headers: {
'Content-Type': contentType,
},
signal: abortController?.signal
}
)
.then((response: Response) => {
if (!response.ok) {
reject("Upload failed. " + response.statusText);
return;
} else {
return response.json();
}
})
.then((json) => {
resolve(json as string);
})
.catch((error) => {
reject("Upload failed. " + error);
})
;
} catch (error) {
reject("Upload failed. " + error);
}
});
return result;
}
uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise<number> {
let result = new Promise<number>((resolve, reject) => {
try {
if (file.size > this.maxPresetUploadSize) {
reject("File is too large.");
}
let url = this.varServerUrl + uploadPage;
if (uploadAfter && uploadAfter !== -1) {
url += "?uploadAfter=" + uploadAfter;
}
fetch(
url,
{
method: "POST",
body: file,
headers: {
'Content-Type': 'application/json',
},
}
)
.then((response: Response) => {
if (!response.ok) {
reject("Upload failed. " + response.statusText);
return;
} else {
return response.json(); // read the empty body to keep the connection alive.
}
})
.then((json) => {
resolve(json as number);
})
.catch((error) => {
reject("Upload failed. " + error);
})
;
} catch (error) {
reject("Upload failed. " + error);
}
});
return result;
}
uploadBank(file: File, uploadAfter: number): Promise<number> {
let result = new Promise<number>((resolve, reject) => {
try {
console.log("File: " + file.name + " Size: " + file.size);
if (file.size > this.maxPresetUploadSize) {
reject("File is too large.");
}
let url = this.varServerUrl + "uploadBank";
if (uploadAfter && uploadAfter !== -1) {
url += "?uploadAfter=" + uploadAfter;
}
fetch(
url,
{
method: "POST",
body: file,
headers: {
'Content-Type': 'application/json',
},
}
)
.then((response: Response) => {
if (!response.ok) {
reject("Upload failed. " + response.statusText);
return;
} else {
return response.json(); // read the empty body to keep the connection alive.
}
})
.then((json) => {
resolve(json as number);
})
.catch((error) => {
reject("Upload failed. " + error);
})
;
} catch (error) {
reject("Upload failed. " + error);
}
});
return result;
}
setGovernorSettings(governor: string): Promise<void> {
let newSettings = this.governorSettings.get().clone();
newSettings.governor = governor;
this.governorSettings.set(newSettings);
return new Promise<void>((resolve, reject) => {
let ws = this.webSocket;
if (!ws) return;
ws.request<void>(
"setGovernorSettings",
governor
)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
}
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
let result = new Promise<void>((resolve, reject) => {
let oldSettings = this.wifiConfigSettings.get();
wifiConfigSettings = wifiConfigSettings.clone();
if ((!oldSettings.enable) && (!wifiConfigSettings.enable)) {
// no effective change.
resolve();
return;
}
if (!wifiConfigSettings.enable) {
wifiConfigSettings.hasPassword = false;
wifiConfigSettings.hotspotName = oldSettings.hotspotName;
} else {
if (wifiConfigSettings.countryCode === oldSettings.countryCode
&& wifiConfigSettings.channel === oldSettings.channel
&& wifiConfigSettings.hotspotName === oldSettings.hotspotName) {
if (!wifiConfigSettings.hasPassword) {
// no effective change.
resolve();
return;
}
}
}
// save a version for the server (potentially carrying a password)
let serverConfigSettings = wifiConfigSettings.clone();
wifiConfigSettings.password = "";
this.wifiConfigSettings.set(wifiConfigSettings);
// notify the server.
let ws = this.webSocket;
if (!ws) {
reject("Not connected.");
return;
}
ws.request<void>(
"setWifiConfigSettings",
serverConfigSettings
)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
return result;
}
setWifiDirectConfigSettings(wifiDirectConfigSettings: WifiDirectConfigSettings): Promise<void> {
let result = new Promise<void>((resolve, reject) => {
let oldSettings = this.wifiDirectConfigSettings.get();
wifiDirectConfigSettings = wifiDirectConfigSettings.clone();
if ((!oldSettings.enable) && (!wifiDirectConfigSettings.enable)
&& (oldSettings.hotspotName === wifiDirectConfigSettings.hotspotName)
) {
// no effective change.
resolve();
return;
}
if (!wifiDirectConfigSettings.enable) {
let t = wifiDirectConfigSettings.hotspotName; // hotspot name can be changed when disabled because it's also the mDNS service name.
wifiDirectConfigSettings = oldSettings.clone();
wifiDirectConfigSettings.enable = false;
wifiDirectConfigSettings.hotspotName = t;
} else {
if (wifiDirectConfigSettings.countryCode === oldSettings.countryCode
&& wifiDirectConfigSettings.channel === oldSettings.channel
&& wifiDirectConfigSettings.hotspotName === oldSettings.hotspotName
&& wifiDirectConfigSettings.enable === oldSettings.enable) {
if (wifiDirectConfigSettings.pin === oldSettings.pin) {
// no effective change.
resolve();
return;
}
}
}
// save a version for the server (potentially carrying a password)
let serverConfigSettings = wifiDirectConfigSettings.clone();
this.wifiDirectConfigSettings.set(wifiDirectConfigSettings);
// notify the server.
let ws = this.webSocket;
if (!ws) {
reject("Not connected.");
return;
}
ws.request<void>(
"setWifiDirectConfigSettings",
serverConfigSettings
)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
return result;
}
getWifiChannels(countryIso3661: string): Promise<WifiChannel[]> {
let result = new Promise<WifiChannel[]>((resolve, reject) => {
if (!this.webSocket) {
reject("Connection closed.");
return;
}
this.webSocket.request<WifiChannel[]>("getWifiChannels", countryIso3661)
.then((data) => {
resolve(WifiChannel.deserialize_array(data));
})
.catch((err) => reject(err));
});
return result;
}
getAlsaDevices(): Promise<AlsaDeviceInfo[]> {
let result = new Promise<AlsaDeviceInfo[]>((resolve, reject) => {
if (!this.webSocket) {
reject("Connection closed.");
return;
}
this.webSocket.request<any>("getAlsaDevices")
.then((data) => {
resolve(AlsaDeviceInfo.deserialize_array(data));
})
.catch((err) => reject(err));
});
return result;
}
zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void {
let name = uiControl.name;
if (uiControl.port_group !== "") {
let pedalboard = this.pedalboard.get();
if (pedalboard) {
let plugin = pedalboard.getItem(instanceId);
let uiPlugin = this.getUiPlugin(plugin.uri);
if (uiPlugin) {
for (let i = 0; i < uiPlugin.port_groups.length; ++i) {
if (uiPlugin.port_groups[i].symbol === uiControl.port_group) {
name = uiPlugin.port_groups[i].name + " / " + name;
break;
}
}
}
}
}
this.zoomedUiControl.set({ source: sourceElement, name: name, instanceId: instanceId, uiControl: uiControl });
}
onPreviousZoomedControl(): void {
let currentUiControl = this.zoomedUiControl.get();
if (!currentUiControl) return;
let currentSymbol = currentUiControl.uiControl.symbol;
let pedalboard = this.pedalboard.get();
if (!pedalboard) return;
let pedalboardItem = pedalboard.getItem(currentUiControl.instanceId);
let uiPlugin = this.getUiPlugin(pedalboardItem.uri);
if (!uiPlugin) return;
let i = 0;
let ix = -1;
for (i = 0; i < uiPlugin.controls.length; ++i) {
if (uiPlugin.controls[i].symbol === currentSymbol) {
ix = i;
break;
}
}
if (ix === -1) return;
++ix;
if (ix >= uiPlugin.controls.length) return;
while (!uiPlugin.controls[ix].is_input)
{
++ix;
if (ix >= uiPlugin.controls.length) return;
}
this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]);
}
onNextZoomedControl(): void {
let currentUiControl = this.zoomedUiControl.get();
if (!currentUiControl) return;
let currentSymbol = currentUiControl.uiControl.symbol;
let pedalboard = this.pedalboard.get();
if (!pedalboard) return;
let pedalboardItem = pedalboard.getItem(currentUiControl.instanceId);
let uiPlugin = this.getUiPlugin(pedalboardItem.uri);
if (!uiPlugin) return;
let i = 0;
let ix = -1;
for (i = 0; i < uiPlugin.controls.length; ++i) {
if (uiPlugin.controls[i].symbol === currentSymbol) {
ix = i;
break;
}
}
if (ix === -1) return;
--ix;
if (ix < 0) return;
while (!uiPlugin.controls[ix].is_input)
{
--ix;
if (ix < 0) return;
}
this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]);
}
clearZoomedControl(): void {
this.zoomedUiControl.set(undefined);
}
setFavorite(pluginUrl: string, isFavorite: boolean): void {
let favorites = this.favorites.get();
let newFavorites: FavoritesList = {};
Object.assign(newFavorites, favorites);
if (isFavorite) {
newFavorites[pluginUrl] = true;
} else {
if (newFavorites[pluginUrl]) {
delete newFavorites[pluginUrl];
}
}
this.favorites.set(newFavorites);
if (this.webSocket) {
this.webSocket.send("setFavorites", newFavorites);
}
// stub: update server.
}
preloadImages(imageList: string): void {
let imageNames = imageList.split(';');
for (let i = 0; i < imageNames.length; ++i) {
let imageName = imageNames[i];
let img = new Image();
img.src = "/img/" + imageName;
}
}
isAndroidHosted(): boolean { return this.androidHost !== undefined; }
showAndroidDonationActivity(): void {
if (this.androidHost) {
this.androidHost.showSponsorship();
}
}
getAndroidHostVersion(): string {
if (this.androidHost) {
return this.androidHost.getHostVersion();
}
return "";
}
chooseNewDevice(): void {
if (this.androidHost) {
return this.androidHost.chooseNewDevice();
}
}
// returns the ID of the new preset.
newPresetItem(createAfter: number): Promise<number>
{
return nullCast(this.webSocket).request<number>("newPreset");
}
getTheme(): ColorTheme {
return isDarkMode() ? ColorTheme.Dark: ColorTheme.Light;
}
setTheme(value: ColorTheme) {
if (this.getTheme() !== value)
{
setDarkMode(value === ColorTheme.Dark);
this.reloadPage();
}
}
reloadRequested: boolean = false;
reloadPage() {
this.reloadRequested = true;
// eslint-disable-next-line no-restricted-globals
location.reload();
}
};
let instance: PiPedalModel | undefined = undefined;
export class PiPedalModelFactory {
static getInstance(): PiPedalModel {
if (instance === undefined) {
let impl: PiPedalModel = new PiPedalModel();
instance = impl;
impl.initialize();
}
return instance;
}
};