diff --git a/react/public/var/config.json b/react/public/var/config.json index a16e5a4..c1902a0 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 8080, + "socket_server_port": 80, "socket_server_address": "*", "max_upload_size": 1048576, diff --git a/react/src/ControlViewFactory.tsx b/react/src/ControlViewFactory.tsx index 54094b4..8857168 100644 --- a/react/src/ControlViewFactory.tsx +++ b/react/src/ControlViewFactory.tsx @@ -31,6 +31,8 @@ import ToobToneStackViewFactory from './ToobToneStackView'; import ToobCabSimViewFactory from './ToobCabSimView'; import ToobPowerStage2Factory from './ToobPowerStage2View'; import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView'; +import ToobMLViewFactory from './ToobMLView'; +import {ToobTunerViewFactory,GxTunerViewFactory> from './GxTunerView'; let pluginFactories: IControlViewFactory[] = [ @@ -38,7 +40,10 @@ let pluginFactories: IControlViewFactory[] = [ new ToobToneStackViewFactory(), new ToobCabSimViewFactory(), new ToobPowerStage2Factory(), - new ToobSpectrumAnalyzerViewFactory() + new ToobSpectrumAnalyzerViewFactory(), + new ToobMLViewFactory(), + new GxTunerViewFactory(), + new ToobTunerViewFactory() ]; diff --git a/react/src/GxTunerView.tsx b/react/src/GxTunerView.tsx new file mode 100644 index 0000000..eee7f95 --- /dev/null +++ b/react/src/GxTunerView.tsx @@ -0,0 +1,127 @@ +// Copyright (c) 2021 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; +import { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import createStyles from '@mui/styles/createStyles'; +import withStyles from '@mui/styles/withStyles'; + +import IControlViewFactory from './IControlViewFactory'; +import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; +import { PedalBoardItem } from './PedalBoard'; +import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; +import GxTunerControl from './GxTunerControl'; + +const GXTUNER_URI = "http://guitarix.sourceforge.net/plugins/gxtuner#tuner"; +const TOOBTUNER_URI = "http://two-play.com/plugins/toob-tuner"; + + + +const styles = (theme: Theme) => createStyles({ +}); + +interface GxTunerProps extends WithStyles { + instanceId: number; + item: PedalBoardItem; + +} +interface GxTunerState { + +} + +const GxTunerView = + withStyles(styles, { withTheme: true })( + class extends React.Component + implements ControlViewCustomization + { + model: PiPedalModel; + + customizationId: number = 1; + + constructor(props: GxTunerProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + } + } + + + getControlIndex(key: string): number { + let item = this.props.item; + for (let i = 0; i < item.controlValues.length; ++i) + { + if (item.controlValues[i].key === key) + { + return i; + } + } + throw new Error("GxTuner: Control '" + key + "' not found."); + } + ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] + { + let refFreqIndex = this.getControlIndex("REFFREQ"); + let thresholdIndex = this.getControlIndex("THRESHOLD"); + + let result: (React.ReactNode | ControlGroup)[] = []; + + let tunerControl = (); + result.push(tunerControl); + + result.push(controls[refFreqIndex]); + result.push(controls[thresholdIndex]); + return result; + } + render() { + return (); + } + } + ); + + + +class GxTunerViewFactory implements IControlViewFactory { + uri: string = GXTUNER_URI; + + Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode { + return (); + } + + +} + +// ToobTuner uses the same controls and control semantics. +class ToobTunerViewFactory implements IControlViewFactory { + uri: string = TOOBTUNER_URI; + + Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode { + return (); + } + + +} + +export ToobTunerViewFactory; +export GxTunerViewFactory; \ No newline at end of file diff --git a/react/src/JackServerSettingsDialog.tsx b/react/src/JackServerSettingsDialog.tsx index 91672f6..1c2725c 100644 --- a/react/src/JackServerSettingsDialog.tsx +++ b/react/src/JackServerSettingsDialog.tsx @@ -332,7 +332,7 @@ const JackServerSettingsDialog = withStyles(styles)( > {bufferSizes.map((buffSize) => ( - {buffSize} + {buffSize} ) )} diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index 1958d12..bf619fd 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -55,6 +55,9 @@ const NARROW_DISPLAY_THRESHOLD = 600; const FILTER_STORAGE_KEY = "com.twoplay.pipedal.load_dlg.filter"; +let lastClickTime: number = 0; + + const pluginGridStyles = (theme: Theme) => createStyles({ frame: { @@ -338,8 +341,26 @@ export const LoadPluginDialog = } } + onClick(e: SyntheticEvent, uri: string): void { + e.preventDefault(); + + let currentTime = Date.now(); + let dt = currentTime-lastClickTime; + let DOUBLE_CLICK_TIME = 500; + let isDoubleClick = dt < DOUBLE_CLICK_TIME; + lastClickTime = currentTime; + + this.setState({ selected_uri: uri }); + + // we have to synthesize double clicks because + // DOM rewrites interfere with natural double click. + if (isDoubleClick) + { + this.props.onOk(uri); + } + } handleMouseEnter(e: SyntheticEvent, uri: string): void { // this.setHoverUri(uri); @@ -373,7 +394,7 @@ export const LoadPluginDialog = for (let i = 0; i < classNode.children.length; ++i) { let child = classNode.children[i]; let name = "\u00A0".repeat(level * 3 + 1) + child.display_name; - result.push(({name})); + result.push(({name})); if (child.children.length !== 0) { this.createFilterChildren(result, child, level + 1); } @@ -383,7 +404,7 @@ export const LoadPluginDialog = let classes = this.model.plugin_classes.get(); let result: ReactNode[] = []; - result.push(( All)); + result.push(( All)); this.createFilterChildren(result, classes, 1); return result; diff --git a/react/src/Lv2Plugin.tsx b/react/src/Lv2Plugin.tsx index 69b636c..a4cb89c 100644 --- a/react/src/Lv2Plugin.tsx +++ b/react/src/Lv2Plugin.tsx @@ -42,6 +42,7 @@ export class Port implements Deserializable { this.is_atom_port = input.is_atom_port; this.is_valid = input.is_valid; this.supports_midi = input.supports_midi; + this.supports_time_position = input.supports_time_position; this.port_group = input.port_group; return this; } @@ -70,6 +71,7 @@ export class Port implements Deserializable { is_atom_port: boolean = false; is_valid: boolean = false; supports_midi: boolean = false; + supports_time_position: boolean = false; port_group: string = ""; } diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 276a370..75e1729 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -311,7 +311,7 @@ export interface PiPedalModel { presets: ObservableProperty; - zoomedUiControl: ObservableProperty< ZoomedControlInfo| undefined >; + zoomedUiControl: ObservableProperty; setJackServerSettings(jackServerSettings: JackServerSettings): void; setMidiBinding(instanceId: number, midiBinding: MidiBinding): void; @@ -382,7 +382,7 @@ export interface PiPedalModel { cancelListenForMidiEvent(listenHandle: ListenHandle): void; addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle - removeControlValueChangeListener(handle: ControlValueChangedHandle): void; + removeControlValueChangeListener(handle: ControlValueChangedHandle): void; listenForAtomOutput(instanceId: number, onComplete: (instanceId: number, atomOutput: any) => void): ListenHandle; cancelListenForAtomOutput(listenHandle: ListenHandle): void; @@ -398,9 +398,9 @@ export interface PiPedalModel { getWifiChannels(countryIso3661: string): Promise; - getAlsaDevices() : Promise; + getAlsaDevices(): Promise; - zoomUiControl(sourceElement: HTMLElement,instanceId: number,uiControl: UiControl): void; + zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void; clearZoomedControl(): void; }; @@ -438,7 +438,7 @@ class PiPedalModelImpl implements PiPedalModel { new PresetIndex() ); - zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); + zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); svgImgUrl(svgImage: string): string { //return this.varServerUrl + "img/" + svgImage; @@ -466,7 +466,7 @@ class PiPedalModelImpl implements PiPedalModel { onSocketReconnecting(retry: number, maxRetries: number): void { if (this.visibilityState.get() === VisibilityState.Hidden) return; //if (retry !== 0) { - this.setState(State.Reconnecting); + this.setState(State.Reconnecting); //} } @@ -536,11 +536,11 @@ class PiPedalModelImpl implements PiPedalModel { this.handleNotifyMidiListener(clientHandle, isNote, noteOrControl); } else if (message === "onNotifyAtomOut") { let clientHandle = body.clientHandle as number; - let instanceId = body.instanceId as number; + let instanceId = body.instanceId as number; let atomJson = body.atomJson as string; - this.handleNotifyAtomOutput(clientHandle, instanceId,atomJson); + this.handleNotifyAtomOutput(clientHandle, instanceId, atomJson); if (header.replyTo) { - this.webSocket?.reply(header.replyTo,"onNotifyAtomOut",true); + this.webSocket?.reply(header.replyTo, "onNotifyAtomOut", true); } } else if (message === "onControlChanged") { let controlChangedBody = body as ControlChangedBody; @@ -733,6 +733,10 @@ class PiPedalModelImpl implements PiPedalModel { }) .then((clientId) => { this.clientId = clientId; + return this.getWebSocket().request("imageList"); + }) + .then((data) => { + this.preloadImages(data); return true; }) .catch((error) => { @@ -842,20 +846,16 @@ class PiPedalModelImpl implements PiPedalModel { } - exitBackgroundState() - { - if (this.state.get() === State.Background) - { + exitBackgroundState() { + if (this.state.get() === State.Background) { console.log("Exiting background state."); this.visibilityState.set(VisibilityState.Visible); this.webSocket?.exitBackgroundState(); } } - enterBackgroundState() - { - if (this.state.get() !== State.Background) - { + enterBackgroundState() { + if (this.state.get() !== State.Background) { console.log("Entering background state."); this.visibilityState.set(VisibilityState.Hidden); this.setState(State.Background); @@ -864,12 +864,10 @@ class PiPedalModelImpl implements PiPedalModel { } - onVisibilityChanged(doc: Document, event: Event) : any - { + onVisibilityChanged(doc: Document, event: Event): any { if (document.visibilityState) { - - switch (document.visibilityState) - { + + switch (document.visibilityState) { case "visible": this.visibilityState.set(VisibilityState.Visible); this.exitBackgroundState(); @@ -898,8 +896,8 @@ class PiPedalModelImpl implements PiPedalModel { }); let t = this.onVisibilityChanged; - (document as any).addEventListener("visibilitychange",(doc: Document,event: Event) => { - return t(doc,event); + (document as any).addEventListener("visibilitychange", (doc: Document, event: Event) => { + return t(doc, event); }); } @@ -958,22 +956,18 @@ class PiPedalModelImpl implements PiPedalModel { } } - + _controlValueChangeItems: ControlValueChangeItem[] = []; - addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle - { + addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle { let handle = ++this.nextListenHandle; - this._controlValueChangeItems.push({ handle: handle, instanceId: instanceId,onValueChanged: onValueChanged}); - return { _ControlValueChangedHandle: handle}; + 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); + 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; } } @@ -992,12 +986,10 @@ class PiPedalModelImpl implements PiPedalModel { if (notifyServer) { this._setServerControl("setControl", instanceId, key, value); } - for (let i = 0; i < this._controlValueChangeItems.length; ++i) - { + for (let i = 0; i < this._controlValueChangeItems.length; ++i) { let item = this._controlValueChangeItems[i]; - if (instanceId === item.instanceId) - { - item.onValueChanged(key,value); + if (instanceId === item.instanceId) { + item.onValueChanged(key, value); } } } @@ -1557,7 +1549,7 @@ class PiPedalModelImpl implements PiPedalModel { listenForAtomOutput(instanceId: number, onComplete: (instanceId: number, atomOutput: any) => void): ListenHandle { let handle = this.nextListenHandle++; - this.atomOutputListeners.push(new AtomOutputListener(handle,instanceId, onComplete)); + this.atomOutputListeners.push(new AtomOutputListener(handle, instanceId, onComplete)); this.webSocket?.send("listenForAtomOutput", { instanceId: instanceId, handle: handle }); return { @@ -1570,7 +1562,7 @@ class PiPedalModelImpl implements PiPedalModel { for (let i = 0; i < this.atomOutputListeners.length; ++i) { let listener = this.atomOutputListeners[i]; if (listener.handle === clientHandle && listener.instanceId === instanceId) { - listener.callback(instanceId,jsonObject); + listener.callback(instanceId, jsonObject); } } } @@ -1760,8 +1752,7 @@ class PiPedalModelImpl implements PiPedalModel { return result; } - getAlsaDevices() : Promise - { + getAlsaDevices(): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { reject("Connection closed."); @@ -1776,15 +1767,22 @@ class PiPedalModelImpl implements PiPedalModel { return result; } - zoomUiControl(sourceElement: HTMLElement,instanceId: number,uiControl: UiControl): void - { - this.zoomedUiControl.set({source: sourceElement,instanceId: instanceId,uiControl: uiControl}); + zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void { + this.zoomedUiControl.set({ source: sourceElement, instanceId: instanceId, uiControl: uiControl }); } - clearZoomedControl(): void - { + clearZoomedControl(): void { this.zoomedUiControl.set(undefined); } + 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; + } + } }; diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index c393010..6700707 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -31,7 +31,6 @@ import PluginControl from './PluginControl'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import VuMeter from './VuMeter'; import { nullCast } from './Utility' -import GxTunerControl from './GxTunerControl'; import { PiPedalStateError } from './PiPedalError'; import Typography from '@mui/material/Typography'; import FullScreenIME from './FullScreenIME'; @@ -40,7 +39,6 @@ import FullScreenIME from './FullScreenIME'; export const StandardItemSize = { width: 80, height: 110 }; -const GXTUNER_URI = "http://guitarix.sourceforge.net/plugins/gxtuner#tuner"; const LANDSCAPE_HEIGHT_BREAK = 500; @@ -127,7 +125,7 @@ const styles = (theme: Theme) => createStyles({ paddingTop: 0, paddingBottom: 0, border: "2pt #AAA solid", - borderRadius: 4, + borderRadius: 8, elevation: 12, display: "flex", flexDirection: "row", flexWrap: "wrap", @@ -144,7 +142,7 @@ const styles = (theme: Theme) => createStyles({ paddingTop: 0, paddingBottom: 0, border: "2pt #AAA solid", - borderRadius: 10, + borderRadius: 8, elevation: 12, display: "flex", flexDirection: "row", flexWrap: "nowrap", @@ -152,7 +150,7 @@ const styles = (theme: Theme) => createStyles({ }, portGroupTitle: { position: "absolute", - top: -10, + top: -15, background: "white", marginLeft: 20, paddingLeft: 8, @@ -344,35 +342,6 @@ const PluginControlView = throw new Error("Not found."); } - getTunerControls(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes { - // display standard version of just a few controls. - let shortList: ControlValue[] = []; - // for (let i = 0; i < controlValues.length; ++i) - // { - // shortList.push(controlValues[i]); - // } - shortList.push(this.getControl(controlValues, "REFFREQ")); - shortList.push(this.getControl(controlValues, "THRESHOLD")); - - - let controls = shortList.map((controlValue) => ( - { this.onValueChanged(controlValue.key, value) }} - onPreviewChange={(value: number) => { this.onPreviewChange(controlValue.key, value) }} - requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)} - /> - )); - let tunerControl = (); - let result: ControlNodes = []; - result.push(tunerControl); - - for (let i = 0; i < controls.length; ++i) - { - result.push(controls[i]); - } - - return result; - } onImeValueChange(key: string, value: number) { this.model.setPedalBoardControlValue(this.props.instanceId, key, value); this.onImeClose(); @@ -464,11 +433,8 @@ const PluginControlView = let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR; let controlNodes: ControlNodes; - if (plugin.uri === GXTUNER_URI) { - controlNodes = this.getTunerControls(plugin, controlValues); - } else { - controlNodes = this.getStandardControlNodes(plugin, controlValues); - } + controlNodes = this.getStandardControlNodes(plugin, controlValues); + if (this.props.customization) { // allow wrapper class to insert/remove/rebuild controls. diff --git a/react/src/PluginInfoDialog.tsx b/react/src/PluginInfoDialog.tsx index 88ec6f5..e0c3f30 100644 --- a/react/src/PluginInfoDialog.tsx +++ b/react/src/PluginInfoDialog.tsx @@ -138,7 +138,7 @@ function makeControls(controls: UiControl[]) { { controls.map((control) => ( - + {control.name} diff --git a/react/src/PluginPresetSelector.tsx b/react/src/PluginPresetSelector.tsx index b66aabb..7d5193d 100644 --- a/react/src/PluginPresetSelector.tsx +++ b/react/src/PluginPresetSelector.tsx @@ -279,7 +279,8 @@ const PluginPresetSelector = Plugin presets { this.state.presets.map((preset) => { - return ( this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}); + return ( this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}); }) } {/* diff --git a/react/src/ToobFrequencyResponseView.tsx b/react/src/ToobFrequencyResponseView.tsx index 22fdd8b..f54a1de 100644 --- a/react/src/ToobFrequencyResponseView.tsx +++ b/react/src/ToobFrequencyResponseView.tsx @@ -46,6 +46,10 @@ const styles = (theme: Theme) => createStyles({ interface ToobFrequencyResponseProps extends WithStyles { instanceId: number; + maxDb?: number; + minDb?: number; + minFrequency?: number; + maxFrequency?: number; } interface ToobFrequencyResponseState { @@ -104,8 +108,8 @@ const ToobFrequencyResponseView = requestOutstanding: boolean = false; requestDeferred: boolean = false; - dbMinOpt: number = -35; - dbMaxOpt: number = 5; + dbMinDefault: number = -35; + dbMaxDefault: number = 5; dbTickSpacingOpt: number = 10; @@ -120,8 +124,6 @@ const ToobFrequencyResponseView = xMax: number = PLOT_WIDTH+4; yMin: number = 0; yMax: number = PLOT_HEIGHT; - dbMin: number = this.dbMaxOpt; // deliberately reversed to flip up and down. - dbMax: number = this.dbMinOpt; dbTickSpacing: number = this.dbTickSpacingOpt; toX(frequency: number): number @@ -189,6 +191,10 @@ const ToobFrequencyResponseView = currentPath: string = ""; + majorGridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode { + return (); + } + gridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode { return (); } @@ -199,27 +205,54 @@ const ToobFrequencyResponseView = for (var db = Math.ceil(this.dbMax/this.dbTickSpacing)*this.dbTickSpacing; db < this.dbMin; db += this.dbTickSpacing ) { var y = (db-this.dbMin)/(this.dbMax-this.dbMin)*(this.yMax-this.yMin); - result.push( - this.gridLine(this.xMin,y,this.xMax,y) - ); + if (db === 0) + { + result.push( + this.majorGridLine(this.xMin,y,this.xMax,y) + ); + + } else { + result.push( + this.gridLine(this.xMin,y,this.xMax,y) + ); + } } let decade0 = Math.pow(10,Math.floor(Math.log10(this.fMin))); for (var decade = decade0; decade < this.fMax; decade *= 10) { - for (var i = 0; i < 10; ++i) + for (var i = 1; i <= 10; ++i) { var f = decade*i; if (f > this.fMin && f < this.fMax) { var x = this.toX(f); - result.push(this.gridLine(x,this.yMin,x,this.yMax)); + if (i === 10) + { + result.push(this.majorGridLine(x,this.yMin,x,this.yMax)); + + } else { + result.push(this.gridLine(x,this.yMin,x,this.yMax)); + } } } } return result; } + dbMin: number = 5; + dbMax: number = -35; + render() { + // deliberately reversed to flip up and down. + this.dbMax = this.props.minDb ?? this.dbMinDefault; + this.dbMin = this.props.maxDb ?? this.dbMaxDefault; + + this.fMin = this.props.minFrequency ?? 30; + this.fMax = this.props.maxFrequency ?? 20000; + this.logMin = Math.log(this.fMin); + this.logMax = Math.log(this.fMax); + + let classes = this.props.classes; return (
diff --git a/react/src/ToobMLView.tsx b/react/src/ToobMLView.tsx new file mode 100644 index 0000000..2cc2e04 --- /dev/null +++ b/react/src/ToobMLView.tsx @@ -0,0 +1,93 @@ +// Copyright (c) 2021 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; +import { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import createStyles from '@mui/styles/createStyles'; +import withStyles from '@mui/styles/withStyles'; + +import IControlViewFactory from './IControlViewFactory'; +import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; +import { PedalBoardItem } from './PedalBoard'; +import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; +import ToobFrequencyResponseView from './ToobFrequencyResponseView'; + + + +const styles = (theme: Theme) => createStyles({ +}); + +interface ToobMLProps extends WithStyles { + instanceId: number; + item: PedalBoardItem; + +} +interface ToobMLState { + +} + +const ToobMLView = + withStyles(styles, { withTheme: true })( + class extends React.Component + implements ControlViewCustomization + { + model: PiPedalModel; + + customizationId: number = 1; + + constructor(props: ToobMLProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + } + } + + ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] + { + let group = controls[1] as ControlGroup; + group.controls.splice(0,0, + ( ) + ); + return controls; + } + render() { + return (); + } + } + ); + + + +class ToobMLViewFactory implements IControlViewFactory { + uri: string = "http://two-play.com/plugins/toob-ml"; + + Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode { + return (); + } + + +} +export default ToobMLViewFactory; \ No newline at end of file diff --git a/react/src/ToobToneStackView.tsx b/react/src/ToobToneStackView.tsx index d023aac..973a413 100644 --- a/react/src/ToobToneStackView.tsx +++ b/react/src/ToobToneStackView.tsx @@ -25,7 +25,7 @@ import createStyles from '@mui/styles/createStyles'; import withStyles from '@mui/styles/withStyles'; import IControlViewFactory from './IControlViewFactory'; -import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; +import { PiPedalModelFactory, PiPedalModel,ControlValueChangedHandle } from "./PiPedalModel"; import { PedalBoardItem } from './PedalBoard'; import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; import ToobFrequencyResponseView from './ToobFrequencyResponseView'; @@ -41,7 +41,7 @@ interface ToobToneStackProps extends WithStyles { } interface ToobToneStackState { - + isBaxandall: boolean; } const ToobToneStackView = @@ -57,14 +57,48 @@ const ToobToneStackView = super(props); this.model = PiPedalModelFactory.getInstance(); this.state = { + isBaxandall: this.IsBaxandall() + } + } + IsBaxandall() : boolean { + return this.props.item.getControl("ampmodel").value === 2.0; + } + + + controlValueChangedHandle?: ControlValueChangedHandle; + componentDidMount() + { + this.controlValueChangedHandle = this.model.addControlValueChangeListener( + this.props.instanceId, + (key,value) => { + if (key === "ampmodel") + { + this.setState({isBaxandall: value === 2.0}); + } + } + ); + } + componentWillUnmount() { + if (this.controlValueChangedHandle) + { + this.model.removeControlValueChangeListener(this.controlValueChangedHandle); + this.controlValueChangedHandle = undefined; } } ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] { - controls.splice(0,0, - ( ) - ); + if (this.state.isBaxandall) + { + controls.splice(0,0, + ( ) + ); + } else { + controls.splice(0,0, + ( ) + ); + } return controls; } render() { diff --git a/src/JackHost.cpp b/src/JackHost.cpp index b45bff5..febb70a 100644 --- a/src/JackHost.cpp +++ b/src/JackHost.cpp @@ -754,7 +754,7 @@ public: #else xxx; // TODO! #endif - + int underrunMessagesGiven = 0; try { @@ -787,8 +787,12 @@ public: uint64_t underruns = this->underruns; if (underruns != lastUnderrunCount) { - Lv2Log::info("Jack - Underrun count: %lu", (unsigned long)underruns); - lastUnderrunCount = underruns; + if (underrunMessagesGiven < 60) // limit how much log file clutter we generate. + { + Lv2Log::info("Jack - Underrun count: %lu", (unsigned long)underruns); + lastUnderrunCount = underruns; + ++underrunMessagesGiven; + } } clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += pollRateS; diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp index 0aa7468..40498de 100644 --- a/src/Lv2Host.cpp +++ b/src/Lv2Host.cpp @@ -33,6 +33,7 @@ #include "lv2/urid.lv2/urid.h" #include "lv2.h" #include "lv2/atom.lv2/atom.h" +#include "lv2/time/time.h" #include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" #include "lv2/lv2plug.in/ns/ext/presets/presets.h" #include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" @@ -40,8 +41,10 @@ #include "lv2/lv2plug.in/ns/ext/port-groups/port-groups.h" #include #include "PiPedalException.hpp" + #include "Locale.hpp" + using namespace pipedal; #define MAP_CHECK() \ @@ -115,6 +118,12 @@ void Lv2Host::LilvUris::Initialize(LilvWorld*pWorld) symbolUri = lilv_new_uri(pWorld, LV2_CORE__symbol); nameUri = lilv_new_uri(pWorld, LV2_CORE__name); + time_Position = lilv_new_uri(pWorld, LV2_TIME__Position); + time_barBeat = lilv_new_uri(pWorld, LV2_TIME__barBeat); + time_beatsPerMinute = lilv_new_uri(pWorld, LV2_TIME__beatsPerMinute); + time_speed = lilv_new_uri(pWorld, LV2_TIME__speed); + + } void Lv2Host::LilvUris::Free() @@ -137,6 +146,11 @@ void Lv2Host::LilvUris::Free() symbolUri.Free(); nameUri.Free(); + time_Position.Free(); + time_barBeat.Free(); + time_beatsPerMinute.Free(); + time_speed.Free(); + } static std::string nodeAsString(const LilvNode *node) @@ -398,10 +412,12 @@ void Lv2Host::Load(const char *lv2Path) { Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); } +#if !SUPPORT_MIDI else if (pluginInfo->plugin_class() == LV2_MIDI_PLUGIN) { Lv2Log::debug("Plugin %s (%s) skipped. (MIDI Plugin).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); } +#endif else if (!pluginInfo->is_valid()) { auto &ports = pluginInfo->ports(); @@ -439,6 +455,16 @@ void Lv2Host::Load(const char *lv2Path) Lv2PluginUiInfo info(this, plugin.get()); if (plugin->is_valid()) { +#if SUPPORT_MIDI + if (info.audio_inputs() == 0 && !info.has_midi_input()) + { + Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str()); + } + else if (info.audio_outputs() == 0 && !info.has_midi_output()) + { + Lv2Log::debug("Plugin %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str()); + } +#else if (info.audio_inputs() == 0) { Lv2Log::debug("Plugin %s (%s) skipped. No audio inputs.", plugin->name().c_str(), plugin->uri().c_str()); @@ -447,6 +473,7 @@ void Lv2Host::Load(const char *lv2Path) { Lv2Log::debug("Plugin %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str()); } +#endif else { ui_plugins_.push_back(std::move(info)); @@ -718,6 +745,7 @@ Lv2PortInfo::Lv2PortInfo(Lv2Host *host, const LilvPlugin *plugin, const LilvPort is_cv_port_ = is_a(host, LV2_CORE__CVPort); supports_midi_ = lilv_port_supports_event(plugin, pPort, host->lilvUris.midiEventNode); + supports_time_position_ = lilv_port_supports_event(plugin, pPort, host->lilvUris.time_Position); NodeAutoFree designationValue = lilv_port_get(plugin, pPort, host->lilvUris.designationNode); designation_ = nodeAsString(designationValue); @@ -1096,6 +1124,7 @@ json_map::storage_type Lv2PortInfo::jmap{{ json_map::reference("is_valid", &Lv2PortInfo::is_valid_), json_map::reference("supports_midi", &Lv2PortInfo::supports_midi_), + json_map::reference("supports_time_position", &Lv2PortInfo::supports_time_position_), json_map::reference("port_group", &Lv2PortInfo::port_group_), json_map::reference("is_logarithmic", &Lv2PortInfo::is_logarithmic_), diff --git a/src/Lv2Host.hpp b/src/Lv2Host.hpp index 177a929..48cf10a 100644 --- a/src/Lv2Host.hpp +++ b/src/Lv2Host.hpp @@ -208,6 +208,7 @@ private: bool is_valid_ = false; bool supports_midi_ = false; + bool supports_time_position_ = false; bool is_logarithmic_ = false; int display_priority_ = -1; int range_steps_ = 0; @@ -267,6 +268,7 @@ public: LV2_PROPERTY_GETSET_SCALAR(is_cv_port); LV2_PROPERTY_GETSET_SCALAR(is_valid); LV2_PROPERTY_GETSET_SCALAR(supports_midi); + LV2_PROPERTY_GETSET_SCALAR(supports_time_position); LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(range_steps); @@ -381,6 +383,24 @@ public: } return false; } + bool hasMidiInput() const { + for (size_t i = 0; i < ports_.size(); ++i) + { + if (ports_[i]->is_atom_port() && ports_[i]->supports_midi() && ports_[i]->is_input()) + { + return true; + } + } + } + bool hasMidiOutput() const { + for (size_t i = 0; i < ports_.size(); ++i) + { + if (ports_[i]->is_atom_port() && ports_[i]->supports_midi() && ports_[i]->is_output()) + { + return true; + } + } + } public: @@ -598,6 +618,12 @@ private: LilvNodePtr symbolUri; LilvNodePtr nameUri; + LilvNodePtr time_Position; + LilvNodePtr time_barBeat; + LilvNodePtr time_beatsPerMinute; + LilvNodePtr time_speed; + + }; LilvUris lilvUris; diff --git a/src/PiPedalConfiguration.hpp b/src/PiPedalConfiguration.hpp index e3077c5..d2e87d1 100644 --- a/src/PiPedalConfiguration.hpp +++ b/src/PiPedalConfiguration.hpp @@ -36,6 +36,7 @@ class PiPedalConfiguration private: std::string lv2_path_ = "/usr/lib/lv2:/usr/local/lib/lv2"; std::filesystem::path docRoot_; + std::filesystem::path webRoot_; std::string local_storage_path_; bool mlock_ = true; std::vector reactServerAddresses_ = {"*:5000"}; @@ -52,7 +53,10 @@ public: std::filesystem::path GetConfigFilePath() const { return docRoot_ / "config.jason"; } - void Load(std::filesystem::path path) { + const std::filesystem::path& GetWebRoot() const { + return webRoot_; + } + void Load(const std::filesystem::path& path, const std::filesystem::path&webRoot) { std::filesystem::path configPath = path / "config.json"; if (!std::filesystem::exists(configPath)) { @@ -68,6 +72,8 @@ public: json_reader reader(f); reader.read(this); docRoot_ = path; + + webRoot_ = webRoot; } const std::filesystem::path &GetDocRoot() const { return docRoot_; } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 4a9ac60..ca5aced 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -113,7 +113,7 @@ void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration) void PiPedalModel::Load(const PiPedalConfiguration &configuration) { - + this->webRoot = configuration.GetWebRoot(); this->jackServerSettings.ReadJackConfiguration(); @@ -1164,4 +1164,9 @@ void PiPedalModel::cancelListenForAtomOutputs(int64_t clientId, int64_t clientHa std::vector PiPedalModel::GetAlsaDevices() { return this->alsaDevices.GetAlsaDevices(); -} \ No newline at end of file +} + +const std::filesystem::path& PiPedalModel::GetWebRoot() const +{ + return webRoot; +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 48f71a5..fd9e0b2 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -92,6 +92,7 @@ private: std::unique_ptr jackHost; JackConfiguration jackConfiguration; std::shared_ptr lv2PedalBoard; + std::filesystem::path webRoot; std::vector subscribers; @@ -219,7 +220,7 @@ public: void cancelListenForAtomOutputs(int64_t clientId, int64_t clientHandle); std::vector GetAlsaDevices(); - + const std::filesystem::path& GetWebRoot() const; }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index d88b83d..8f7b558 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -34,6 +34,7 @@ #include "WifiChannels.hpp" #include "SysExec.hpp" #include "PiPedalAlsa.hpp" +#include using namespace std; using namespace pipedal; @@ -344,6 +345,7 @@ private: std::recursive_mutex writeMutex; PiPedalModel &model; static std::atomic nextClientId; + std::string imageList; uint64_t clientId; // pedalboard is mutable and not thread-safe. @@ -390,6 +392,19 @@ public: PiPedalSocketHandler(PiPedalModel &model) : model(model), clientId(++nextClientId) { + std::stringstream imageList; + const std::filesystem::path& webRoot = model.GetWebRoot() / "img"; + bool firstTime = true; + for (const auto&entry: std::filesystem::directory_iterator(webRoot)) + { + if (!firstTime) + { + imageList << ";"; + } + firstTime = false; + imageList << entry.path().filename().string(); + } + this->imageList = imageList.str(); } private: @@ -1067,7 +1082,7 @@ public: std::lock_guard guard(subscriptionMutex); activeVuSubscriptions.push_back(VuSubscription{subscriptionHandle, instanceId}); } - this->Reply(replyTo, "addVuSubscriptin", subscriptionHandle); + this->Reply(replyTo, "addVuSubscription", subscriptionHandle); } else if (message == "removeVuSubscription") { @@ -1087,6 +1102,11 @@ public: } model.removeVuSubscription(subscriptionHandle); } + else if (message == "imageList") + { + + this->Reply(replyTo, "imageList", imageList); + } else { Lv2Log::error("Unknown message received: %s", message.c_str()); diff --git a/src/json.cpp b/src/json.cpp index e5ce2b9..2103f8d 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -416,6 +416,9 @@ void json_reader::skip_property() int c = is_.peek(); switch (c) { + case -1: + throw_format_error("Premature end of file."); + break; case '[': skip_array(); break; @@ -442,8 +445,10 @@ void json_reader::skip_string() consume('"'); while (true) { - char c; + int c; c = get(); + if (c == -1) throw_format_error("Premature end of file."); + if (c == '\"') { if (peek() == '\"') @@ -463,27 +468,54 @@ void json_reader::skip_string() void json_reader::skip_number() { skip_whitespace(); - char c; - while (true) + int c; + if (is_.peek() == '-') { - int ic = is_.peek(); - if (ic == -1) - break; - if (is_whitespace((char)ic)) + get(); + } + if (!std::isdigit(is_.peek())) + { + throw_format_error("Expecting a number."); + } + while (std::isdigit(is_.peek())) + { + get(); + } + if (is_.peek() == '.') + { + get(); + } + while (std::isdigit(is_.peek())) + { + get(); + } + c = is_.peek(); + if (c == 'e' || c == 'E') + { + get(); + c = is_.peek(); + if (c == '+' || c == 'i') { - break; + get(); } - c = get(); + while (std::isdigit(is_.peek())) + { + get(); + } + } } void json_reader::skip_array() { - char c; + int c; consume('['); while (true) { - if (peek() == ']') + c = peek(); + if (c == -1) throw_format_error("Premature end of file."); + + if (c == ']') { c = get(); break; @@ -499,11 +531,13 @@ void json_reader::skip_array() void json_reader::skip_object() { - char c; + int c; consume('{'); while (true) { - if (peek() == '}') + c = peek(); + if (c == -1) throw_format_error("Premature end of file."); + if (c == '}') { c = get(); break; diff --git a/src/json.hpp b/src/json.hpp index cfb127c..0c61530 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -27,6 +27,7 @@ #include #include "HtmlHelper.hpp" #include +#include #include "PiPedalException.hpp" @@ -243,6 +244,7 @@ class json_writer { public: const char*CRLF; private: + bool allowNaN_ = true; std::ostream &os; int indent_level; bool compressed; @@ -276,13 +278,17 @@ public: os << text; } using string_view = boost::string_view; - json_writer(std::ostream &os, bool compressed = false) + json_writer(std::ostream &os, bool compressed = false, bool allowNaN = true) : os(os) , compressed(compressed) + , allowNaN_(allowNaN) , indent_level(0) { this->CRLF = compressed? "" : "\r\n"; } + bool allowNaN() const { return allowNaN_; } + void allowNaN(bool allow) { allowNaN_ = allow; } + void write(long long value) { os << value; @@ -342,11 +348,21 @@ public: } void write(float f) { - os << std::setprecision(std::numeric_limits::max_digits10) << f; // round-trip format + if (allowNaN_ && (std::isnan(f) || std::isinf(f))) + { + os << "NaN"; + } else { + os << std::setprecision(std::numeric_limits::max_digits10) << f; // round-trip format + } } void write(double f) { - os << std::setprecision(std::numeric_limits::max_digits10) << f; // round-trip format + if (allowNaN_ && (std::isnan(f) || std::isinf(f))) + { + os << "NaN"; + } else { + os << std::setprecision(std::numeric_limits::max_digits10) << f; // round-trip format + } } template void write(const std::vector &value) @@ -526,20 +542,26 @@ public: class json_reader { +private: std::istream &is_; const uint16_t UTF16_SURROGATE_1_BASE = 0xD800U; const uint16_t UTF16_SURROGATE_2_BASE = 0xDC00U; const uint16_t UTF16_SURROGATE_MASK = 0x3FFU; + bool allowNaN_ = true; + public: - json_reader(std::istream &input) + json_reader(std::istream &input, bool allowNaN = true) : is_(input) { - + this->allowNaN_ = allowNaN; } + bool allowNaN() const { return allowNaN_;} + void allowNaN(bool allow) { allowNaN_ = allow; } + private: void throw_format_error(const char*error); @@ -719,11 +741,32 @@ public: void read(float*value) { skip_whitespace(); + if (allowNaN_) + { + if (peek() == 'N') + { + consumeToken("NaN","Expecting a number."); + *value = std::nanf(""); + return; + } + } is_ >> *value; if (is_.fail()) throw PiPedalException("Invalid format."); } void read(double*value) { skip_whitespace(); + if (allowNaN_) + { + if (peek() == 'N') + { + consumeToken("NaN","Expecting a number."); + + *value = std::nan(""); + return; + } + } + + is_ >> *value; if (is_.fail()) throw PiPedalException("Invalid format."); } diff --git a/src/main.cpp b/src/main.cpp index cd9488b..d061155 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -480,7 +480,7 @@ int main(int argc, char *argv[]) PiPedalConfiguration configuration; try { - configuration.Load(doc_root); + configuration.Load(doc_root,web_root); } catch (const std::exception &e) { diff --git a/src/pch.h b/src/pch.h index d5215fb..f3c3fc8 100644 --- a/src/pch.h +++ b/src/pch.h @@ -22,6 +22,10 @@ // Diagnostic type used to report calculated type T in an error message. template class TypeDisplay; +#ifndef SUPPORT_MIDI // currently, only whether midi plugins can be loaded. No routing or handling implemented (yet). +#define SUPPORT_MIDI 0 +#endif + #include #include