// 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 { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin'; import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError'; import { UpdateStatus, UpdatePolicyT } from './Updater'; import ObservableEvent from './ObservableEvent'; import { ObservableProperty } from './ObservableProperty'; import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard' import PluginClass from './PluginClass'; import ScreenOrientation from './ScreenOrientation'; import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket'; import { Tone3000DownloadHandler } from './Tone3000Downloader'; import Tone3000DownloadType from './Tone3000DownloadType'; 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 { ColorTheme, getColorScheme, setColorScheme } from './DarkMode'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; import { getDefaultModGuiPreference } from './ModGuiHost'; import ChannelRouterSettings from './ChannelRouterSettings'; import Tone3000DownloadProgress from './Tone3000DownloadProgress'; import { Model, Tone } from './t3k/types'; import { ModelSelectionDialogParams } from './ModelSelectionDialog'; export enum State { Loading, Ready, Error, Background, Reconnecting, ApplyingChanges, ReloadingPlugins, DownloadingUpdate, InstallingUpdate, HotspotChanging, }; export interface T3kModelSelectionDialogListener { onShowDialog: ( modelSelectionDialogParams: ModelSelectionDialogParams ) => void; }; export interface Tone3000PkceParams { publishableKey: string; redirectUrl: string; codeVerifier: string; codeChallenge: string; state: string; } export function getErrorMessage(error: any) { if (error instanceof Error) { return (error as Error).message; } if (!error) { return ""; } return error.toString(); } class UpdatedError extends Error { }; export function wantsReloadingScreen(state: State) { return state >= State.Reconnecting; } export enum ReconnectReason { Disconnected, LoadingSettings, ReloadingPlugins, Updating, HotspotChanging }; export interface Tone3000ModelInfo { url: string; name: string; }; export interface Tone3000DownloadRequest { downloadType: Tone3000DownloadType; downloadPath: string; appId: string; state: string; codeChallenge: string; codeVerifier: string; authToken: string; id: number, title: string; description: string; imageUrl: string; models: Tone3000ModelInfo[]; updated_at: string; user: string; sizes: string[]; platform: string; gear: string; license: string; links: string[]; }; export class HostVersion { constructor(hostVersion: string) { this.versionString = hostVersion; let pos = hostVersion.indexOf(":"); let remainder: string; if (pos >= 0) { this.hostName = hostVersion.substring(0, pos).trim(); remainder = hostVersion.substring(pos + 1).trim(); } else { this.hostName = "Unknown Host"; remainder = "v0.0.0"; } const args = remainder.split(","); // make make provisions for more. if (args.length >= 1) { let versionString = args[0].trim(); if (versionString.startsWith('v')) { versionString = versionString.substring(1).trim(); } let releaseTypePos = versionString.indexOf("-"); if (releaseTypePos >= 0) { this.releaseType = versionString.substring(releaseTypePos + 1).trim(); versionString = versionString.substring(0, releaseTypePos).trim(); } else { this.releaseType = "Release"; } const parts = args[0].split("."); if (parts.length >= 3) { this.majorVersion = parseInt(parts[0]); this.minorVersion = parseInt(parts[1]); this.buildNumber = parseInt(parts[2]); } } } lessThan(major: number, minor: number, build: number): boolean { if (this.majorVersion < major) return true; if (this.majorVersion > major) return false; if (this.minorVersion < minor) return true; if (this.minorVersion > minor) return false; return this.buildNumber < build; } versionString: string = ""; hostName: string = ""; releaseType: string = ""; majorVersion: number = 0; minorVersion: number = 0; buildNumber: number = 0; } export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void; interface PedalboardItemEnabledChangeItem { handle: number; instanceId: number; currentValue: boolean; onEnabledChanged: PedalboardItemEnabledChangeCallback; }; export type PedalboardItemUseModUiChangeCallback = (instanceId: number, useModUi: boolean) => void; interface PedalboardItemUseModUiChangeItem { handle: number; instanceId: number; currentValue: boolean; onUseModUiChanged: PedalboardItemUseModUiChangeCallback; }; export type ControlValueChangedHandler = (key: string, value: number) => void; export interface FileEntry { pathname: string; displayName: string; isDirectory: boolean; isProtected: boolean; // optional metadata for audio metadata?: AudioFileMetadata; }; export interface BreadcrumbEntry { pathname: string; displayName: string; }; export class FileRequestResult { files: FileEntry[] = []; isProtected: boolean = false; breadcrumbs: BreadcrumbEntry[] = []; currentDirectory: string = ""; }; 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; }; export class MidiMessage { constructor(cc0: number, cc1: number, cc2: number) { this.cc0 = cc0; this.cc1 = cc1; this.cc2 = cc2; } cc0: number; cc1: number; cc2: number; isNote() { return (this.cc0 & 0xF0) == 0x90 && this.cc2 !== 0; } isControl() { return (this.cc0 & 0xF0) == 0xB0; } }; class MidiEventListener { constructor(handle: number, callback: (message: MidiMessage) => void) { this.handle = handle; this.callback = callback; } handle: number; callback: (midiMessage: MidiMessage) => 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 ChannelRouterSettingsChangedBody { clientId: number; channelRouterSettings: ChannelRouterSettings; } 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 Vst3ControlChangedBody { clientId: number; instanceId: number; symbol: string; value: number; state: string; }; export interface FavoritesList { [url: string]: boolean; }; export interface SnapshotModifiedEvent { snapshotIndex: number; modified: boolean; }; export class PiPedalModel //implements PiPedalModel { clientId: number = -1; serverVersion?: PiPedalVersion; countryCodes: { [Name: string]: string } = {}; serverUrl: string = ""; socketServerUrl: string = ""; varServerUrl: string = ""; modResourcesUrl: string = ""; lv2Path: string = ""; webSocket?: PiPedalSocket; static getInstance(): PiPedalModel { return PiPedalModelFactory.getInstance(); } canKeepScreenOn: boolean = false; keepScreenOn: ObservableProperty = new ObservableProperty(false); canSetScreenOrientation: boolean = false; screenOrientation: ObservableProperty = new ObservableProperty(ScreenOrientation.SystemDefault); hasWifiDevice: ObservableProperty = new ObservableProperty(false); onSnapshotModified: ObservableEvent = new ObservableEvent(); onTone3000DownloadCompleteEvent: ObservableEvent = new ObservableEvent(); ui_plugins: ObservableProperty = new ObservableProperty([]); state: ObservableProperty = new ObservableProperty(State.Loading); visibilityState: ObservableProperty = new ObservableProperty(VisibilityState.Visible); promptForUpdate: ObservableProperty = new ObservableProperty(false); updateStatus: ObservableProperty = new ObservableProperty(new UpdateStatus()); errorMessage: ObservableProperty = new ObservableProperty(""); alertMessage: ObservableProperty = new ObservableProperty(""); showStatusMonitor: ObservableProperty = new ObservableProperty(true); pedalboard: ObservableProperty = new ObservableProperty(new Pedalboard()); presetChanged: ObservableProperty = new ObservableProperty(false); selectedSnapshot: ObservableProperty = new ObservableProperty(-1); plugin_classes: ObservableProperty = new ObservableProperty(new PluginClass()); jackConfiguration: ObservableProperty = new ObservableProperty(new JackConfiguration()); jackSettings: ObservableProperty = new ObservableProperty(new JackChannelSelection()); banks: ObservableProperty = new ObservableProperty(new BankIndex()); jackServerSettings: ObservableProperty = new ObservableProperty(new JackServerSettings()); channelRouterSettings: ObservableProperty = new ObservableProperty(new ChannelRouterSettings()); wifiConfigSettings: ObservableProperty = new ObservableProperty(new WifiConfigSettings()); wifiDirectConfigSettings: ObservableProperty = new ObservableProperty(new WifiDirectConfigSettings()); governorSettings: ObservableProperty = new ObservableProperty(new GovernorSettings()); favorites: ObservableProperty = new ObservableProperty({}); alsaSequencerConfiguration: ObservableProperty = new ObservableProperty(new AlsaSequencerConfiguration()); presets: ObservableProperty = new ObservableProperty ( new PresetIndex() ); systemMidiBindings: ObservableProperty = new ObservableProperty ( [ MidiBinding.systemBinding("prevProgram"), MidiBinding.systemBinding("nextProgram"), MidiBinding.systemBinding("prevSnapshot"), MidiBinding.systemBinding("nextSnapshot") ] ); zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); tone3000Downloading: ObservableProperty = new ObservableProperty(false); tone3000DownloadProgress: ObservableProperty = new ObservableProperty(null); uiPluginsByUri: Map = new Map(); private tone3000DownloadHandler: Tone3000DownloadHandler | null = null; svgImgUrl(svgImage: string): string { // return this.varServerUrl + "img/" + svgImage; return "img/" + svgImage; } getUiPlugin(uri: string): UiPlugin | null { return this.uiPluginsByUri.get(uri) ?? null; } androidHost?: AndroidHostInterface; hostVersion?: HostVersion; 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); } private expectDisconnectTimer?: number = undefined; cancelExpectDisconnectTimer() { if (this.expectDisconnectTimer) { clearTimeout(this.expectDisconnectTimer); } } startExpectDisconnectTimer() { this.cancelExpectDisconnectTimer(); // poll for access to a running pipedal server this.expectDisconnectTimer = setTimeout( async () => { this.reconnectReason = ReconnectReason.Disconnected; }, 5 * 1000); } expectDisconnect(reason: ReconnectReason) { this.cancelExpectDisconnectTimer(); this.reconnectReason = reason; if (this.reconnectReason !== ReconnectReason.Disconnected) { this.startExpectDisconnectTimer(); } } onSocketReconnecting(retry: number, maxRetries: number): boolean { this.cancelExpectDisconnectTimer(); if (this.isClosed) return false; //if (retry !== 0) { switch (this.reconnectReason) { case ReconnectReason.Disconnected: default: this.setState(State.Reconnecting); break; case ReconnectReason.LoadingSettings: this.setState(State.ApplyingChanges); break; case ReconnectReason.ReloadingPlugins: this.setState(State.ReloadingPlugins); break; case ReconnectReason.Updating: this.setState(State.InstallingUpdate); break; case ReconnectReason.HotspotChanging: this.setState(State.HotspotChanging); this.startHotspotReconnectTimer(); break; } return true; } 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; } async makeTone3000Pkce(redirectUrl: string): Promise { if (this.webSocket === undefined) { throw new Error("Server disconnected."); } return await this.webSocket.request( "makeTone3000Pkce", redirectUrl); } async sha256Base64url(intput: string): Promise { if (this.webSocket === undefined) { throw new Error("Server disconnected."); } return this.webSocket.request( "sha256Base64url", intput); } async pingTone3000Server(): Promise { if (this.webSocket === undefined) { return false; } try { let result = await this.webSocket.request("pingTone3000Server", {}); return result; } catch (error) { console.log("Error pinging TONE3000 Server on PiPedal server: " + getErrorMessage(error)); return false; } } async downloadModelsFromTone3000( downloadRequest: Tone3000DownloadRequest ): Promise { try { if (!this.webSocket) { return; } this.webSocket.send("downloadModelsFromTone3000", downloadRequest); } catch (error) { this.showAlert(getErrorMessage(error)); } } private cancelTone3000Download(): void { let downloadProgress = this.tone3000DownloadProgress.get(); if (downloadProgress !== null) { if (this.tone3000DownloadHandler) { this.tone3000DownloadHandler.cancelDownload(this.tone3000DownloadProgress.get()?.handle ?? -1); } // if (downloadProgress.handle !== -1) { // if (!this.webSocket) { // this.tone3000DownloadProgress.set(null); // this.tone3000Downloading.set(false); // return; // } // this.webSocket.send("cancelTone3000Download", downloadProgress.handle); // } } } showTone3000DownloadPopup( downloadType: Tone3000DownloadType, downloadPath: string, options?: { userName?: string; } ): void { if (this.tone3000DownloadHandler === null) { this.tone3000DownloadHandler = new Tone3000DownloadHandler(this); } this.tone3000DownloadHandler.launchTone3000Popup( downloadType, downloadPath, options); }; showTone3000DownloadStatus(): void { this.tone3000Downloading.set(true); this.tone3000DownloadProgress.set(null); console.log("Showing TONE3000 download status popup"); } hideTone3000DownloadStatus(): void { this.tone3000Downloading.set(false); this.tone3000DownloadProgress.set(null); console.log("Hiding TONE3000 download status popup"); } closeTone3000DownloadPopup(): void { this.cancelTone3000Download(); this.tone3000Downloading.set(false); this.tone3000DownloadProgress.set(null); if (this.tone3000DownloadHandler) { this.tone3000DownloadHandler.closeTone3000Popup(); } } private updateEnabledItems(pedalboard: Pedalboard) { for (let item of pedalboard.itemsGenerator()) { this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled); this.updatePedalboardItemUseModUi(item.instanceId, item.useModUi); } } private setModelPedalboard(pedalboard: Pedalboard) { this.removeInvalidSidechains(pedalboard); this.pedalboard.set(pedalboard); this.selectedSnapshot.set(pedalboard.selectedSnapshot); this.updateEnabledItems(pedalboard); } onSocketMessage(header: PiPedalMessageHeader, body?: any) { let message = header.message; if (message === "onControlChanged") { let controlChangedBody = body as ControlChangedBody; if (body.clientId !== this.clientId) { this.lastControlMessageWasSentbyMe = false; } if (this.lastControlMessageWasSentbyMe) { return; // shortcut! } 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 === "onChannelRouterSettingsChanged") { let channelRouterSettingChangedBody = body as ChannelRouterSettingsChangedBody; let channelRouterSettings = new ChannelRouterSettings().deserialize( channelRouterSettingChangedBody.channelRouterSettings); this.channelRouterSettings.set(channelRouterSettings); } else if (message === "onSnapshotModified") { let { snapshotIndex, modified } = (body as { snapshotIndex: number, modified: boolean }); let snapshots = this.pedalboard.get().snapshots; if (snapshotIndex >= 0 && snapshotIndex < snapshots.length) { let snapshot = snapshots[snapshotIndex] if (snapshot) { if (snapshot.isModified !== modified) { snapshot.isModified = modified; this.onSnapshotModified.fire({ snapshotIndex: snapshotIndex, modified: modified }); } } } } else if (message === "onSelectedSnapshotChanged") { let selectedSnapshot = body as number; this.pedalboard.get().selectedSnapshot = selectedSnapshot; this.selectedSnapshot.set(selectedSnapshot); } else if (message === "onPresetChanged") { let changed = body as boolean; if (this.presets.get().presetChanged !== changed) { let newPresets = this.presets.get().clone(); // deep clone. newPresets.presetChanged = changed; this.presets.set(newPresets); } this.presetChanged.set(changed); } else if (message === "onPresetsChanged") { let presetsChangedBody = body as PresetsChangedBody; let presets = new PresetIndex().deserialize(presetsChangedBody.presets); this.presets.set(presets); this.presetChanged.set(presets.presetChanged); } 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 === "onAlsaSequencerConfigurationChanged") { this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().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 == "onItemUseModUiChanged") { let itemEnabledBody = body as PedalboardItemEnableBody; this._setPedalboardItemUseModUi( itemEnabledBody.instanceId, itemEnabledBody.enabled, false // No server notification. ); } else if (message === "onPedalboardChanged") { let pedalChangedBody = body as PedalboardChangedBody; this.setModelPedalboard(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 notifyBody = body as { clientHandle: number, cc0: number, cc1: number, cc2: number }; let clientHandle = notifyBody.clientHandle as number; this.handleNotifyMidiListener( clientHandle, notifyBody.cc0, notifyBody.cc1, notifyBody.cc2); } else if (message === "onNotifyPathPatchPropertyChanged") { let instanceId = body.instanceId as number; let propertyUri = body.propertyUri as string; let atomJsonString = body.atomJson as string; this.handleNotifyPathPatchPropertyChanged(instanceId, propertyUri, atomJsonString); if (header.replyTo) { this.webSocket?.reply(header.replyTo, "onNotifyPathPatchPropertyChanged", true); } } 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 if (message === "onLv2PluginsChanging") { this.onLv2PluginsChanging(); } else if (message === "onUpdateStatusChanged") { let updateStatus = new UpdateStatus().deserialize(body); this.onUpdateStatusChanged(updateStatus); } else if (message === "onNetworkChanging") { this.onNetworkChanging(body as boolean); } else if (message === "onHasWifiChanged") { let hasWifi = body as boolean; this.hasWifiDevice.set(hasWifi); } else if (message === "onTone3000DownloadStarted") { let { handle, title } = body as { handle: number, title: string }; this.onTone3000DownloadStarted(handle, title); } else if (message === "onTone3000DownloadProgress") { // body is DownloadProgress this.onTone3000DownloadProgress(body); } else if (message === "onTone3000DownloadComplete") { let resultPath = body as string; this.onTone3000DownloadComplete(resultPath); } else if (message === "onTone3000DownloadError") { let { handle, errorMessage } = body as { handle: number, errorMessage: string }; this.onTone3000DownloadError(handle, errorMessage); } } private updateLaterTimeout?: number = undefined; private clearPromptForUpdateTimer() { if (this.updateLaterTimeout) { clearTimeout(this.updateLaterTimeout); this.updateLaterTimeout = undefined; } } private setPromptForUpdateTimer(when: Date) { let ms = when.getTime() - Date.now(); this.updateLaterTimeout = setTimeout( () => { this.updateLaterTimeout = undefined; // make the server do a fresh check this.getUpdateStatus() .then( () => { this.updatePromptForUpdate(); }) .catch((e) => { }); }, ms); } private lastCanUpdateNow: boolean = false; private updatePromptForUpdate() { this.clearPromptForUpdateTimer(); if (!this.enableAutoUpdate) { return; } let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready; let timeEnabled = false; let updateLaterTime = this.getUpdateTime(); if (updateLaterTime == null) { timeEnabled = true; } else { let nDate = Date.now(); let now: Date = new Date(nDate); let maxDate: Date = new Date(nDate + 86400000 * 2); // sanity check for systems with unstable system clock timeEnabled = (updateLaterTime < now || updateLaterTime >= maxDate) } let updateStatus = this.updateStatus.get(); let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable; let canUpdateNow: boolean = (stateEnabled && timeEnabled && statusEnabled); if (updateStatus.updatePolicy === UpdatePolicyT.Disable) { canUpdateNow = false; } if (canUpdateNow && canUpdateNow !== this.lastCanUpdateNow) { this.showUpdateDialogValue = true; // make the dialog sticky so it can show OK button } this.lastCanUpdateNow = canUpdateNow; this.promptForUpdate.set(this.showUpdateDialogValue || canUpdateNow); if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime) { this.setPromptForUpdateTimer(updateLaterTime); } } private showUpdateDialogValue: boolean = false; showUpdateDialog(show: boolean = true) { if (this.showUpdateDialogValue !== show) { this.showUpdateDialogValue = show; if (show) { this.forceUpdateCheck(); } this.updatePromptForUpdate(); } } onUpdateStatusChanged(updateStatus: UpdateStatus): void { let current = this.updateStatus.get(); if (!current.equals(updateStatus)) { this.updateStatus.set(updateStatus); if (current.currentVersion.length !== 0 && current.currentVersion !== updateStatus.currentVersion) { // !! Server has been updated!!! this.reloadPage(); throw new UpdatedError("Server has been updated"); } this.updatePromptForUpdate() } } onLv2PluginsChanging(): void { this.expectDisconnect(ReconnectReason.ReloadingPlugins); // this.webSocket?.reconnect(); // let the server do it for us. } // TONE3000 download notification handlers (stub implementations) onTone3000DownloadStarted(handle: number, title: string): void { this.tone3000Downloading.set(true); } onTone3000DownloadProgress(progress: Tone3000DownloadProgress): void { this.tone3000Downloading.set(true); this.tone3000DownloadProgress.set(progress); } onTone3000DownloadComplete(resultPath: string): void { this.tone3000Downloading.set(false); this.tone3000DownloadProgress.set(null); this.onTone3000DownloadCompleteEvent.fire(resultPath); } onTone3000DownloadError(handle: number, errorMessage: string): void { console.error(`TONE3000 download error: handle=${handle}, error=${errorMessage}`); this.tone3000Downloading.set(false); this.tone3000DownloadProgress.set(null); this.onTone3000DownloadCompleteEvent.fire(""); setTimeout(() => { this.showAlert(errorMessage); }, 100); } setError(message: string): void { this.errorMessage.set(message); this.setState(State.Error); } varRequest(url: string): string { return "var/" + url; } private setState(state: State) { if (this.state.get() !== state) { this.state.set(state); if (state === State.Error) { this.closeTone3000DownloadPopup(); } } } validatePluginClasses(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.validatePluginClasses(plugin.children[i]); } } private reconnectReason: ReconnectReason = ReconnectReason.Disconnected; isReloading(): boolean { return this.state.get() !== State.Ready; } getWebSocket(): PiPedalSocket { if (this.webSocket === undefined) { throw new PiPedalStateError("Attempt to access web socket before it's connected."); } return this.webSocket; } androidReconnectTimeout?: number = undefined; cancelAndroidReconnectTimer() { if (this.androidReconnectTimeout) { clearTimeout(this.androidReconnectTimeout); this.androidReconnectTimeout = undefined; } } startAndroidReconnectTimer() { this.cancelAndroidReconnectTimer(); this.androidReconnectTimeout = setTimeout(() => { this.androidReconnectTimeout = undefined; this.androidHost?.setDisconnected(true); }, 20 * 1000); } onSocketConnectionLost() { // remove all the events and subscriptions we have. if (this.isClosed) { return; // page unloading. do NOT change the UI. } this.vuSubscriptions = []; this.monitorPatchPropertyListeners = []; this.tone3000Downloading.set(false); this.tone3000DownloadProgress.set(null); if (this.isAndroidHosted()) { // if unexpected, go back to the device browser immediately. if (this.reconnectReason === ReconnectReason.Disconnected) { this.androidHost?.setDisconnected(true); } else { this.startAndroidReconnectTimer(); } } } async onSocketReconnected(): Promise { this.cancelOnNetworkChanging(); this.cancelAndroidReconnectTimer(); if (this.isAndroidHosted()) { this.androidHost?.setDisconnected(false); } if (this.visibilityState.get() === VisibilityState.Hidden) return; // reload state, but not configuration. this.clientId = await this.getWebSocket().request("hello"); let newServerVersion = this.serverVersion = await this.getWebSocket().request("version"); if (newServerVersion.serverVersion !== this.serverVersion.serverVersion) { this.reloadPage(); return; } // anything could have changed while we were disconnected. await this.loadServerState(); } private makeSocketServerUrl(hostName: string, port: number): string { return "ws://" + hostName + ":" + port + "/pipedal"; } private makeVarServerUrl(protocol: string, hostName: string, port: number): string { return protocol + "://" + hostName + ":" + port + "/var/"; } private makeServerUrl(protocol: string, hostName: string, port: number): string { return protocol + "://" + hostName + ":" + port; } private makeModResourceUrl(protocol: string, hostName: string, port: number): string { return protocol + "://" + hostName + ":" + port + "/resources/"; } async getNextAudioFile(filePath: string): Promise { try { let url = this.varServerUrl + "NextAudioFile?path=" + encodeURIComponent(filePath); let response = await fetch(url); let json = await response.json(); return json as string; } catch (e) { return ""; } } async getPreviousAudioFile(filePath: string): Promise { try { let url = this.varServerUrl + "PreviousAudioFile?path=" + encodeURIComponent(filePath); let response = await fetch(url); let json = await response.json(); return json as string; } catch (e) { return ""; } } async getAudioFileMetadata(filePath: string): Promise { try { let url = this.varServerUrl + "AudioMetadata?path=" + encodeURIComponent(filePath); let response = await fetch(url); let json = await response.json(); return new AudioFileMetadata().deserialize(json); } catch (e) { return new AudioFileMetadata(); } } async writeTone3000Readme( filePath: string, tone: Tone, thumbnailUrl: string ): Promise { if (!this.webSocket) { throw new Error("Server disconnected."); } await this.webSocket.request( "writeTone3000Readme", { filePath: filePath, tone: tone, thumbnailUrl: thumbnailUrl }); } maxFileUploadSize: number = 512 * 1024 * 1024; maxPresetUploadSize: number = 1024 * 1024; debug: boolean = false; tone3000_A2_models: boolean = true; enableAutoUpdate: boolean = false; async requestConfig(): Promise { try { const myRequest = new Request(this.varRequest('config.json')); let response: Response = await fetch(myRequest); let data = await response.json(); this.tone3000_A2_models = data.tone3000_A2_models ?? true; this.enableAutoUpdate = !!data.enable_auto_update; this.hasWifiDevice.set(!!data.has_wifi_device); 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, max_upload_size } = 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.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port); this.socketServerUrl = socket_server; this.varServerUrl = var_server_url; this.serverUrl = this.makeServerUrl("http", socket_server_address, socket_server_port); this.maxFileUploadSize = parseInt(max_upload_size); } catch (error: any) { this.setError("Can't connect to server. " + getErrorMessage(error)); return false; } this.webSocket = new PiPedalSocket( this.socketServerUrl, { onMessageReceived: this.onSocketMessage, onError: this.onSocketError, onConnectionLost: this.onSocketConnectionLost, onReconnect: this.onSocketReconnected, onReconnecting: this.onSocketReconnecting } ); try { await this.webSocket.connect(); } catch (error) { this.setError("Failed to connect to server. " + getErrorMessage(error)); return false; } try { this.countryCodes = await this.getWebSocket().request<{ [Name: string]: string }>("getWifiRegulatoryDomains"); this.clientId = (await this.getWebSocket().request("hello")) as number; this.preloadImages((await this.getWebSocket().request("imageList"))); } catch (error) { this.setError("Failed to establish connection. " + getErrorMessage(error)); return false; } return await this.loadServerState(); } async loadServerState(): Promise { try { this.serverVersion = await this.getWebSocket().request("version"); this.updateStatus.set(new UpdateStatus().deserialize(await this.getUpdateStatus())); this.hasWifiDevice.set(await this.getWebSocket().request("getHasWifi")); this.ui_plugins.set( UiPlugin.deserialize_array(await this.getWebSocket().request("plugins")) ); // index ui plugins. this.uiPluginsByUri = new Map(); for (let i of this.ui_plugins.get()) { this.uiPluginsByUri.set(i.uri, i); } this.setModelPedalboard( new Pedalboard().deserialize( await this.getWebSocket().request("currentPedalboard") ) ); this.plugin_classes.set(new PluginClass().deserialize( await this.getWebSocket().request("pluginClasses") )); this.validatePluginClasses(this.plugin_classes.get()); this.presets.set( new PresetIndex().deserialize( await this.getWebSocket().request("getPresets") ) ); this.wifiConfigSettings.set( new WifiConfigSettings().deserialize( await this.getWebSocket().request("getWifiConfigSettings") )); this.wifiDirectConfigSettings.set( new WifiDirectConfigSettings().deserialize( await this.getWebSocket().request("getWifiDirectConfigSettings") )); this.governorSettings.set(new GovernorSettings().deserialize( await this.getWebSocket().request("getGovernorSettings") )); this.showStatusMonitor.set( await this.getWebSocket().request("getShowStatusMonitor") ); this.jackServerSettings.set( new JackServerSettings().deserialize( await this.getWebSocket().request("getJackServerSettings") ) ); this.channelRouterSettings.set( new ChannelRouterSettings().deserialize( await this.getWebSocket().request("getChannelRouterSettings") ) ) this.jackConfiguration.set(new JackConfiguration().deserialize( await this.getWebSocket().request("getJackConfiguration") )); this.jackSettings.set(new JackChannelSelection().deserialize( await this.getWebSocket().request("getJackSettings") )); this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( await this.getWebSocket().request("getAlsaSequencerConfiguration") )); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request("getBankIndex"))); this.favorites.set(await this.getWebSocket().request("getFavorites")); this.systemMidiBindings.set(MidiBinding.deserialize_array(await this.getWebSocket().request("getSystemMidiBindings"))); // load at lest once before we allow a reconnect. this.getWebSocket().canReconnect = true; this.setState(State.Ready); return true; } catch (error) { this.setError("Failed to fetch server state.\n\n" + getErrorMessage(error)); return false; } } onError(message: string | Error): void { let m = message; if (message instanceof Error) { let e = message as Error; if (e.message) { m = e.message as string; } else { m = e.toString(); } } else { m = message.toString(); } this.errorMessage.set(m); this.setState(State.Error); } backgroundStateTimeout?: number = undefined; exitBackgroundState() { if (this.backgroundStateTimeout) { clearTimeout(this.backgroundStateTimeout); this.backgroundStateTimeout = undefined; } 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()) { // yyyx; // if (this.backgroundStateTimeout) { // clearTimeout(this.backgroundStateTimeout); // } // this.backgroundStateTimeout = setTimeout(() => { // this.backgroundStateTimeout = undefined; // this.enterBackgroundState_(); // }, 180000); // } else { // this.enterBackgroundState_(); // } 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.exitBackgroundState(); break; case "hidden": this.enterBackgroundState(); break; } } return false; } initialize(): void { this.setError(""); this.setState(State.Loading); this.requestConfig() .then((succeeded) => { if (succeeded) { this.setState(State.Ready); if (this.androidHost) { this.hostVersion = new HostVersion(this.androidHost.getHostVersion()); if (!this.hostVersion.lessThan(1, 1, 16)) { this.androidHost.setServerVersion(this.serverVersion?.serverVersion ?? ""); this.canKeepScreenOn = true; this.keepScreenOn.set(this.androidHost.getKeepScreenOn()) this.canSetScreenOrientation = true; this.screenOrientation.set(this.androidHost.getScreenOrientation()) } } } }) .catch((error) => { this.setError("Failed to get server state. \n\n" + error.toString()); }); 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.tryGetItem(instanceId); if (!item) return; 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.setModelPedalboard(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); } } nextBank() { this.webSocket?.send("nextBank"); } previousBank() { this.webSocket?.send("previousBank"); } nextPreset() { this.webSocket?.send("nextPreset"); } previousPreset() { this.webSocket?.send("previousPreset"); } selectSnapshot(index: number) { this.webSocket?.send("setSnapshot", index); } private pruneSnapshotValues(pedalboard: Pedalboard) { let validPluginIds: Set = new Set(); let it = pedalboard.itemsGenerator(); while (true) { let v = it.next(); if (v.done) break; validPluginIds.add(v.value.instanceId); } for (let snapshot of pedalboard.snapshots) { if (snapshot) { let ix = 0; while (ix < snapshot.values.length) { let value = snapshot.values[ix]; if (validPluginIds.has(value.instanceId)) { ix++; } else { snapshot.values.splice(ix, 1); } } } } } setSnapshots(snapshots: (Snapshot | null)[], selectedSnapshot: number) { let pedalboard = this.pedalboard.get().clone(); pedalboard.snapshots = snapshots; pedalboard.selectedSnapshot = selectedSnapshot; this.pruneSnapshotValues(pedalboard); this.setModelPedalboard(pedalboard); this.webSocket?.send("setSnapshots", { snapshots: pedalboard.snapshots, selectedSnapshot: selectedSnapshot }); } 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.setModelPedalboard(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_ID === 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.setModelPedalboard(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_ID === item.instanceId) { item.onValueChanged("volume_db", volume_db); } } } } private lastControlMessageWasSentbyMe = false; 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 changed: boolean; let newPedalboard = pedalboard.clone(); if (instanceId === Pedalboard.START_CONTROL_ID && key === "volume_db") { this._setInputVolume(value, notifyServer); return; } else if (instanceId === Pedalboard.END_CONTROL_ID) { this._setOutputVolume(value, notifyServer); return; } let item = newPedalboard.getItem(instanceId); changed = item.setControlValue(key, value); if (changed) { if (notifyServer) { this.lastControlMessageWasSentbyMe = true; this._setServerControl("setControl", instanceId, key, value); } this.setModelPedalboard(newPedalboard); 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.setModelPedalboard(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); } } } } sendPedalboardControlTrigger(instanceId: number, key: string, value: number): void { // no state change, no saving the value, just send it to the realtime thread/ this._setServerControl("previewControl", instanceId, 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); } setPedalboardItemUseModUi(instanceId: number, useModUi: boolean): void { this._setPedalboardItemUseModUi(instanceId, useModUi, true); } getPedalboardItemUseModUi(instanceId: number): boolean { if (!this.pedalboard.get().hasItem(instanceId)) return false; let item = this.pedalboard.get().getItem(instanceId); return item.useModUi; } _setPedalboardItemUseModUi(instanceId: number, useModUi: 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 = useModUi !== item.useModUi; if (changed) { item.useModUi = useModUi; this.setModelPedalboard(newPedalboard); if (notifyServer) { let body = { clientId: this.clientId, instanceId: instanceId, useModUi: useModUi }; this.webSocket?.send("setPedalboardItemUseModUi", body); } } } getPedalboardItemEnabled(instanceId: number): boolean { if (!this.pedalboard.get().hasItem(instanceId)) return false; let item = this.pedalboard.get().getItem(instanceId); return item.isEnabled; } 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: PedalboardItem = newPedalboard.getItem(instanceId); let changed = value !== item.isEnabled; if (changed) { item.isEnabled = value; let pluginInfo: UiPlugin | null = this.getUiPlugin(item.uri); if (pluginInfo !== null) { for (let uiControl of pluginInfo.controls) { if (uiControl.is_bypass) { this._setPedalboardControlValue(instanceId, uiControl.symbol, value ? 1.0 : 0.0, false); } } } this.setModelPedalboard(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(); let oldInstanceId = -1; let newInstanceId = -1; while (true) { let v = it.next(); if (v.done) break; let item = v.value; if (item.instanceId === itemId) { oldInstanceId = item.instanceId; item.deserialize(new PedalboardItem()); // skeezy way to re-initialize. item.instanceId = ++newPedalboard.nextInstanceId; newInstanceId = item.instanceId; item.uri = selectedUri; item.pluginName = plugin.name; item.controlValues = this.getDefaultValues(item.uri); item.isEnabled = true; item.lv2State = [false, {}]; item.vstState = ""; item.pathProperties = {}; item.useModUi = getDefaultModGuiPreference(selectedUri); for (let fileProperty of plugin.fileProperties) { // stringized json for an atom. see AtomConverter.hpp. // null -> we've never seen a value. item.pathProperties[fileProperty.patchProperty] = "null"; } newPedalboard.selectedPlugin = item.instanceId; if (oldInstanceId !== -1) { this.updateSidechainReferences(newPedalboard, oldInstanceId, newInstanceId); } this.setModelPedalboard(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. // respect "expensive" port attribute. // Handle special cases for input/output volume if (instanceId === Pedalboard.START_CONTROL_ID && key === "volume_db") { this.previewInputVolume(value); return; } else if (instanceId === Pedalboard.END_CONTROL_ID) { this.previewOutputVolume(value); return; } // Get the control info to check if it's expensive let pedalboard = this.pedalboard.get(); if (pedalboard) { let item = pedalboard.tryGetItem(instanceId); if (item) { let plugin = this.getUiPlugin(item.uri); if (plugin) { let control = plugin.getControl(key); if (control && control.is_expensive) { // Don't send preview for expensive controls // The final value will be sent when the control is committed 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) { newPedalboard.selectedPlugin = result; this.pruneSnapshotValues(newPedalboard); this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } return result; } replacePedalboarditem(instanceId: number, newItem: PedalboardItem): 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."); } let newInstanceId = newPedalboard.replaceItem(instanceId, newItem.clone()); newPedalboard.selectedPlugin = newInstanceId; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } 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); newPedalboard.selectedPlugin = fromItem.instanceId; this.setModelPedalboard(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); newPedalboard.selectedPlugin = fromItem.instanceId; this.setModelPedalboard(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); newPedalboard.selectedPlugin = fromItem.instanceId; this.setModelPedalboard(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); newPedalboard.selectedPlugin = fromItem.instanceId; this.setModelPedalboard(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); newPedalboard.selectedPlugin = fromItem.instanceId; if (!toItem.isEmpty()) { this.updateSidechainReferences(newPedalboard, toItem.instanceId, fromItem.instanceId); } this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } addPedalboardItem(instanceId: number, append: boolean): number { let pedalboard = this.pedalboard.get(); if (instanceId === Pedalboard.START_CONTROL_ID && append) { instanceId = pedalboard.items[0].instanceId; append = false; } else if (instanceId === Pedalboard.END_CONTROL_ID && !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); newPedalboard.selectedPlugin = newItem.instanceId; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); return newItem.instanceId; } addPedalboardSplitItem(instanceId: number, append: boolean): number { let pedalboard = this.pedalboard.get(); if (instanceId === Pedalboard.START_CONTROL_ID && append) { instanceId = pedalboard.items[0].instanceId; append = false; } else if (instanceId === Pedalboard.END_CONTROL_ID && !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); newPedalboard.selectedPlugin = newItem.instanceId; this.setModelPedalboard(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); newPedalboard.selectedPlugin = item.instanceId; this.setModelPedalboard(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); } moveAudioFile( path: string, from: number, to: number ): Promise { return new Promise( (accept, reject) => { this.webSocket?.request("moveAudioFile", { path: path, from: from, to: to }).then(() => { accept(true); }).catch((e) => { this.setError(getErrorMessage(e)); accept(false); }); } ); } saveCurrentPresetAs(bankInstanceId: number, newName: string, saveAfterInstanceId = -1): Promise { // default behaviour is to save after the currently selected preset. if (saveAfterInstanceId === -1) { saveAfterInstanceId = this.presets.get().selectedInstanceId; } let request: any = { clientId: this.clientId, bankInstanceId: bankInstanceId, name: newName, saveAfterInstanceId: saveAfterInstanceId }; return nullCast(this.webSocket) .request("saveCurrentPresetAs", request) .then((newPresetId) => { if (bankInstanceId === this.banks.get().selectedBank) { this.loadPreset(newPresetId); } return newPresetId; }); } getUpdateStatus(): Promise { return new Promise( (accept, reject) => { nullCast(this.webSocket) .request('getUpdateStatus') .then((result) => { let updateStatus = new UpdateStatus().deserialize(result); this.onUpdateStatusChanged(updateStatus); accept(result); }).catch( (e) => { reject(e); } ); }); } launchExternalUrl(url: string) { if (this.isAndroidHosted()) { try { this.androidHost?.launchExternalUrl(url); } catch (e) { // if they haven't updated their client yet, just don't do it. } } else { window.open(url, "_blank")?.focus(); } } updateLater(delayMs: number) { let futureDate = new Date(Date.now() + delayMs); localStorage.setItem('nextUpdateTime', futureDate.toISOString()); this.updatePromptForUpdate(); } updateNow(): Promise { return new Promise( (accept, reject) => { let updateStatus = this.updateStatus.get(); if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable) { this.setState(State.DownloadingUpdate); this.expectDisconnect(ReconnectReason.Updating); let url = updateStatus.getActiveRelease().updateUrl; nullCast(this.webSocket) .request('updateNow', url) .then(() => { this.setState(State.InstallingUpdate); accept(); }) .catch( (e: any) => { this.expectDisconnect(ReconnectReason.Disconnected); this.setState(State.Ready); // TODO: hopefully we haven't had an intermediate disconnect. reject(e); }); } else { reject(new Error("Invalid update request.")); } } ); } getUpdateTime(): Date | null { let item = localStorage.getItem('nextUpdateTime'); if (item) { return new Date(item); } return null; } // deprecated. requestFileList(piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) .request('requestFileList', piPedalFileProperty); } requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) .request('requestFileList2', { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } ); } deleteUserFile(fileName: string): Promise { return nullCast(this.webSocket).request('deleteUserFile', fileName); } saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): Promise { // default behaviour is to save after the currently selected preset. let request: any = { instanceId: pluginInstanceId, name: newName, }; return nullCast(this.webSocket) .request("savePluginPresetAs", request) .then((data) => { return data; }); } loadPreset(instanceId: number): void { this.webSocket?.send("loadPreset", instanceId); } updatePresets(presets: PresetIndex): Promise { return nullCast(this.webSocket).request("updatePresets", presets); } moveBank(from: number, to: number): Promise { return nullCast(this.webSocket).request("moveBank", { from: from, to: to }); } deletePresetItems(instanceIds: Set): Promise { return nullCast(this.webSocket).request("deletePresetItems", Array.from(instanceIds)); } deleteBankItem(instanceId: number): Promise { return nullCast(this.webSocket).request("deleteBankItem", instanceId); } renamePresetItem(instanceId: number, name: string): Promise { let body: RenamePresetBody = { clientId: this.clientId, instanceId: instanceId, name: name }; return nullCast(this.webSocket).request("renamePresetItem", body); } copyPreset(fromId: number, toId: number = -1): Promise { let body: any = { clientId: this.clientId, fromId: fromId, toId: toId }; return nullCast(this.webSocket).request("copyPreset", body); } duplicatePreset(instanceId: number): Promise { return this.copyPreset(instanceId, -1); } showAlert(message: any): void { let m: string; if (message instanceof Error) { let e = message as Error; if (e.message) { m = e.message as string; } else { m = e.toString(); } } else { m = message.toString(); } this.alertMessage.set(m); } 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("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.vuSubscriptions[instanceId] = newTarget; this.webSocket.request("addVuSubscription", instanceId) .then((subscriptionHandle) => { newTarget.serverSubscriptionHandle = subscriptionHandle; if (newTarget.subscribers.length === 0) { this.webSocket?.send("removeVuSubscription", subscriptionHandle); } else { newTarget.serverSubscriptionHandle = subscriptionHandle; } }).catch((error) => { // failed to subscribe. this.vuSubscriptions[instanceId] = undefined; }); 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; } } } } } } private isClosed = false; close() { if (!this.isClosed) { this.isClosed = true; this.webSocket?.close(); this.webSocket = undefined; } } setPatchProperty(instanceId: number, uri: string, value: any): Promise { let result = new Promise((resolve, reject) => { if (this.webSocket) { this.webSocket.request( "setPatchProperty", { instanceId: instanceId, propertyUri: uri, value: value } ).then((result) => { resolve(true); }).catch((message) => { reject(message); }); } else { reject("Socket closed."); } }); return result; } getPatchProperty(instanceId: number, uri: string): Promise { let result = new Promise((resolve, reject) => { let pedalboard = this.pedalboard.get(); if (pedalboard) { try { let item = pedalboard.getItem(instanceId); if (item) { if (item.pathProperties.hasOwnProperty(uri)) { let value = item.pathProperties[uri]; let jsonValue = JSON.parse(value); resolve(jsonValue as Type); } } } catch (error) { reject(error); return; } } if (!this.webSocket) { reject("Socket closed."); } else { this.webSocket.request("getPatchProperty", { instanceId: instanceId, propertyUri: uri }) .then((data) => { resolve(data); }) .catch((error) => { reject(error + " (" + uri + ")"); }); } }); return result; } openBank(bankId: number): Promise { let result = new Promise((resolve, reject) => { if (this.webSocket) { this.webSocket.request("openBank", bankId) .then(() => { resolve(); }) .catch((error) => { reject(error); }); } }); return result; } renameBank(bankId: number, newName: string): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("No server connection."); } else { this.webSocket.request("renameBank", { bankId: bankId, newName: newName }) .then(() => { resolve(); }) .catch(error => reject(error)); } }); return result; } saveBankAs(bankId: number, newName: string): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("No server connection."); } else { this.webSocket.request("saveBankAs", { bankId: bankId, newName: newName }) .then((newInstanceId) => { resolve(newInstanceId); }) .catch(error => reject(error)); } }); return result; } getJackStatus(): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("No connection to server."); } else { this.webSocket.request("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 { return new Promise((resolve, reject) => { if (this.presetCache[uri] !== undefined) { resolve(this.presetCache[uri]); } else { this.webSocket!.request("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 { this.presetCache[pluginUri] = presets; this.firePluginPresetsChanged(pluginUri); return nullCast(this.webSocket).request("updatePluginPresets", presets); } duplicatePluginPreset(pluginUri: string, instanceId: number): Promise { return nullCast(this.webSocket).request("copyPluginPreset", { pluginUri: pluginUri, instanceId: instanceId }); } shutdown(): Promise { return this.webSocket!.request("shutdown"); } restart(): Promise { return this.webSocket!.request("restart"); } getJackServerSettings(): Promise { return this.webSocket!.request("getJackServerSettings") .then((data) => { return new JackServerSettings().deserialize(data); }); } setJackServerSettings(jackServerSettings: JackServerSettings): void { this.webSocket?.request("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.setModelPedalboard(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(onComplete: (midiMessage: MidiMessage) => void): ListenHandle { let handle = this.nextListenHandle++; this.midiListeners.push(new MidiEventListener(handle, onComplete)); this.webSocket?.send("listenForMidiEvent", { 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); this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle); break; } } } private handleNotifyPathPatchPropertyChanged( instanceId: number, propertyUri: string, jsonObjectString: string ) { let pedalboard = this.pedalboard.get(); let pedalboardItem = pedalboard.getItem(instanceId); if (pedalboardItem) { pedalboardItem.pathProperties[propertyUri] = jsonObjectString; } // No NOT notify monitorPatchProperty listeners, because they extpect objects not strings, // AND they will a NotifyPatchPropertychanged message anyway. } private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) { let pedalboard = this.pedalboard.get(); try { let pedalboardItem = pedalboard.tryGetItem(instanceId); if (pedalboardItem && pedalboardItem.pathProperties[propertyUri] !== undefined) { pedalboardItem.pathProperties[propertyUri] = JSON.stringify(jsonObject); } } catch (ignored) { // e.g. notification for a pedalboard item that is no longer valid. } 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, cc0: number, cc1: number, cc2: number): void { let midiMessage = new MidiMessage(cc0, cc1, cc2); if (!midiMessage.isNote() && !midiMessage.isControl()) { return; } for (let i = 0; i < this.midiListeners.length; ++i) { let listener = this.midiListeners[i]; if (listener.handle === clientHandle) { listener.callback(midiMessage); } } } 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); } displayMediaFile(filePath: string) { let url = this.varServerUrl + "displayMediaFile?path=" + encodeURIComponent(filePath); window.open(url, "_blank"); } downloadAudioFile(filePath: string) { let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath); // download with no flashing temporary tab. let link = window.document.createElement("A") as HTMLAnchorElement; link.href = downloadUrl; link.target = "_blank"; link.setAttribute("download", pathFileName(filePath)); document.body.appendChild(link); link.click(); document.body.removeChild(link); } download(targetType: string, instanceId: number | string): void { if (instanceId === -1) return; let url = this.varServerUrl + targetType + "?id=" + instanceId; // window.open(url, "_blank"); // download with no flashing temporary tab. let link = window.document.createElement("A") as HTMLAnchorElement; link.href = url; link.target = "_blank"; link.download = "download.piPreset"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { let result = new Promise((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) => { let response = json as { errorMessage: string, path: string }; if (response.errorMessage !== "") { throw new Error(response.errorMessage); } resolve(response.path); }) .catch((error) => { if (error instanceof Error) { reject("Upload failed. " + (error as Error).message); } else { reject("Upload failed. " + error); } }) ; } catch (error) { reject("Upload failed. " + error); } }); return result; } uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise { let result = new Promise((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.toString()); }) ; } catch (error) { reject("Upload failed. " + error); } }); return result; } uploadBank(file: File, uploadAfter: number): Promise { let result = new Promise((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 { let newSettings = this.governorSettings.get().clone(); newSettings.governor = governor; this.governorSettings.set(newSettings); return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { resolve(); return; } ws.request( "setGovernorSettings", governor ) .then(() => { resolve(); }) .catch((err) => { reject(err); }); }); } createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { resolve(""); return; } ws.request( "createNewSampleDirectory", { relativePath: relativePath, uiFileProperty: uiFileProperty } ) .then((newPath) => { resolve(newPath); }) .catch((err) => { reject(err); }); }); } getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty, selectedPath: string): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { resolve(new FilePropertyDirectoryTree()); return; } ws.request( "getFilePropertyDirectoryTree", { fileProperty: uiFileProperty, selectedPath: selectedPath } ).then((result) => { resolve(new FilePropertyDirectoryTree().deserialize(result)); }).catch((e) => { reject(e); }); }); } renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { resolve(""); return; } ws.request( "renameFilePropertyFile", { oldRelativePath: oldRelativePath, newRelativePath: newRelativePath, uiFileProperty: uiFileProperty } ) .then((newPath) => { resolve(newPath); }) .catch((err) => { reject(err); }); }); } copyFilePropertyFile( oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty, overwrite: boolean ): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { resolve(""); return; } ws.request( "copyFilePropertyFile", { oldRelativePath: oldRelativePath, newRelativePath: newRelativePath, uiFileProperty: uiFileProperty, overwrite: overwrite } ) .then((newPath) => { resolve(newPath); }) .catch((err) => { reject(err); }); }); } setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise { let result = new Promise((resolve, reject) => { let oldSettings = this.wifiConfigSettings.get(); wifiConfigSettings = wifiConfigSettings.clone(); if ((!oldSettings.isEnabled()) && (!wifiConfigSettings.isEnabled())) { // no effective change. resolve(); return; } if (!wifiConfigSettings.isEnabled()) { wifiConfigSettings.hasPassword = false; wifiConfigSettings.password = ""; } else { if (wifiConfigSettings.hasPassword) { wifiConfigSettings.hasSavedPassword = true; } } // save a version for the server (potentially carrying a password) let serverConfigSettings: WifiConfigSettings; if (wifiConfigSettings.isEnabled()) { serverConfigSettings = wifiConfigSettings.clone(); } else { // avoid leaking edits to the server serverConfigSettings = oldSettings.clone(); serverConfigSettings.autoStartMode = wifiConfigSettings.autoStartMode; wifiConfigSettings = oldSettings.clone(); wifiConfigSettings.autoStartMode = 0; } wifiConfigSettings.hasPassword = false; wifiConfigSettings.password = ""; this.wifiConfigSettings.set(wifiConfigSettings); // notify the server. let ws = this.webSocket; if (!ws) { reject("Not connected."); return; } ws.request( "setWifiConfigSettings", serverConfigSettings ) .then(() => { resolve(); }) .catch((err) => { reject(err); }); }); return result; } setWifiDirectConfigSettings(wifiDirectConfigSettings: WifiDirectConfigSettings): Promise { let result = new Promise((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( "setWifiDirectConfigSettings", serverConfigSettings ) .then(() => { //resolve(); }) .catch((err) => { //resolve(); }); resolve(); }); this.expectDisconnect(ReconnectReason.LoadingSettings); // this.webSocket?.reconnect(); // avoid races by letting the server do it for us. return result; } getKnownWifiNetworks(): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("Connection closed."); return; } this.webSocket.request("getKnownWifiNetworks") .then((data) => { resolve(data); }) .catch((err) => reject(err)); }); return result; } getWifiChannels(countryIso3661: string): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("Connection closed."); return; } this.webSocket.request("getWifiChannels", countryIso3661) .then((data) => { resolve(WifiChannel.deserialize_array(data)); }) .catch((err) => reject(err)); }); return result; } getAlsaDevices(): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("Connection closed."); return; } this.webSocket.request("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); } } setUpdatePolicy(updatePolicy: UpdatePolicyT): void { let iPolicy = updatePolicy as number; if (this.webSocket) { this.webSocket.send("setUpdatePolicy", iPolicy); } } forceUpdateCheck() { if (this.webSocket) { this.webSocket.send("forceUpdateCheck"); } } 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 { return nullCast(this.webSocket).request("newPreset"); } getTheme(): ColorTheme { return getColorScheme(); } setTheme(value: ColorTheme) { if (this.getTheme() !== value) { setColorScheme(value); setTimeout(() => { this.reloadPage(); }, 200); } } reloadPage() { // eslint-disable-next-line no-restricted-globals let url = window.location.href.split('#')[0]; window.location.href = url; //window.location.reload(); } private networkChanging_expectHotspot = false; private expectNetworkChangeTimeout?: number = undefined; private hotspotReconnectTimer?: number = undefined; async detectServer(address: string) { let port = window.location.port; let newUrl = new URL("http://" + address + ":" + port + "/manifest.json"); try { let response = await fetch(newUrl); if (response.ok) { return "http://" + address + ":" + port; } return ""; } catch (error: any) { return ""; } } async pollForLiveServer() { let wifiConfigSettings = this.wifiConfigSettings.get(); if (wifiConfigSettings.mdnsName.length !== 0) { let newUrl = await this.detectServer(wifiConfigSettings.mdnsName); if (newUrl.length !== 0) { return newUrl; } } if (this.networkChanging_expectHotspot) { let newUrl = await this.detectServer("10.40.0.1"); if (newUrl.length !== 0) { return newUrl; } } return ""; } cancelHotspotReconnectTimer() { if (this.hotspotReconnectTimer) { clearTimeout(this.hotspotReconnectTimer); } } startHotspotReconnectTimer() { // poll for access to a running pipedal server this.hotspotReconnectTimer = setTimeout( async () => { let newUrl = await this.pollForLiveServer(); if (newUrl.length === 0) { this.startHotspotReconnectTimer(); } else { this.cancelOnNetworkChanging(); window.location.replace(newUrl); } }, 5 * 1000); } cancelOnNetworkChanging() { this.cancelHotspotReconnectTimer(); if (this.expectNetworkChangeTimeout) { clearTimeout(this.expectNetworkChangeTimeout); this.expectNetworkChangeTimeout = undefined; } this.expectDisconnect(ReconnectReason.Disconnected); } onNetworkChanging(hotspotConnected: boolean) { this.cancelOnNetworkChanging(); this.networkChanging_expectHotspot = hotspotConnected; this.expectDisconnect(ReconnectReason.HotspotChanging); this.expectNetworkChangeTimeout = setTimeout( () => { this.cancelOnNetworkChanging(); }, 30 * 1000); } setPedalboardItemTitle(instanceId: number, title: string, iconColor: string): void { let pedalboard = this.pedalboard.get(); if (!pedalboard) { throw new PiPedalStateError("Pedalboard not loaded."); } let newPedalboard = pedalboard.clone(); this.updateVst3State(newPedalboard); let item = newPedalboard.getItem(instanceId); if (item.title === title && item.iconColor === iconColor) { return; } item.title = title; item.iconColor = iconColor; this.pedalboard.set(newPedalboard); // notify the server. this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title, colorKey: iconColor }); } setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void { this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); } async getAlsaSequencerConfiguration(): Promise { if (this.webSocket) { let result = await this.webSocket.request("getAlsaSequencerConfiguration"); return new AlsaSequencerConfiguration().deserialize(result); } throw new Error("No connection."); } async getAlsaSequencerPorts(): Promise { if (this.webSocket) { let result = await this.webSocket.request("getAlsaSequencerPorts"); return AlsaSequencerPortSelection.deserialize_array(result); } throw new Error("No connection."); } private pedalboardItemEnabledChangeListeners: PedalboardItemEnabledChangeItem[] = []; private pedalboardItemUseModUiChangeListeners: PedalboardItemUseModUiChangeItem[] = []; private updatePedalboardItemEnabled(instanceId: number, enabled: boolean) { for (let listener of this.pedalboardItemEnabledChangeListeners) { if (listener.instanceId === instanceId) { if (listener.currentValue !== enabled) { listener.currentValue = enabled; listener.onEnabledChanged(instanceId, enabled); } } } } private updatePedalboardItemUseModUi(instanceId: number, useModUi: boolean) { for (let listener of this.pedalboardItemUseModUiChangeListeners) { if (listener.instanceId === instanceId) { if (listener.currentValue !== useModUi) { listener.currentValue = useModUi; listener.onUseModUiChanged(instanceId, useModUi); } } } } addPedalboardItemEnabledChangeListener( instanceId: number, onEnabledChanged: PedalboardItemEnabledChangeCallback ): ListenHandle { let handle = ++this.nextListenHandle; let currentValue = this.getPedalboardItemEnabled(instanceId); let entry: PedalboardItemEnabledChangeItem = { instanceId: instanceId, handle: handle, currentValue: currentValue, onEnabledChanged: onEnabledChanged }; this.pedalboardItemEnabledChangeListeners.push(entry); onEnabledChanged(instanceId, currentValue); return { _handle: handle }; } addPedalboardItemUseModUiChangeListener( instanceId: number, onUseModUiChanged: PedalboardItemUseModUiChangeCallback ): ListenHandle { let handle = ++this.nextListenHandle; let currentValue = this.getPedalboardItemUseModUi(instanceId); let entry: PedalboardItemUseModUiChangeItem = { instanceId: instanceId, handle: handle, currentValue: currentValue, onUseModUiChanged: onUseModUiChanged }; this.pedalboardItemUseModUiChangeListeners.push(entry); onUseModUiChanged(instanceId, currentValue); return { _handle: handle }; } removePedalboardItemEnabledChangeListener(listenHandle: ListenHandle): boolean { for (let i = 0; i < this.pedalboardItemEnabledChangeListeners.length; ++i) { if (this.pedalboardItemEnabledChangeListeners[i].handle === listenHandle._handle) { this.pedalboardItemEnabledChangeListeners.splice(i, 1); return true; } } return false; } removePedalboardItemUseModUiChangeListener(listenHandle: ListenHandle): boolean { for (let i = 0; i < this.pedalboardItemUseModUiChangeListeners.length; ++i) { if (this.pedalboardItemUseModUiChangeListeners[i].handle === listenHandle._handle) { this.pedalboardItemUseModUiChangeListeners.splice(i, 1); return true; } } return false; } setKeepScreenOn(keepScreenOn: boolean): void { if (this.canKeepScreenOn) { this.keepScreenOn.set(keepScreenOn); if (this.androidHost) { this.androidHost.setKeepScreenOn(keepScreenOn); } } } getKeepScreenOn(): boolean { return this.keepScreenOn.get(); } getScreenOrientation(): ScreenOrientation { return this.screenOrientation.get(); } setScreenOrientation(value: ScreenOrientation) { if (this.canSetScreenOrientation) { this.screenOrientation.set(value); this.androidHost?.setScreenOrientation(value as number); } } setPedalboardSelectedPlugin(pluginId: number): void { let pedalboard = this.pedalboard.get(); if (!pedalboard) { throw new PiPedalStateError("Pedalboard not loaded."); } if (pedalboard.selectedPlugin === pluginId) { return; // no change. } pedalboard.selectedPlugin = pluginId; // naughty, because it doesn't broadcast. // notify the server. this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId }); } requestBankPresets(bankInstanceId: number): Promise { return nullCast(this.webSocket).request("requestBankPresets", { bankInstanceId: bankInstanceId }); } importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise { return nullCast(this.webSocket).request("importPresetsFromBank", { bankInstanceId: bankInstanceId, presets: presets }); } copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise { return nullCast(this.webSocket).request("copyPresetsToBank", { bankInstanceId: bankInstanceId, presets: presets }); } setPedalboardSideChainInput(instanceId: number, sideChainInputId: number): void { let pedalboard = this.pedalboard.get().clone(); let pedalboardItem = pedalboard.getItem(instanceId); if (!pedalboardItem) { return; } pedalboardItem.sideChainInputId = sideChainInputId; let oldId = pedalboardItem.instanceId; let newId = ++pedalboard.nextInstanceId; pedalboardItem.instanceId = newId; // force reload of pedalboard. pedalboard.selectedPlugin = pedalboardItem.instanceId; this.updateSidechainReferences(pedalboard, oldId, newId); this.setModelPedalboard(pedalboard); this.updateServerPedalboard(); } removeInvalidSidechains(pedalboard: Pedalboard) { let it = pedalboard.itemsGeneratorSplitAfter(); let previousItemIds: Set = new Set(); previousItemIds.add(-2); // start is visible. while (true) { let r = it.next(); if (r.done) { break; } let item = r.value; if (item.sideChainInputId !== -1) { if (!previousItemIds.has(item.sideChainInputId)) { item.sideChainInputId = -1; } } if (!item.isEmpty()) { // empty items are not valid sidechain targets. previousItemIds.add(item.instanceId); } } } updateSidechainReferences(pedalboard: Pedalboard, oldItemId: number, newItemId: number) { let it = pedalboard.itemsGenerator(); while (true) { let r = it.next(); if (r.done) break; let item = r.value; if (item.sideChainInputId === oldItemId) { item.sideChainInputId = newItemId; } } this.removeInvalidSidechains(pedalboard); } setChannelRouterSettings(settings: ChannelRouterSettings) { this.channelRouterSettings.set(settings); if (this.webSocket) { this.webSocket.send("setChannelRouterSettings", settings); } } DownloadModelsFromTone3000( responseUri: string, tone3000PckceParams: Tone3000PkceParams, downloadPath: string, downloadType: Tone3000DownloadType ): void { if (this.webSocket) { this.webSocket.send("DownloadModelsFromTone3000", { responseUri: responseUri, tone3000PckceParams: tone3000PckceParams, downloadPath: downloadPath, downloadType: downloadType === Tone3000DownloadType.Nam ? 0 : 1 }); } } private t3kModelSelectionDialogListener?: T3kModelSelectionDialogListener = undefined; setT3kModelSelectionDialogListener( listener: T3kModelSelectionDialogListener | undefined ) { this.t3kModelSelectionDialogListener = listener; } showT3kModelSelectionDialog( downloadType: Tone3000DownloadType, toneName: string, models: Model[], onModelsSelected: (selectedModels: Model[]) => void, onCancel: () => void ) { if (!this.t3kModelSelectionDialogListener) { onCancel(); return; } this.tone3000Downloading.set(false); this.t3kModelSelectionDialogListener.onShowDialog( { downloadType: downloadType, toneName: toneName, models: models, onModelsSelected: (selectedModels: Model[]) => { this.tone3000Downloading.set(true); onModelsSelected(selectedModels); }, onCancel: () => { this.tone3000Downloading.set(false); onCancel(); } } ); } }; 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; } };