Precached images. ToobTuner. key additions.

This commit is contained in:
Robin Davies
2022-02-23 04:29:21 -05:00
parent ac51296782
commit f677b8b608
24 changed files with 589 additions and 137 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"socket_server_port": 8080,
"socket_server_port": 80,
"socket_server_address": "*",
"max_upload_size": 1048576,
+6 -1
View File
@@ -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()
];
+127
View File
@@ -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<typeof styles> {
instanceId: number;
item: PedalBoardItem;
}
interface GxTunerState {
}
const GxTunerView =
withStyles(styles, { withTheme: true })(
class extends React.Component<GxTunerProps, GxTunerState>
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 = (<GxTunerControl instanceId={this.props.instanceId} />);
result.push(tunerControl);
result.push(controls[refFreqIndex]);
result.push(controls[thresholdIndex]);
return result;
}
render() {
return (<PluginControlView
instanceId={this.props.instanceId}
item={this.props.item}
customization={this}
customizationId={this.customizationId}
/>);
}
}
);
class GxTunerViewFactory implements IControlViewFactory {
uri: string = GXTUNER_URI;
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
return (<GxTunerView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
}
}
// ToobTuner uses the same controls and control semantics.
class ToobTunerViewFactory implements IControlViewFactory {
uri: string = TOOBTUNER_URI;
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
return (<GxTunerView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
}
}
export ToobTunerViewFactory;
export GxTunerViewFactory;
+1 -1
View File
@@ -332,7 +332,7 @@ const JackServerSettingsDialog = withStyles(styles)(
>
{bufferSizes.map((buffSize) =>
(
<MenuItem value={buffSize}>{buffSize}</MenuItem>
<MenuItem key={"b"+buffSize} value={buffSize}>{buffSize}</MenuItem>
)
)}
+23 -2
View File
@@ -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((<MenuItem value={child.plugin_type}>{name}</MenuItem>));
result.push((<MenuItem key={child.plugin_type} value={child.plugin_type}>{name}</MenuItem>));
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((<MenuItem value={PluginType.Plugin}>&nbsp;All</MenuItem>));
result.push((<MenuItem key={PluginType.Plugin} value={PluginType.Plugin}>&nbsp;All</MenuItem>));
this.createFilterChildren(result, classes, 1);
return result;
+2
View File
@@ -42,6 +42,7 @@ export class Port implements Deserializable<Port> {
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<Port> {
is_atom_port: boolean = false;
is_valid: boolean = false;
supports_midi: boolean = false;
supports_time_position: boolean = false;
port_group: string = "";
}
+48 -50
View File
@@ -311,7 +311,7 @@ export interface PiPedalModel {
presets: ObservableProperty<PresetIndex>;
zoomedUiControl: ObservableProperty< ZoomedControlInfo| undefined >;
zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined>;
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<WifiChannel[]>;
getAlsaDevices() : Promise<AlsaDeviceInfo[]>;
getAlsaDevices(): Promise<AlsaDeviceInfo[]>;
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<ZoomedControlInfo | undefined> = new ObservableProperty<ZoomedControlInfo | undefined>(undefined);
zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined> = new ObservableProperty<ZoomedControlInfo | undefined>(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<string>("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<AlsaDeviceInfo[]>
{
getAlsaDevices(): Promise<AlsaDeviceInfo[]> {
let result = new Promise<AlsaDeviceInfo[]>((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;
}
}
};
+5 -39
View File
@@ -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) => (
<PluginControl instanceId={this.props.instanceId} uiControl={plugin.getControl(controlValue.key)} value={controlValue.value}
onChange={(value: number) => { this.onValueChanged(controlValue.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue.key, value) }}
requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)}
/>
));
let tunerControl = (<GxTunerControl instanceId={this.props.instanceId} />);
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.
+1 -1
View File
@@ -138,7 +138,7 @@ function makeControls(controls: UiControl[]) {
<Grid container direction="row" justifyContent="flex-start" alignItems="flex-start" spacing={1} style={{ paddingLeft: "24px" }}>
{
controls.map((control) => (
<Grid item spacing={2} xs={6} sm={4} key={control.symbol} >
<Grid xs={6} sm={4} key={control.symbol} >
<Typography variant="body2">
{control.name}
</Typography>
+2 -1
View File
@@ -279,7 +279,8 @@ const PluginPresetSelector =
<Typography variant="caption" color="textSecondary" style={{marginLeft: 16,marginTop:4,marginBottom: 4}}>Plugin presets</Typography>
{
this.state.presets.map((preset) => {
return (<MenuItem onClick={(e) => this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}</MenuItem>);
return (<MenuItem key={preset.presetUri}
onClick={(e) => this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}</MenuItem>);
})
}
{/*
+42 -9
View File
@@ -46,6 +46,10 @@ const styles = (theme: Theme) => createStyles({
interface ToobFrequencyResponseProps extends WithStyles<typeof styles> {
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 (<line x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.75" opacity="1" />);
}
gridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode {
return (<line x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.25" opacity="1" />);
}
@@ -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 (
<div className={classes.frame} >
+93
View File
@@ -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<typeof styles> {
instanceId: number;
item: PedalBoardItem;
}
interface ToobMLState {
}
const ToobMLView =
withStyles(styles, { withTheme: true })(
class extends React.Component<ToobMLProps, ToobMLState>
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,
( <ToobFrequencyResponseView instanceId={this.props.instanceId} minDb={-20} maxDb={20} />)
);
return controls;
}
render() {
return (<PluginControlView
instanceId={this.props.instanceId}
item={this.props.item}
customization={this}
customizationId={this.customizationId}
/>);
}
}
);
class ToobMLViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-ml";
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
return (<ToobMLView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
}
}
export default ToobMLViewFactory;
+39 -5
View File
@@ -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<typeof styles> {
}
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,
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
if (this.state.isBaxandall)
{
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId}
minDb={-20} maxDb={20} />)
);
} else {
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
}
return controls;
}
render() {
+7 -3
View File
@@ -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;
+29
View File
@@ -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 <fstream>
#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> 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_),
+26
View File
@@ -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;
+7 -1
View File
@@ -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<std::string> 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_; }
+7 -2
View File
@@ -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<AlsaDeviceInfo> PiPedalModel::GetAlsaDevices()
{
return this->alsaDevices.GetAlsaDevices();
}
}
const std::filesystem::path& PiPedalModel::GetWebRoot() const
{
return webRoot;
}
+2 -1
View File
@@ -92,6 +92,7 @@ private:
std::unique_ptr<JackHost> jackHost;
JackConfiguration jackConfiguration;
std::shared_ptr<Lv2PedalBoard> lv2PedalBoard;
std::filesystem::path webRoot;
std::vector<IPiPedalModelSubscriber*> subscribers;
@@ -219,7 +220,7 @@ public:
void cancelListenForAtomOutputs(int64_t clientId, int64_t clientHandle);
std::vector<AlsaDeviceInfo> GetAlsaDevices();
const std::filesystem::path& GetWebRoot() const;
};
} // namespace pipedal.
+21 -1
View File
@@ -34,6 +34,7 @@
#include "WifiChannels.hpp"
#include "SysExec.hpp"
#include "PiPedalAlsa.hpp"
#include <filesystem>
using namespace std;
using namespace pipedal;
@@ -344,6 +345,7 @@ private:
std::recursive_mutex writeMutex;
PiPedalModel &model;
static std::atomic<uint64_t> 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<std::recursive_mutex> 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());
+47 -13
View File
@@ -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;
+48 -5
View File
@@ -27,6 +27,7 @@
#include <sstream>
#include "HtmlHelper.hpp"
#include <cctype>
#include <cmath>
#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<float>::max_digits10) << f; // round-trip format
if (allowNaN_ && (std::isnan(f) || std::isinf(f)))
{
os << "NaN";
} else {
os << std::setprecision(std::numeric_limits<float>::max_digits10) << f; // round-trip format
}
}
void write(double f)
{
os << std::setprecision(std::numeric_limits<double>::max_digits10) << f; // round-trip format
if (allowNaN_ && (std::isnan(f) || std::isinf(f)))
{
os << "NaN";
} else {
os << std::setprecision(std::numeric_limits<double>::max_digits10) << f; // round-trip format
}
}
template <typename T> void write(const std::vector<T> &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.");
}
+1 -1
View File
@@ -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)
{
+4
View File
@@ -22,6 +22,10 @@
// Diagnostic type used to report calculated type T in an error message.
template <typename T> 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 <mutex>
#include <string>