From 5434f40ca16dda82546a8ae3edde9af205b68d06 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Mon, 1 Sep 2025 15:59:29 -0400 Subject: [PATCH] TooB Parametric EQ UI --- src/AudioHost.cpp | 2 +- src/PiPedalModel.cpp | 5 + src/PluginHost.cpp | 17 +- src/PluginHost.hpp | 6 +- todo.txt | 9 +- vite/CMakeLists.txt | 76 +- vite/src/pipedal/ControlViewFactory.tsx | 6 +- vite/src/pipedal/DialogEx.tsx | 4 +- vite/src/pipedal/GxTunerView.tsx | 4 +- vite/src/pipedal/Lv2Plugin.tsx | 9 + vite/src/pipedal/Pedalboard.tsx | 13 + vite/src/pipedal/PiPedalModel.tsx | 92 +-- vite/src/pipedal/PluginControl.tsx | 2 +- vite/src/pipedal/PluginControlView.tsx | 90 ++- vite/src/pipedal/PluginOutputControl.tsx | 64 +- vite/src/pipedal/ToobCabSimView.tsx | 4 +- .../src/pipedal/ToobFrequencyResponseView.tsx | 25 +- vite/src/pipedal/ToobInputStageView.tsx | 4 +- vite/src/pipedal/ToobMLView.tsx | 4 +- vite/src/pipedal/ToobNamView.tsx | 325 ++++++++ vite/src/pipedal/ToobParametricEqView.tsx | 724 ++++++++++++++++++ vite/src/pipedal/ToobPlayerView.tsx | 4 +- vite/src/pipedal/ToobPowerStage2View.tsx | 4 +- vite/src/pipedal/ToobSpectrumAnalyzerView.tsx | 4 +- vite/src/pipedal/ToobToneStackView.tsx | 4 +- .../pipedal/svg/expand_circle_down_24dp.svg | 1 + .../src/pipedal/svg/expand_circle_up_24dp.svg | 5 + 27 files changed, 1375 insertions(+), 132 deletions(-) create mode 100644 vite/src/pipedal/ToobNamView.tsx create mode 100644 vite/src/pipedal/ToobParametricEqView.tsx create mode 100644 vite/src/pipedal/svg/expand_circle_down_24dp.svg create mode 100644 vite/src/pipedal/svg/expand_circle_up_24dp.svg diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 79afb3c..2740516 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -1769,7 +1769,7 @@ public: std::lock_guard guard(mutex); this->currentPedalboard = pedalboard; - if (active) + if (active && pedalboard) { pedalboard->Activate(); this->activePedalboards.push_back(pedalboard); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 267124d..d5f20e1 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2040,6 +2040,11 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered // calibration is OFF when upgrading. pedalboardItem->SetControlValue("inputCalibrationMode",0.0f); } + pValue = pedalboardItem->GetControlValue("version"); + if (pValue == nullptr) + { + pedalboardItem->SetControlValue("version",0.0f); + } } diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index c7b8f51..78d9a12 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -127,6 +127,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); units__unit = lilv_new_uri(pWorld, LV2_UNITS__unit); + units__render = lilv_new_uri(pWorld, LV2_UNITS__render); invada_units__unit = lilv_new_uri(pWorld, "http://lv2plug.in/ns/extension/units#unit"); // a typo in invada plugin ttl files. invada_portprops__logarithmic = lilv_new_uri(pWorld, "http://lv2plug.in/ns/dev/extportinfo#logarithmic"); // a typo in invada plugin ttl files. @@ -1235,6 +1236,19 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP if (unitsValueUri) { this->units_ = UriToUnits(nodeAsString(unitsValueUri)); + if (this->units_ == Units::unknown) { + // try for a custom format string. + AutoLilvNode render = lilv_world_get( + pWorld, + unitsValueUri, + host->lilvUris->units__render, + nullptr); + if (render) { + const char*strRender = lilv_node_as_string(render); + custom_units_ = strRender; + } + + } } else { @@ -1964,6 +1978,7 @@ json_map::storage_type Lv2PortInfo::jmap{ MAP_REF(Lv2PortInfo, pipedal_ledColor), json_map::enum_reference("units", &Lv2PortInfo::units_, get_units_enum_converter()), + MAP_REF(Lv2PortInfo, custom_units), MAP_REF(Lv2PortInfo, comment)}}; json_map::storage_type Lv2PortGroup::jmap{{ @@ -2041,10 +2056,10 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ MAP_REF(Lv2PluginUiPort, pipedal_ledColor), json_map::enum_reference("units", &Lv2PluginUiPort::units_, get_units_enum_converter()), + MAP_REF(Lv2PluginUiPort, custom_units), MAP_REF(Lv2PluginUiPort, comment), MAP_REF(Lv2PluginUiPort, is_bypass), MAP_REF(Lv2PluginUiPort, is_program_controller), - MAP_REF(Lv2PluginUiPort, custom_units), MAP_REF(Lv2PluginUiPort, connection_optional), }}; diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 2cdb9cb..59ad1c2 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -233,6 +233,7 @@ namespace pipedal std::string designation_; bool is_bypass_ = false; Units units_ = Units::none; + std::string custom_units_; std::string comment_; PiPedalUI::ptr piPedalUI_; @@ -307,6 +308,7 @@ namespace pipedal LV2_PROPERTY_GETSET(port_group); LV2_PROPERTY_GETSET(comment); LV2_PROPERTY_GETSET_SCALAR(units); + LV2_PROPERTY_GETSET(custom_units); LV2_PROPERTY_GETSET(pipedal_ledColor); LV2_PROPERTY_GETSET(buffer_type); @@ -574,6 +576,7 @@ namespace pipedal trigger_property_(pPort->trigger_property()), pipedal_ledColor_(pPort->pipedal_ledColor()), comment_(pPort->comment()), units_(pPort->units()), + custom_units_(pPort->custom_units()), connection_optional_(pPort->connection_optional()) { // Use symbols to index port groups, instead of uris. @@ -626,10 +629,10 @@ namespace pipedal std::string pipedal_ledColor_; Units units_ = Units::none; + std::string custom_units_; std::string comment_; bool is_bypass_ = false; bool is_program_controller_ = false; - std::string custom_units_; bool connection_optional_ = false; public: @@ -768,6 +771,7 @@ namespace pipedal AutoLilvNode core__designation; AutoLilvNode portgroups__group; AutoLilvNode units__unit; + AutoLilvNode units__render; AutoLilvNode invada_units__unit; // typo in invada plugins. AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins. diff --git a/todo.txt b/todo.txt index 340a01b..7fd066e 100644 --- a/todo.txt +++ b/todo.txt @@ -1,5 +1,11 @@ -add apt install expat + +Versioning for legacy NAM presets! +Convert CRVB presets! + +double select on double-click/ok in file dialog. sudo apt install libbz2-dev +Move status to pedalboard view. +Default to dark theme. Carla Project icons. @@ -9,6 +15,7 @@ Exactly one underrun per seek in Toob Player json "skip" code doesn't work with complex objects (MidiChannelBinding specifically). +json nan/inf is fatal. - pipewire aux in? diff --git a/vite/CMakeLists.txt b/vite/CMakeLists.txt index b881fe9..da38a0e 100644 --- a/vite/CMakeLists.txt +++ b/vite/CMakeLists.txt @@ -3,40 +3,59 @@ find_program(NPM_COMMAND npm) -if (CMAKE_BUILD_TYPE MATCHES Debug) - set(BUILD_REACT "echo") - set(BUILD_REACT_ARGS Skipping react debug build) +if(CMAKE_BUILD_TYPE MATCHES Debug) + set(BUILD_REACT "echo") + set(BUILD_REACT_ARGS Skipping react debug build) else() set(BUILD_REACT npm) set(BUILD_REACT_ARGS run build) endif() -set (BUILD_DIRECTORY ${PROJECT_SOURCE_DIR}/vite/dist) +set(BUILD_DIRECTORY ${PROJECT_SOURCE_DIR}/vite/dist) add_custom_command( OUTPUT ${BUILD_DIRECTORY}/index.html COMMAND ${BUILD_REACT} - ARGS ${BUILD_REACT_ARGS} + ARGS ${BUILD_REACT_ARGS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/vite - DEPENDS + DEPENDS # find public src *.json *.ts -type f >/tmp/tmp.txt public/serve.json public/fonts/Roboto-BoldItalic.woff2 public/fonts/Roboto-MediumItalic.woff2 public/fonts/Roboto-Thin.woff2 public/fonts/Roboto-LightItalic.woff2 + public/fonts/epf/epf_lul-webfont.woff + public/fonts/epf/stylesheet.css + public/fonts/england-hand/stylesheet.css + public/fonts/england-hand/england-webfont.woff + public/fonts/comforta/Comfortaa-Bold.ttf + public/fonts/comforta/Comfortaa-Light.ttf + public/fonts/comforta/Comfortaa-Regular.ttf public/fonts/Roboto-Light.woff2 public/fonts/Roboto-BlackItalic.woff2 public/fonts/Roboto-ThinItalic.woff2 + public/fonts/cooper/cooperhewitt-book-webfont.woff2 + public/fonts/cooper/cooperhewitt-lightitalic-webfont.woff2 + public/fonts/cooper/cooperhewitt-bookitalic-webfont.woff2 + public/fonts/cooper/cooperhewitt-semibold-webfont.woff2 + public/fonts/cooper/cooperhewitt-light-webfont.woff2 public/fonts/LICENSE.txt public/fonts/Roboto-Regular.woff2 public/fonts/Roboto-Medium.woff2 public/fonts/Roboto-Black.woff2 public/fonts/Roboto-Bold.woff2 public/fonts/Roboto-Italic.woff2 + public/fonts/pirulen/stylesheet.css + public/fonts/pirulen/pirulen_rg-webfont.woff + public/fonts/questrial/questrial-regular-webfont.woff + public/fonts/questrial/stylesheet.css + public/fonts/nexa/Nexa_Free_Bold-webfont.woff + public/fonts/nexa/stylesheet.css public/robots.txt + public/css/modGui.css public/css/roboto.css public/manifest.json public/logo192.png @@ -48,6 +67,7 @@ add_custom_command( public/img/fx_lr.svg public/img/fx_pitch.svg public/img/vst.svg + public/img/missing_thumbnail.jpg public/img/ic_bank.svg public/img/fx_utility.svg public/img/fx_compressor.svg @@ -70,28 +90,35 @@ add_custom_command( public/img/fx_flanger2.svg public/img/fx_generator.svg public/img/Pi-Logo-3.png + public/img/red-light-on.png public/img/fx_oscillator.svg public/img/ic_drawer_2.svg public/img/ic_presets_white.svg public/img/fx_chorus.svg public/img/settings_black_24dp.svg + public/img/footswitch_strip.png + public/img/BlackKnob.png public/img/fx_analyzer.svg public/img/old_delete_outline_white_24dp.svg public/img/fx_reverb.svg public/img/fx_instrument.svg + public/img/audio-output.png public/img/ic_drawer_2.png public/img/cx_mono.svg public/img/fx_split_b.svg + public/img/default_album.jpg public/img/fx_filter_hp.svg public/img/fx_parametric_eq.svg public/img/fx_filter.svg public/img/fx_error.svg public/img/cx_stereo.svg public/img/fx_modulator.svg + public/img/red-light-off.png public/img/delete_outline_black_24dp.svg public/img/fx_gate.svg public/img/vst.png public/img/fx_function.svg + public/img/audio-input.png public/img/fx_terminal.svg public/img/fx_plugin.svg public/img/fx_distortion.svg @@ -109,30 +136,46 @@ add_custom_command( public/logo512.png public/sample_lv2_plugins.json src/pipedal/SplitUiControls.tsx + src/pipedal/IconButtonEx.tsx src/pipedal/JackHostStatus.tsx + src/pipedal/FontTest.tsx + src/pipedal/DraggableButtonBase.tsx + src/pipedal/AlsaSequencer.tsx src/pipedal/VirtualKeyboardHandler.tsx + src/pipedal/TimebaseselectorDialog.tsx src/pipedal/JackStatusView.tsx src/pipedal/Pedal.tsx src/pipedal/PiPedalError.tsx + src/pipedal/AudioFileMetadata.tsx src/pipedal/FilePropertyControl.tsx src/pipedal/PerformanceView.tsx src/pipedal/ChannelBindingsHelpDialog.tsx + src/pipedal/UseWindowSize.tsx src/pipedal/TemporaryDrawer.tsx src/pipedal/OkCancelDialog.tsx src/pipedal/SystemMidiBindingView.tsx src/pipedal/WifiConfigSettings.tsx + src/pipedal/Timebase.tsx + src/pipedal/LinkEx.tsx src/pipedal/react-app-env.d.ts + src/pipedal/FileUtils.tsx src/pipedal/UpdateDialog.tsx src/pipedal/PresetSelector.tsx + src/pipedal/OkDialog.tsx src/pipedal/JackServerSettings.tsx + src/pipedal/LoopDialog.tsx + src/pipedal/ToobNamView.tsx src/pipedal/SearchControl.tsx + src/pipedal/Tone3000AuthComplete.tsx src/pipedal/ObservableEvent.tsx src/pipedal/WindowScale.tsx src/pipedal/DialogStack.tsx src/pipedal/ToobToneStackView.tsx src/pipedal/TextFieldEx.tsx src/pipedal/AppThemed.css + src/pipedal/ScreenOrientation.tsx src/pipedal/PluginIcon.tsx + src/pipedal/ControlSlider.tsx src/pipedal/svg/snapshot_4.svg src/pipedal/svg/fx_dial.svg src/pipedal/svg/fx_spatial.svg @@ -141,9 +184,11 @@ add_custom_command( src/pipedal/svg/ic_save_bank_as.svg src/pipedal/svg/fx_lr.svg src/pipedal/svg/fx_pitch.svg + src/pipedal/svg/fx_nam.svg src/pipedal/svg/snapshot_3.svg src/pipedal/svg/ic_pluginpreset2.svg src/pipedal/svg/ic_bank.svg + src/pipedal/svg/mod_ui.svg src/pipedal/svg/fx_utility.svg src/pipedal/svg/fx_compressor.svg src/pipedal/svg/fx_phaser.svg @@ -155,6 +200,7 @@ add_custom_command( src/pipedal/svg/fx_amplifier.svg src/pipedal/svg/ic_help_outline.svg src/pipedal/svg/fx_eq.svg + src/pipedal/svg/pp_ui.svg src/pipedal/svg/fx_split_a.svg src/pipedal/svg/fx_spectral.svg src/pipedal/svg/fx_converter.svg @@ -191,6 +237,7 @@ add_custom_command( src/pipedal/svg/fx_empty.svg src/pipedal/svg/fx_flanger.svg src/pipedal/AboutDialog.tsx + src/pipedal/ToobPlayerView.tsx src/pipedal/NumericInput.tsx src/pipedal/SelectHoverBackground.tsx src/pipedal/MidiBindingView.tsx @@ -199,6 +246,7 @@ add_custom_command( src/pipedal/AndroidHost.tsx src/pipedal/SplitControlView.tsx src/pipedal/ToobFrequencyResponseView.tsx + src/pipedal/ButtonTooltip.tsx src/pipedal/ObservableProperty.tsx src/pipedal/UploadPresetDialog.tsx src/pipedal/Utility.tsx @@ -209,6 +257,8 @@ add_custom_command( src/pipedal/StringBuilder.tsx src/pipedal/ToobCabSimView.tsx src/pipedal/ToobSpectrumAnalyzerView.tsx + src/pipedal/ScratchClass.tsx + src/pipedal/ButtonEx.tsx src/pipedal/ControlTooltip.tsx src/pipedal/AlsaMidiDeviceInfo.tsx src/pipedal/ToobInputStageView.tsx @@ -224,6 +274,7 @@ add_custom_command( src/pipedal/DarkMode.tsx src/pipedal/Updater.tsx src/pipedal/Rect.tsx + src/pipedal/ToolTipEx.tsx src/pipedal/PedalboardView.tsx src/pipedal/JackServerSettingsDialog.tsx src/pipedal/PluginOutputControl.tsx @@ -236,6 +287,7 @@ add_custom_command( src/pipedal/WifiChannel.tsx src/pipedal/App.tsx src/pipedal/IControlViewFactory.tsx + src/pipedal/Tone3000HelpDialog.tsx src/pipedal/WifiDirectConfigSettings.tsx src/pipedal/ZoomedUiControl.tsx src/pipedal/MidiBindingsDialog.tsx @@ -244,6 +296,7 @@ add_custom_command( src/pipedal/FullScreenIME.tsx src/pipedal/FilePropertyDialog.tsx src/pipedal/RenameDialog.tsx + src/pipedal/SelectScreenOrientationDialog.tsx src/pipedal/ControlViewFactory.tsx src/pipedal/MaterialColors.tsx src/pipedal/PluginPresetsDialog.tsx @@ -265,19 +318,25 @@ add_custom_command( src/pipedal/BankDialog.tsx src/pipedal/FilePropertyDirectorySelectDialog.tsx src/pipedal/GovernorSettings.tsx + src/pipedal/ModGuiHost.tsx src/pipedal/XxxSnippet.tsx + src/pipedal/Tone3000Dialog.tsx src/pipedal/PluginPresetSelector.tsx src/pipedal/OldDeleteIcon.tsx src/pipedal/SelectMidiChannelsDialog.tsx src/pipedal/SearchFilter.tsx src/pipedal/Jack.tsx + src/pipedal/ToobPlayerControl.tsx + src/pipedal/ModGuiErrorBoundary.tsx src/pipedal/LoadPluginDialog.tsx src/pipedal/Pedalboard.tsx src/pipedal/Draggable.tsx src/pipedal/SelectChannelsDialog.tsx src/pipedal/PluginClass.tsx src/pipedal/DraggableGrid.tsx + src/pipedal/GraphicEqCtl.tsx src/pipedal/WifiConfigDialog.tsx + src/pipedal/GuitarMlHelpDialog.tsx src/pipedal/SelectThemeDialog.tsx src/pipedal/WifiDirectConfigDialog.tsx src/pipedal/UploadFileDialog.tsx @@ -285,6 +344,7 @@ add_custom_command( src/pipedal/Rectangle.tsx src/pipedal/PluginPreset.tsx src/pipedal/SettingsDialog.tsx + src/pipedal/AutoZoom.tsx src/pipedal/AppThemed.tsx src/pipedal/OptionsDialog.tsx src/pipedal/MidiChannelBinding.tsx @@ -299,12 +359,12 @@ add_custom_command( tsconfig.json tsconfig.node.json vite.config.ts - + ) message(STATUS "VITE TARGET: ${BUILD_DIRECTORY}/index.html") -add_custom_target ( +add_custom_target( ReactBuild ALL DEPENDS ${BUILD_DIRECTORY}/index.html ) diff --git a/vite/src/pipedal/ControlViewFactory.tsx b/vite/src/pipedal/ControlViewFactory.tsx index 00cc738..1f00945 100644 --- a/vite/src/pipedal/ControlViewFactory.tsx +++ b/vite/src/pipedal/ControlViewFactory.tsx @@ -31,6 +31,8 @@ import ToobPowerstage2ViewFactory from './ToobPowerStage2View'; import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView'; import ToobMLViewFactory from './ToobMLView'; import ToobPlayerFactory from './ToobPlayerView'; +import ToobNamViewFactory from './ToobNamView'; +import ToobParametericEqViewFactory from './ToobParametricEqView'; let pluginFactories: IControlViewFactory[] = [ @@ -38,7 +40,9 @@ let pluginFactories: IControlViewFactory[] = [ new ToobPowerstage2ViewFactory(), new ToobSpectrumAnalyzerViewFactory(), new ToobMLViewFactory(), - new ToobPlayerFactory() + new ToobPlayerFactory(), + new ToobNamViewFactory(), + new ToobParametericEqViewFactory() ]; diff --git a/vite/src/pipedal/DialogEx.tsx b/vite/src/pipedal/DialogEx.tsx index 39de5ae..c22a885 100644 --- a/vite/src/pipedal/DialogEx.tsx +++ b/vite/src/pipedal/DialogEx.tsx @@ -30,6 +30,7 @@ interface DialogExProps extends DialogProps { tag: string; fullwidth?: boolean; onEnterKey: () => void; + dlgRef?: (instance: HTMLDivElement | null) => void; } class DialogEx extends React.Component implements IDialogStackable { @@ -99,12 +100,13 @@ class DialogEx extends React.Component implements evt.stopPropagation(); } render() { - let { tag, onClose, fullWidth, onEnterKey, ...extra } = this.props; + let { dlgRef,tag, onClose, fullWidth, onEnterKey, ...extra } = this.props; return ( { this.myOnClose(event, reason); }} onKeyDown={(evt) => { this.onKeyDown(evt); }} + ref={dlgRef} > {this.props.children} diff --git a/vite/src/pipedal/GxTunerView.tsx b/vite/src/pipedal/GxTunerView.tsx index 2a512e4..2cf0643 100644 --- a/vite/src/pipedal/GxTunerView.tsx +++ b/vite/src/pipedal/GxTunerView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost,ControlGroup,ControlViewCustomization } from './PluginControlView'; import GxTunerControl from './GxTunerControl'; const GXTUNER_URI = "http://guitarix.sourceforge.net/plugins/gxtuner#tuner"; @@ -81,7 +81,7 @@ const GxTunerView = return false; } - modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] + modifyControls(host: ICustomizationHost,controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] { let refFreqIndex = this.getControlIndex("REFFREQ"); let thresholdIndex = this.getControlIndex("THRESHOLD"); diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index dc78b41..f63ced5 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -647,6 +647,10 @@ export class UiControl implements Deserializable { } return this; } + + clone() { + return new UiControl().deserialize(this); + } applyProperties(properties: Partial): UiControl { return { ...this, ...properties }; } @@ -879,6 +883,11 @@ export class UiControl implements Deserializable { case Units.pc: text += "%"; break; + case Units.unknown: + if (this.custom_units !== "") { + text = this.custom_units.replace("%f",text); + } + break; default: break; diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 34c54a3..25d9951 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -486,6 +486,19 @@ export class Pedalboard implements Deserializable { } throw new PiPedalArgumentError("Item not found."); } + tryGetItem(instanceId: number): PedalboardItem | null { + let it = this.itemsGenerator(); + while (true) + { + let v = it.next(); + if (v.done) break; + if (v.value.instanceId === instanceId) + { + return v.value; + } + } + return null; + } deleteItem_(instanceId: number,items: PedalboardItem[]): number | null { for (let i = 0; i < items.length; ++i) diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index c62ddf6..8984ddb 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -45,7 +45,7 @@ import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; -import {getDefaultModGuiPreference} from './ModGuiHost'; +import { getDefaultModGuiPreference } from './ModGuiHost'; export enum State { Loading, @@ -96,11 +96,10 @@ export class HostVersion { } else { this.hostName = "Unknown Host"; remainder = "v0.0.0"; - } + } const args = remainder.split(","); // make make provisions for more. - if (args.length >= 1) - { + if (args.length >= 1) { let versionString = args[0].trim(); if (versionString.startsWith('v')) { versionString = versionString.substring(1).trim(); @@ -119,7 +118,7 @@ export class HostVersion { this.majorVersion = parseInt(parts[0]); this.minorVersion = parseInt(parts[1]); this.buildNumber = parseInt(parts[2]); - } + } } } lessThan(major: number, minor: number, build: number): boolean { @@ -133,7 +132,7 @@ export class HostVersion { hostName: string = ""; releaseType: string = ""; majorVersion: number = 0; - minorVersion: number = 0; + minorVersion: number = 0; buildNumber: number = 0; } export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void; @@ -533,7 +532,7 @@ export class PiPedalModel //implements PiPedalModel favorites: ObservableProperty = new ObservableProperty({}); - alsaSequencerConfiguration : ObservableProperty = new ObservableProperty(new AlsaSequencerConfiguration()); + alsaSequencerConfiguration: ObservableProperty = new ObservableProperty(new AlsaSequencerConfiguration()); presets: ObservableProperty = new ObservableProperty ( @@ -652,11 +651,10 @@ export class PiPedalModel //implements PiPedalModel return true; } - private updateEnabledItems(pedalboard: Pedalboard) - { + private updateEnabledItems(pedalboard: Pedalboard) { for (let item of pedalboard.itemsGenerator()) { this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled); - this.updatePedalboardItemUseModUi(item.instanceId, item.useModUi); + this.updatePedalboardItemUseModUi(item.instanceId, item.useModUi); } } @@ -1358,9 +1356,8 @@ export class PiPedalModel //implements PiPedalModel this.state.set(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??""); + 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; @@ -1424,7 +1421,8 @@ export class PiPedalModel //implements PiPedalModel if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready."); let newPedalboard = pedalboard.clone(); - let item = newPedalboard.getItem(instanceId); + let item = newPedalboard.tryGetItem(instanceId); + if (!item) return; let changed = false; for (let i = 0; i < controlValues.length; ++i) { @@ -1780,7 +1778,7 @@ export class PiPedalModel //implements PiPedalModel // null -> we've never seen a value. item.pathProperties[fileProperty.patchProperty] = "null"; } - newPedalboard.selectedPlugin = item.instanceId; + newPedalboard.selectedPlugin = item.instanceId; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard() return item.instanceId; @@ -2372,14 +2370,19 @@ export class PiPedalModel //implements PiPedalModel let result = new Promise((resolve, reject) => { let pedalboard = this.pedalboard.get(); if (pedalboard) { - 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); - } + 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) { @@ -2618,9 +2621,13 @@ export class PiPedalModel //implements PiPedalModel } private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) { let pedalboard = this.pedalboard.get(); - let pedalboardItem = pedalboard.getItem(instanceId); - if (pedalboardItem) { - pedalboardItem.pathProperties[propertyUri] = JSON.stringify(jsonObject); + try { + let pedalboardItem = pedalboard.tryGetItem(instanceId); + if (pedalboardItem) { + 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]; @@ -3390,9 +3397,8 @@ export class PiPedalModel //implements PiPedalModel this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); } async getAlsaSequencerConfiguration(): Promise { - if (this.webSocket) - { - let result = await this.webSocket.request("getAlsaSequencerConfiguration"); + if (this.webSocket) { + let result = await this.webSocket.request("getAlsaSequencerConfiguration"); return new AlsaSequencerConfiguration().deserialize(result); } throw new Error("No connection."); @@ -3417,7 +3423,7 @@ export class PiPedalModel //implements PiPedalModel listener.onEnabledChanged(instanceId, enabled); } } - } + } } private updatePedalboardItemUseModUi(instanceId: number, useModUi: boolean) { @@ -3434,18 +3440,18 @@ export class PiPedalModel //implements PiPedalModel addPedalboardItemEnabledChangeListener( instanceId: number, onEnabledChanged: PedalboardItemEnabledChangeCallback - ) : ListenHandle{ + ): ListenHandle { let handle = ++this.nextListenHandle; let currentValue = this.getPedalboardItemEnabled(instanceId); - let entry: PedalboardItemEnabledChangeItem = { - instanceId: instanceId, - handle: handle, - currentValue: currentValue, + let entry: PedalboardItemEnabledChangeItem = { + instanceId: instanceId, + handle: handle, + currentValue: currentValue, onEnabledChanged: onEnabledChanged }; - this.pedalboardItemEnabledChangeListeners.push( entry); + this.pedalboardItemEnabledChangeListeners.push(entry); onEnabledChanged(instanceId, currentValue); @@ -3453,20 +3459,20 @@ export class PiPedalModel //implements PiPedalModel } addPedalboardItemUseModUiChangeListener( instanceId: number, - onUseModUiChanged : PedalboardItemUseModUiChangeCallback - ) : ListenHandle{ + onUseModUiChanged: PedalboardItemUseModUiChangeCallback + ): ListenHandle { let handle = ++this.nextListenHandle; let currentValue = this.getPedalboardItemUseModUi(instanceId); - let entry: PedalboardItemUseModUiChangeItem = { - instanceId: instanceId, - handle: handle, - currentValue: currentValue, + let entry: PedalboardItemUseModUiChangeItem = { + instanceId: instanceId, + handle: handle, + currentValue: currentValue, onUseModUiChanged: onUseModUiChanged }; - this.pedalboardItemUseModUiChangeListeners.push( entry); + this.pedalboardItemUseModUiChangeListeners.push(entry); onUseModUiChanged(instanceId, currentValue); @@ -3493,7 +3499,7 @@ export class PiPedalModel //implements PiPedalModel return false; } - setKeepScreenOn(keepScreenOn: boolean): void { + setKeepScreenOn(keepScreenOn: boolean): void { if (this.canKeepScreenOn) { this.keepScreenOn.set(keepScreenOn); if (this.androidHost) { diff --git a/vite/src/pipedal/PluginControl.tsx b/vite/src/pipedal/PluginControl.tsx index 9374188..7112056 100644 --- a/vite/src/pipedal/PluginControl.tsx +++ b/vite/src/pipedal/PluginControl.tsx @@ -143,7 +143,7 @@ export interface PluginControlProps extends WithStyles void; } -type PluginControlState = { +export type PluginControlState = { error: boolean; editFocused: boolean; previewValue?: string; diff --git a/vite/src/pipedal/PluginControlView.tsx b/vite/src/pipedal/PluginControlView.tsx index 76d483f..ddc77c9 100644 --- a/vite/src/pipedal/PluginControlView.tsx +++ b/vite/src/pipedal/PluginControlView.tsx @@ -27,7 +27,7 @@ import AutoZoom from './AutoZoom'; import ModGuiHost from './ModGuiHost'; -import {midiChannelBindingControlFeatureEnabled} from './MidiChannelBinding'; +import { midiChannelBindingControlFeatureEnabled } from './MidiChannelBinding'; import { withStyles } from "tss-react/mui"; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; @@ -60,6 +60,12 @@ export const StandardItemSize = { width: 80, height: 110 }; const LANDSCAPE_HEIGHT_BREAK = 500; +export interface ICustomizationHost { + makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode; + renderControlGroup(controlGroup: ControlGroup, key: string): ReactNode; + isLandscapeGrid(): boolean; +} + function makeIoPluginInfo(name: string, uri: string): UiPlugin { let result = new UiPlugin(); result.name = name; @@ -336,7 +342,7 @@ export type ControlNodes = (ReactNode | ControlGroup)[]; export interface ControlViewCustomization { - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[]; + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[]; fullScreen(): boolean; } @@ -365,7 +371,7 @@ type PluginControlViewState = { const PluginControlView = withTheme(withStyles( - class extends ResizeResponsiveComponent { + class extends ResizeResponsiveComponent implements ICustomizationHost { model: PiPedalModel; constructor(props: PluginControlViewProps) { @@ -546,6 +552,47 @@ const PluginControlView = } } + isLandscapeGrid(): boolean { + return this.state.landscapeGrid; + } + renderControlGroup(controlGroup: ControlGroup, key: string): ReactNode { + let isLandscapeGrid = this.state.landscapeGrid; + + const classes = withStyles.getClasses(this.props); + + let controls: ReactNode[] = []; + for (let j = 0; j < controlGroup.controls.length; ++j) { + let item = controlGroup.controls[j]; + controls.push( + ( +
+ {item} +
+ + ) + ); + } + return ( +
+ {controlGroup.name !== "" && ( +
+ + {controlGroup.name} + +
+ )} +
+ { + controls + } +
+
+ ); + } getStandardControlNodes(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes { let result: ControlNodes = []; let portGroupMap: { [id: string]: ControlGroup } = {}; @@ -697,7 +744,6 @@ const PluginControlView = controlKeyIndex: number = 0; controlNodesToNodes(nodes: (ReactNode | ControlGroup)[]): ReactNode[] { const classes = withStyles.getClasses(this.props); - let isLandscapeGrid = this.state.landscapeGrid; let hasGroups = this.hasGroups(nodes); let result: ReactNode[] = []; @@ -706,36 +752,8 @@ const PluginControlView = let node = nodes[i]; if (node instanceof ControlGroup) { let controlGroup = node as ControlGroup; - let controls: ReactNode[] = []; - for (let j = 0; j < controlGroup.controls.length; ++j) { - let item = controlGroup.controls[j]; - controls.push( - ( -
- {item} -
- - ) - ); - } - - result.push(( -
-
- - {controlGroup.name} - -
-
- { - controls - } -
-
- )); - + let item = this.renderControlGroup(controlGroup, "cgix" + i) + result.push(item); } else { if (this.fullScreen()) { result.push( @@ -825,7 +843,7 @@ const PluginControlView = if (this.props.onSetShowModGui) { this.props.onSetShowModGui(this.props.instanceId, false); } - this.setState({showModGuiZoomed: false}) + this.setState({ showModGuiZoomed: false }) }} handleFileSelect={(instanceId, fileProperty, selectedFile) => { this.handleModGuiFileProperty( @@ -883,7 +901,7 @@ const PluginControlView = if (this.props.customization) { // allow wrapper class to insert/remove/rebuild controls. - controlNodes = this.props.customization.modifyControls(controlNodes); + controlNodes = this.props.customization.modifyControls(this, controlNodes); } let nodes = this.controlNodesToNodes(controlNodes); diff --git a/vite/src/pipedal/PluginOutputControl.tsx b/vite/src/pipedal/PluginOutputControl.tsx index 2f048d4..e5a681b 100644 --- a/vite/src/pipedal/PluginOutputControl.tsx +++ b/vite/src/pipedal/PluginOutputControl.tsx @@ -33,6 +33,30 @@ import ControlTooltip from './ControlTooltip'; import { css } from '@emotion/react'; +export interface VuColor { + minDb: number; + maxDb: number; + color: string; +}; + +const defaultVuColors: VuColor[] = [ + { minDb: 0, maxDb: 10000, color: "#F00" }, + { minDb: -10, maxDb: 0, color: "#FF0" }, + { minDb: -10000, maxDb: -10, color: "#0C0" }, +]; + +function getVuColor(vuColors: VuColor[], levelDb: number): string { + let ix = 0; + for (let i = 0; i < vuColors.length; ++i) + { + let c = vuColors[i]; + if (levelDb > c.minDb && levelDb <= c.maxDb) { + ix = i; + break; + } + } + return vuColors[ix].color; +} function makeLedGradient(color: string) { if (color === "green") { @@ -243,6 +267,7 @@ const PluginOutputControl = } } + updateDbVuTelltale() { let telltaleDone = true; if (this.dbVuHoldTime !== 0) { @@ -264,13 +289,8 @@ const PluginOutputControl = if (this.dbVuTelltaleRef.current) { let telltaleStyle = this.dbVuTelltaleRef.current.style; + let telltaleColor = getVuColor(defaultVuColors, this.dbVuTelltale); telltaleStyle.marginTop = y + "px"; - let telltaleColor = "#0C0"; - if (this.dbVuTelltale >= 0) { - telltaleColor = "#F00"; - } else if (this.dbVuTelltale >= -10) { - telltaleColor = "#FF0"; - } telltaleStyle.background = telltaleColor; } } @@ -366,6 +386,13 @@ const PluginOutputControl = dbVuMap(value: number): number { let control = this.props.uiControl; + if (value > control.max_value) { + value = control.max_value; + } + if (value < control.min_value) + { + value = control.min_value; + } let y = (control.max_value - value) * this.DB_VU_HEIGHT / (control.max_value - control.min_value); return y; } @@ -448,8 +475,8 @@ const PluginOutputControl = } else if (control.isDbVu()) { item_width = undefined; - let redLevel = this.dbVuMap(0); - let yellowLevel = this.dbVuMap(-10); + let vuColors = defaultVuColors; + return (
@@ -469,11 +496,24 @@ const PluginOutputControl = style={{ flex: "1 1 1", display: "flex", justifyContent: "center", alignItems: "start", flexFlow: "row nowrap" }}>
-
-
-
+
+ { + vuColors.map((vuColor,ix)=>{ + let top = Math.floor(this.dbVuMap(vuColor.maxDb)); + let bottom = Math.ceil(this.dbVuMap(vuColor.minDb))+1; -
+ return ( +
+ ); + }) + } +
+ +
diff --git a/vite/src/pipedal/ToobCabSimView.tsx b/vite/src/pipedal/ToobCabSimView.tsx index 74fc3b4..f68bfd3 100644 --- a/vite/src/pipedal/ToobCabSimView.tsx +++ b/vite/src/pipedal/ToobCabSimView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup,ControlViewCustomization } from './PluginControlView'; import ToobFrequencyResponseView from './ToobFrequencyResponseView'; @@ -64,7 +64,7 @@ const ToobCabSimView = return false; } - modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] + modifyControls(host: ICustomizationHost,controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] { controls.splice(0,0, ( ) diff --git a/vite/src/pipedal/ToobFrequencyResponseView.tsx b/vite/src/pipedal/ToobFrequencyResponseView.tsx index 4a07ed3..2948cb9 100644 --- a/vite/src/pipedal/ToobFrequencyResponseView.tsx +++ b/vite/src/pipedal/ToobFrequencyResponseView.tsx @@ -32,14 +32,14 @@ import SvgPathBuilder from './SvgPathBuilder'; const StandardItemSize = { width: 80, height: 110 }; -const FREQUENCY_RESPONSE_VECTOR_URI = "http://two-play.com/plugins/toob#frequencyResponseVector"; +export const FREQUENCY_RESPONSE_VECTOR_URI = "http://two-play.com/plugins/toob#frequencyResponseVector"; -const PLOT_WIDTH = StandardItemSize.width * 2 - 16; +const DEFAULT_PLOT_WIDTH = StandardItemSize.width * 2 - 16; const PLOT_HEIGHT = StandardItemSize.height - 12; const styles = (theme: Theme) => createStyles({ frame: { - width: PLOT_WIDTH, height: PLOT_HEIGHT, background: "#444", + width: DEFAULT_PLOT_WIDTH, height: PLOT_HEIGHT, background: "#444", borderRadius: 6, marginTop: 12, marginLeft: 8, marginRight: 8, boxShadow: "1px 4px 8px #000 inset" @@ -127,14 +127,16 @@ const ToobFrequencyResponseView = // Size of the SVG element. xMin: number = 0; - xMax: number = PLOT_WIDTH + 4; + xMax() { + return this.props.width ? this.props.width: DEFAULT_PLOT_WIDTH; + } yMin: number = 0; yMax: number = PLOT_HEIGHT; dbTickSpacing: number = this.dbTickSpacingOpt; toX(frequency: number): number { var logV = Math.log(frequency); - return (this.xMax - this.xMin) * (logV - this.logMin) / (this.logMax - this.logMin) + this.xMin; + return (this.xMax() - this.xMin) * (logV - this.logMin) / (this.logMax - this.logMin) + this.xMin; } toY(value: number): number { @@ -156,11 +158,12 @@ const ToobFrequencyResponseView = let dbMin = data[2]; let dbMax = data[3]; + let xMax = this.xMax(); let n = data.length-4; let toX_ = (bin: number): number => { - return (this.xMax - this.xMin) * bin/n; + return (xMax - this.xMin) * bin/n; }; let toY_ = (value: number): number => { @@ -240,16 +243,17 @@ const ToobFrequencyResponseView = grid(): React.ReactNode[] { let result: React.ReactNode[] = []; + let xMax = this.xMax(); 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); if (db === 0) { result.push( - this.majorGridLine(this.xMin, y, this.xMax, y) + this.majorGridLine(this.xMin, y, xMax, y) ); } else { result.push( - this.gridLine(this.xMin, y, this.xMax, y) + this.gridLine(this.xMin, y, xMax, y) ); } } @@ -286,11 +290,12 @@ const ToobFrequencyResponseView = this.logMin = Math.log(this.fMin); this.logMax = Math.log(this.fMax); + let xMax = this.xMax(); const classes = withStyles.getClasses(this.props); return ( -
- +
+ {this.grid()} diff --git a/vite/src/pipedal/ToobInputStageView.tsx b/vite/src/pipedal/ToobInputStageView.tsx index 91321f7..cce7a64 100644 --- a/vite/src/pipedal/ToobInputStageView.tsx +++ b/vite/src/pipedal/ToobInputStageView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; //import ToobFrequencyResponseView from './ToobFrequencyResponseView'; @@ -63,7 +63,7 @@ const ToobInputStageView = return false; } - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + modifyControls(host: ICustomizationHost,controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { return controls; // let group = controls[1] as ControlGroup; // group.controls.splice(0,0, diff --git a/vite/src/pipedal/ToobMLView.tsx b/vite/src/pipedal/ToobMLView.tsx index 378b07f..e9ce76a 100644 --- a/vite/src/pipedal/ToobMLView.tsx +++ b/vite/src/pipedal/ToobMLView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel, MonitorPortHandle } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; // import ToobFrequencyResponseView from './ToobFrequencyResponseView'; const styles = (theme: Theme) => createStyles({ @@ -107,7 +107,7 @@ const ToobMLView = return false; } - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + modifyControls(host: ICustomizationHost,controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { // Find EQ group // let group = controls.find((control) => typeof control !== 'string' && (control as ControlGroup).name === "EQ") as ControlGroup; // if (group) { diff --git a/vite/src/pipedal/ToobNamView.tsx b/vite/src/pipedal/ToobNamView.tsx new file mode 100644 index 0000000..31043e4 --- /dev/null +++ b/vite/src/pipedal/ToobNamView.tsx @@ -0,0 +1,325 @@ +// Copyright (c) 2025 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 './WithStyles'; +import { createStyles } from './WithStyles'; + +import { withStyles } from "tss-react/mui"; + +import IControlViewFactory from './IControlViewFactory'; +import { PiPedalModelFactory, PiPedalModel, ListenHandle, State } from "./PiPedalModel"; +import { PedalboardItem,ControlValue } from './Pedalboard'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; +import { UiPlugin,UiControl, ControlType } from './Lv2Plugin'; + + +const TOOB_NAM__MODEL_METADATA = "http://two-play.com/plugins/toob-nam#model_metadata"; + +// offset 0: an integer with the folloing bits set. +class TOOB_NAM_METADATA_OFFSETS { + static readonly flags = 0; + static readonly preset_version = 1; + static readonly loudness = 2; + static readonly gain = 3; + static readonly input_level_dbu = 4; + static readonly output_level_dbu = 5; + static readonly max_medatadata = 6; +}; + + +class TOOB_NAM_METADATA_FLAGS { + static readonly has_model = 1; + static readonly has_loudness = 2; + static readonly has_gain = 4; + static readonly has_input_level_dbu = 8; + static readonly has_output_level_dbu = 16; +}; + +class ModelMetadata { + constructor(metadataValues?: number[]) + { + + + if (metadataValues) { + let flags = metadataValues[TOOB_NAM_METADATA_OFFSETS.flags]; + this.hasModel = (flags & TOOB_NAM_METADATA_FLAGS.has_model) !== 0; + this.hasLoudness = (flags & TOOB_NAM_METADATA_FLAGS.has_loudness) !== 0; + this.hasGain = (flags & TOOB_NAM_METADATA_FLAGS.has_gain) !== 0; + this.hasInputLevelDBU = (flags & TOOB_NAM_METADATA_FLAGS.has_input_level_dbu) !== 0; + this.hasOutputLevelDBU = (flags & TOOB_NAM_METADATA_FLAGS.has_output_level_dbu) !== 0; + + this.loudness = metadataValues[TOOB_NAM_METADATA_OFFSETS.loudness]; + this.gain = metadataValues[TOOB_NAM_METADATA_OFFSETS.gain]; + this.inputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.input_level_dbu]; + this.outputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.output_level_dbu]; + this.preset_version = metadataValues[TOOB_NAM_METADATA_OFFSETS.preset_version]; + } else { + this.hasModel = false; + this.hasLoudness = false; + this.hasGain = false; + this.hasInputLevelDBU = false; + this.hasOutputLevelDBU = false; + + this.loudness = 0; + this.gain = 0; + this.inputlevelDBU = 0; + this.outputlevelDBU = 0; + this.preset_version = 1; + } + } + + preset_version: number; + + hasModel: boolean; + hasLoudness: boolean; + hasGain: boolean; + hasInputLevelDBU: boolean; + hasOutputLevelDBU: boolean; + + loudness: number; + gain: number; + inputlevelDBU: number; + outputlevelDBU: number; + +} + + +const styles = (theme: Theme) => createStyles({ +}); + +interface ToobNamViewProps extends WithStyles { + instanceId: number; + item: PedalboardItem; + +} +interface ToobNamViewState { + showEqSection: boolean; + enableCalibration: boolean; + enableOutputNormalization: boolean; + modelMetadata: ModelMetadata; +} + +const ToobNamView = + withStyles( + class extends React.Component + implements ControlViewCustomization { + model: PiPedalModel; + + customizationId: number = 1; + + constructor(props: ToobNamViewProps) { + super(props); + this.handleConnectionStateChanged = this.handleConnectionStateChanged.bind(this); + + this.model = PiPedalModelFactory.getInstance(); + this.state = { + showEqSection: false, + enableCalibration: false, + enableOutputNormalization: false, + modelMetadata: new ModelMetadata() + } + let pluginInfo: UiPlugin | null = this.model.getUiPlugin(this.props.item.uri); + if (pluginInfo === null) + { + throw new Error("Plugin not fouund."); + } + let inputCalibrationControl = pluginInfo.getControl("inputCalibrationMode"); + if (!inputCalibrationControl) + { + throw new Error("Control not found."); + } + let patchedInputControl = new UiControl().deserialize(inputCalibrationControl); + patchedInputControl.controlType = ControlType.Select; + this.patchedInputControl = patchedInputControl + + let outputCalibrationControl = pluginInfo.getControl("outputCalibration"); + if (!outputCalibrationControl) + { + throw new Error("Control not found."); + } + let patchedOutputControl = new UiControl().deserialize(outputCalibrationControl); + patchedOutputControl.controlType = ControlType.Select; + this.noCalibrationOutputControl = patchedOutputControl + this.noCalibrationOutputControl.scale_points.splice(1,1); + } + + private patchedInputControl: UiControl; + private noCalibrationOutputControl: UiControl; + fullScreen() { + return false; + } + + disableControl(control: React.ReactNode, key: string) { + return ( +
{ + e.stopPropagation(); + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + > + {control} +
+ ); + } + + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + let EqPos = 7; + let CalibrationGroupPos = 8; + + let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup; + + calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl,this.props.item.controlValues); + + if (this.state.enableCalibration) + { + calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl,this.props.item.controlValues); + + } else { + calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl, + [ new ControlValue("inputCalibrationMode",0.0)] + ); + calibrationGroup.controls[0] = this.disableControl(calibrationGroup.controls[0], "cg_01d"); + calibrationGroup.controls[1] = this.disableControl(calibrationGroup.controls[1], "cg_02d"); + + if (this.state.enableOutputNormalization) + { + calibrationGroup.controls[2] = host.makeStandardControl(this.noCalibrationOutputControl, this.props.item.controlValues); + } else { + let tControl: UiControl = this.noCalibrationOutputControl.clone(); + tControl.symbol = "_disabled_output"; + calibrationGroup.controls[2] = host.makeStandardControl( + tControl, + [ new ControlValue("_disabled_output",2.0)]); + calibrationGroup.controls[2] = this.disableControl(calibrationGroup.controls[2], "cg_03d"); + + } + } + if (!this.state.showEqSection) { + controls.splice(EqPos, 1); + } + return controls; + } + + private handleConnectionStateChanged(state: State) { + if (state === State.Ready) { + this.unsubscribeFromMetadata(); + this.subscribeToMetdata(); + } + } + + handleModelMetadata(atomData: any) { + if (atomData && atomData.otype_ === "Vector" && atomData.value) { + let metadata = new ModelMetadata(atomData.value as number[]); + this.setState({ + modelMetadata: metadata, + showEqSection: metadata.preset_version === 0, + enableCalibration: metadata.hasInputLevelDBU && metadata.hasModel, + enableOutputNormalization: metadata.hasLoudness && metadata.hasModel + }); + } + } + + subscribeToMetdata() { + this.subscribedId = this.props.instanceId; + this.listenHandle = this.model.monitorPatchProperty( + this.props.instanceId, + TOOB_NAM__MODEL_METADATA, + (instanceId, propertyUri, atomData) => { + this.handleModelMetadata(atomData); + }); + this.model.getPatchProperty( + this.props.instanceId, + TOOB_NAM__MODEL_METADATA + ).then((atomData) => { + this.handleModelMetadata(atomData); + }).catch((e) => { + + }); + + } + unsubscribeFromMetadata() { + this.subscribedId = null; + if (this.listenHandle) { + this.model.cancelMonitorPatchProperty(this.listenHandle); + this.listenHandle = null; + } + } + + private listenHandle: ListenHandle | null = null; + componentDidMount() { + if (super.componentDidMount) { + super.componentDidMount(); + } + this.subscribeToMetdata(); + this.model.state.addOnChangedHandler(this.handleConnectionStateChanged); + } + componentWillUnmount() { + this.unsubscribeFromMetadata(); + if (super.componentWillUnmount) { + super.componentWillUnmount(); + } + this.model.state.removeOnChangedHandler(this.handleConnectionStateChanged); + + } + + private subscribedId: number | null = null; + componentDidUpdate() { + if (this.props.instanceId !== this.subscribedId) { + this.unsubscribeFromMetadata(); + this.subscribeToMetdata(); + } + } + + + render() { + return ( { }} + + />); + } + }, + styles + ); + + + +class ToobNamViewFactory implements IControlViewFactory { + uri: string = "http://two-play.com/plugins/toob-nam"; + + Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode { + return (); + } + + +} +export default ToobNamViewFactory; \ No newline at end of file diff --git a/vite/src/pipedal/ToobParametricEqView.tsx b/vite/src/pipedal/ToobParametricEqView.tsx new file mode 100644 index 0000000..d40fb70 --- /dev/null +++ b/vite/src/pipedal/ToobParametricEqView.tsx @@ -0,0 +1,724 @@ +// Copyright (c) 2025 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 ToobFrequencyResponseView, { FREQUENCY_RESPONSE_VECTOR_URI } from './ToobFrequencyResponseView'; +import WithStyles from './WithStyles'; +import { createStyles } from './WithStyles'; +import SvgPathBuilder from './SvgPathBuilder'; +import CircleUpIcon from './svg/expand_circle_up_24dp.svg?react'; +import CircleDownIcon from './svg/expand_circle_down_24dp.svg?react'; +import { withStyles } from "tss-react/mui"; +import DialogEx from './DialogEx'; +import DialogContent from '@mui/material/DialogContent'; + +import IControlViewFactory from './IControlViewFactory'; +import { PiPedalModelFactory, PiPedalModel, State } from "./PiPedalModel"; +import { PedalboardItem } from './Pedalboard'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; +import { UiPlugin } from './Lv2Plugin'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton/IconButton'; +import { css } from '@emotion/react'; + + + + +const styles = (theme: Theme) => createStyles({ + menuIcon: css({ + fill: (theme.palette.text.primary + "!important"), //theme.palette.text.primary, + opacity: 0.6 + }), + upIcon: css({ + fill: (theme.palette.text.primary + "!important"), //theme.palette.text.primary, + opacity: 0.6, + alignSelf: "flex-start" + }), +}); + +interface Point { x: number, y: number }; + + +function offsetFromEnd(pt0: Point, pt1: Point, distance: number) { + let dx = pt1.x - pt0.x; + let dy = pt1.y - pt0.y; + let mag = Math.sqrt(dx * dx + dy * dy); + let normX = dx / mag; + let normY = dy / mag; + return { + x: pt1.x - normX * distance, + y: pt1.y - normY * distance + } +} + + +const getWindowSize = () => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + }; +}; + + +interface UnpackedControls { + eq: React.ReactNode; + filters_loCut: React.ReactNode; + filters_hiCut: React.ReactNode; + low_level: React.ReactNode; + low_freq: React.ReactNode; + lmf_level: React.ReactNode; + lmf_freq: React.ReactNode; + lmf_q: React.ReactNode; + hmf_level: React.ReactNode; + hmf_freq: React.ReactNode; + hmf_q: React.ReactNode; + hi_level: React.ReactNode; + hi_freq: React.ReactNode; + gain: React.ReactNode; +} + +interface ToobParametricEqViewProps extends WithStyles { + instanceId: number; + item: PedalboardItem; + +} +interface ToobParametricEqViewState { + windowSize: { width: number, height: number }; + maximized: boolean; + +} + +const ToobParametricEqView = + withStyles( + class extends React.Component + implements ControlViewCustomization { + model: PiPedalModel; + + customizationId: number = 943; + + constructor(props: ToobParametricEqViewProps) { + super(props); + + this.handleResize = this.handleResize.bind(this); + this.handleConnectionStateChanged = this.handleConnectionStateChanged.bind(this); + + this.model = PiPedalModelFactory.getInstance(); + this.state = { + windowSize: getWindowSize(), + maximized: false + } + let pluginInfo: UiPlugin | null = this.model.getUiPlugin(this.props.item.uri); + if (pluginInfo === null) { + throw new Error("Plugin not fouund."); + } + } + + fullScreen() { + return true; + } + + horizontalScrollControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + // Implement horizontal scrolling behavior for tiny horizontal screens + controls[0] = ( +
+ {controls[0] as React.ReactNode} +
+ ); + return controls; + } + + unpackControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): UnpackedControls { + return { + eq: controls[0] as React.ReactNode, + filters_loCut: (controls[1] as ControlGroup).controls[0], + filters_hiCut: (controls[1] as ControlGroup).controls[1], + low_level: (controls[2] as ControlGroup).controls[0], + low_freq: (controls[2] as ControlGroup).controls[1], + lmf_level: (controls[3] as ControlGroup).controls[0], + lmf_freq: (controls[3] as ControlGroup).controls[1], + lmf_q: (controls[3] as ControlGroup).controls[2], + hmf_level: (controls[4] as ControlGroup).controls[0], + hmf_freq: (controls[4] as ControlGroup).controls[1], + hmf_q: (controls[4] as ControlGroup).controls[2], + hi_level: (controls[5] as ControlGroup).controls[0], + hi_freq: (controls[5] as ControlGroup).controls[1], + gain: controls[6] as React.ReactNode + }; + } + absolutePosition(x: number, y: number, el: React.ReactNode) { + return (
{el}
); + } + private ROW_HEIGHT = 140; + private BORDER_HEIGHT = 350; + private DIAL_WIDTH = 63; + private GRAPH_WIDTH = this.DIAL_WIDTH * 2; + + private smoothPath(pt0: Point, pt1: Point, pt2: Point, pt3: Point, curveRadius: number): string { + let pathBuilder = new SvgPathBuilder(); + + + let ctl01 = offsetFromEnd(pt0, pt1, curveRadius); + let ctl02 = offsetFromEnd(pt0, pt1, curveRadius / 3); + let ctl03 = offsetFromEnd(pt2, pt1, curveRadius / 3); + let ctl04 = offsetFromEnd(pt2, pt1, curveRadius); + + let ctl11 = offsetFromEnd(pt1, pt2, curveRadius); + let ctl12 = offsetFromEnd(pt1, pt2, curveRadius / 3); + let ctl13 = offsetFromEnd(pt3, pt2, curveRadius / 3); + let ctl14 = offsetFromEnd(pt3, pt2, curveRadius); + + pathBuilder.moveTo(pt0.x, pt0.y); + pathBuilder.lineTo(ctl01.x, ctl01.y); + pathBuilder.bezierTo( + ctl02.x, ctl02.y, + ctl03.x, ctl03.y, + ctl04.x, ctl04.y + ); + pathBuilder.lineTo(ctl11.x, ctl11.y); + pathBuilder.bezierTo( + ctl12.x, ctl12.y, + ctl13.x, ctl13.y, + ctl14.x, ctl14.y + ); + pathBuilder.lineTo(pt3.x, pt3.y); + return pathBuilder.toString(); + } + horizontalCompactControls(host: ICustomizationHost, controls: UnpackedControls) { + let height = this.BORDER_HEIGHT; + let row0 = height / 2 - this.ROW_HEIGHT; + let row1 = row0 + this.ROW_HEIGHT; + + let borderTop = 0; + let borderBottom = height - 8; + + let labelTop = row0 - 30; + let labelBottom = row1 + this.ROW_HEIGHT - 5; + + let colPos = (group: number, col: number) => this.DIAL_WIDTH * (0.3 * group + col); + let cpos0 = (group: number, col: number, el: React.ReactNode) => this.absolutePosition(colPos(group, col), row0, el); + let cpos1 = (group: number, col: number, el: React.ReactNode) => this.absolutePosition(colPos(group, col) + this.DIAL_WIDTH / 2, row1, el); + + + let label1 = (y: number, group: number, col0: number, col1: number, text: string, align: "left" | "center" | "right") => { + let left = colPos(group, col0); + let right = colPos(group, col1); + return ( + {text} + ); + }; + let divider = (group: number, col0: number, y0: number, col1: number, y1: number) => { + let x0 = colPos(group, col0); + let x1 = colPos(group, col1); + return ( + + + + ); + }; + let leftDivider = (group: number, col0: number, y0: number, col1: number, y1: number) => { + let x0 = colPos(group, col0); + let x1 = colPos(group, col1); + let xOrg = Math.min(x0, x1) - 2; + let yOrg = Math.min(y0, y1) - 2; + let width = Math.abs(x1 - x0) + 4; + let height = Math.abs(y1 - y0) + 4; + + let bezierLength = 10; + let pt0 = { x: x0, y: y0 }; + let pt1 = { x: x0, y: row1 - 18 }; + let pt2 = { x: x1, y: row1 + 3 }; + let pt3 = { x: x1, y: y1 }; + + let path = this.smoothPath(pt0, pt1, pt2, pt3, bezierLength); + + return ( + + + + ); + }; + let graph = ( + + ); + + return [(
+
+
+ {label1(labelBottom, 1, 2, 3.0, "Lo Shelf", "center")} + {label1(labelTop, 2, 3, 5, "Low Mid", "center")} + {label1(labelBottom, 3, 4.5, 6.5, "High Mid", "center")} + {label1(labelTop, 4, 6.5, 7.5, "Hi Shelf", "center")} + {divider(1, 2.0, borderTop, 2.0, borderBottom)} + {divider(5, 7.5, borderTop, 7.5, borderBottom)} + {leftDivider(2, 3.0, borderTop, 3.0, borderBottom)} + {leftDivider(3, 5.0, borderTop, 4.5, borderBottom)} + {leftDivider(4, 6.5, borderTop, 6.5, borderBottom)} + + {this.absolutePosition(0, row0, graph)} + {this.absolutePosition(colPos(0, 0), row1, controls.filters_loCut)} + {this.absolutePosition(colPos(0, 1), row1, controls.filters_hiCut)} + + {cpos0(1, 2, controls.low_level)} + {cpos1(1, 2 - 0.5, controls.low_freq)} + + {cpos0(2, 3.0, controls.lmf_q)} + {cpos1(2, 2.75, controls.lmf_freq)} + {cpos0(2, 4, controls.lmf_level)} + + {cpos1(3, 4, controls.hmf_freq)} + {cpos0(3, 5.25, controls.hmf_level)} + {cpos1(3, 5, controls.hmf_q)} + + {cpos0(4, 6.5, controls.hi_level)} + {cpos1(4, 6, controls.hi_freq)} + + {cpos0(5, 7.5, controls.gain)} +
+
+ +
)]; + } + verticalCompactControls(host: ICustomizationHost, controls: UnpackedControls) { + let width = 380; + + let graph = ( + + ); + let divider = () => { + return ( +
+ ); + } + let vDivider = () => { + return ( +
+ ); + } + let panel = (label: string, controls: React.ReactNode[]) => { + return ( +
+
+ {controls.map((ctl, i) => { + return ( +
+ {ctl} +
+ ); + })} +
+ {label} +
+ ); + } + + return [(
+
+
+ + {graph} + {(
)} + {panel("", [ + controls.filters_loCut, + controls.filters_hiCut, + ])} + {panel("", [ + controls.gain + ])} + {divider()} + {panel("Low", + [controls.low_level, + controls.low_freq + ]) + } + {vDivider()} + {panel("Low Mid", + + [ + controls.lmf_level, + controls.lmf_freq, + controls.lmf_q, + ]) + } + {divider()} + {panel("High Mid", + + [ + controls.hmf_level, + controls.hmf_freq, + controls.hmf_q, + ]) + } + {vDivider()} + {panel("High", + [controls.hi_level, + controls.hi_freq + ]) + } +
+
+ +
)]; + } + + tinyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + const classes = withStyles.getClasses(this.props); + let isLandscape = host.isLandscapeGrid(); + let panelControls: React.ReactNode[] = []; + + let filterGroup = controls[1] as ControlGroup; + let miscGroup: ControlGroup = { + name: "", + indexes: [], + controls: [ + filterGroup.controls[0], + filterGroup.controls[1], + controls[controls.length - 1] as React.ReactNode + ] + }; + panelControls.push(host.renderControlGroup(miscGroup, "vtc_miscGroup")); + if (!isLandscape) { + panelControls.push( + ( +
+ ) + ); + } + + for (let i = 2; i < controls.length - 1; ++i) { + let c = controls[i]; + if (c instanceof ControlGroup) { + panelControls.push( + host. + renderControlGroup(c as ControlGroup, "vtc_cg" + i) + ); + } else { + panelControls.push(c as React.ReactNode); + } + } + let panelStyle: React.CSSProperties; + let scrollStyle: React.CSSProperties; + let frameStyle: React.CSSProperties; + + if (isLandscape) { + panelStyle = { display: "flex", flexDirection: "row", alignItems: "stretch", width: "100%" }; + scrollStyle = { flex: "1 1 auto", overflowX: "auto", overflowY: "hidden", whiteSpace: "nowrap" }; + frameStyle = { display: "flex", flexFlow: "row nowrap", alignItems: "center",paddingTop: 8 }; + } else { + panelStyle = { display: "flex", flexDirection: "column", alignItems: "stretch", height: "100%" }; + scrollStyle = { flex: "1 1 auto", overflowX: "hidden", overflowY: "auto", whiteSpace: "nowrap" }; + frameStyle = { display: "flex", flexFlow: "row wrap", alignItems: "left", justifyContent: "left", gap: 12, paddingTop: 24, paddingBottom: 48 }; + } + + return [( +
+
+ {controls[0] as React.ReactNode} +
+ { + e.stopPropagation(); + this.setState({ maximized: true }); + }}> + + +
+
+
+
+ {panelControls} +
+
+
+ )]; + } + private dialogRef: HTMLDivElement | null = null; + private panelRef: HTMLDivElement | null = null; + + dlgTickInAnimation(yOffset: number, startTime: number) { + let elapsed = Date.now() - startTime; + let pct = elapsed / 200; + if (pct > 1) pct = 1; + if (!this.dialogRef) { + return; + } + this.dialogRef.style.opacity = `${pct}`; + this.dialogRef.style.transform = `translateY(${(1 - pct) * yOffset}px)`; + if (pct >= 1) { + return; + } + window.requestAnimationFrame(() => { + this.dlgTickInAnimation(yOffset, startTime); + }); + } + dlgTickOutAnimation(yOffset: number, startTime: number) { + if (!this.dialogRef || !this.panelRef) { + this.setState({ maximized: false }); + return; + } + let elapsed = Date.now() - startTime; + let pct = elapsed / 200; + if (pct > 1) pct = 1; + if (!this.dialogRef) { + return; + } + this.dialogRef.style.transform = `translateY(${(pct) * yOffset}px)`; + this.dialogRef.style.opacity = `${1 - pct}`; + if (pct >= 1) { + this.setState({ maximized: false }); + return; + } + window.requestAnimationFrame(() => { + this.dlgTickOutAnimation(yOffset, startTime); + }); + } + dlgSlideInAnimation(yOffset: number) { + if (!this.animateDialog) { + return; + } + this.animateDialog = false; + window.requestAnimationFrame(() => { + if (this.dialogRef) { + let startTime = Date.now(); + this.dlgTickInAnimation(yOffset, startTime); + } + }); + } + dlgSlideOutAnimation(yOffset: number) { + if (!this.animateDialog) { + return; + } + this.animateDialog = false; + + window.requestAnimationFrame(() => { + if (this.dialogRef) { + let startTime = Date.now(); + this.dlgTickInAnimation(yOffset, startTime); + } + }); + } + startSlideOutAnimation() { + if (this.panelRef && this.dialogRef) { + let yOffset = this.panelRef.getBoundingClientRect().top; + + window.requestAnimationFrame(() => { + if (this.dialogRef && this.dialogRef) { + let startTime = Date.now(); + this.dlgTickOutAnimation(yOffset, startTime); + } + }); + } else { + this.setState({ maximized: false }); + } + } + checkTransitionStart() { + if (this.dialogRef && this.panelRef) { + let panelRect = this.panelRef.getBoundingClientRect(); + this.dlgSlideInAnimation(panelRect.top); + } + } + setDialogRef(ref: HTMLDivElement | null) { + this.dialogRef = ref; + this.checkTransitionStart(); + }; + setPanelRef(ref: HTMLDivElement | null) { + this.panelRef = ref; + this.checkTransitionStart(); + } + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + const classes = withStyles.getClasses(this.props); + let unpackedControls = this.unpackControls(host, controls); + + if (this.state.maximized) { + return [( +
{ this.setPanelRef(instance); }} + style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}> + this.setState({ maximized: false })} + onClose={() => this.setState({ maximized: false })} + fullScreen={true} + tag="peq_dialog" + + dlgRef={(instance: HTMLDivElement | null): void => { + this.setDialogRef(instance); + }} + > + + + {this.state.windowSize.width > this.state.windowSize.height ? ( +
+
+ { + e.stopPropagation(); + this.startSlideOutAnimation(); + }} style={{ paddingTop: 48, paddingLeft: 8, paddingRight: 8 }}> + + +
+
+ {this.horizontalCompactControls(host, unpackedControls)} +
+
+ + ) : ( +
+
+ { + e.stopPropagation(); + this.startSlideOutAnimation(); + }} style={{ paddingLeft: 8, paddingTop: 8, paddingBottom: 8 }}> + + +
+
+ {this.verticalCompactControls(host, unpackedControls)} +
+
+ + ) + } +
+
+
+ + )]; + } + + + if (this.state.windowSize.width > 750 && this.state.windowSize.height > 600) { + return this.horizontalCompactControls(host, unpackedControls); + } + return this.tinyControls(host, controls); + } + + private handleConnectionStateChanged(state: State) { + if (state === State.Ready) { + this.unsubscribe(); + this.subscribe(); + } + } + + private handleResize() { + this.setState({ windowSize: getWindowSize() }); + } + + subscribe() { + this.subscribedId = this.props.instanceId; + } + unsubscribe() { + } + + componentDidMount() { + if (super.componentDidMount) { + super.componentDidMount(); + } + this.subscribe(); + this.model.state.addOnChangedHandler(this.handleConnectionStateChanged); + window.addEventListener("resize", this.handleResize); + + } + componentWillUnmount() { + this.unsubscribe(); + if (super.componentWillUnmount) { + super.componentWillUnmount(); + } + this.model.state.removeOnChangedHandler(this.handleConnectionStateChanged); + window.removeEventListener("resize", this.handleResize); + + } + + private subscribedId: number | null = null; + private animateDialog: boolean = false; + componentDidUpdate(oldProps: ToobParametricEqViewProps, oldState: ToobParametricEqViewState) { + if (this.props.instanceId !== this.subscribedId) { + this.unsubscribe(); + this.subscribe(); + } + this.animateDialog = this.state.maximized != oldState.maximized; + + } + + + render() { + return ( { }} + + />); + } + }, + styles + ); + + + +class ToobParametricEqViewFactory implements IControlViewFactory { + uri: string = "http://two-play.com/plugins/toob-parametric-eq"; + + Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode { + return (); + } + + +} +export default ToobParametricEqViewFactory; \ No newline at end of file diff --git a/vite/src/pipedal/ToobPlayerView.tsx b/vite/src/pipedal/ToobPlayerView.tsx index 751620c..3f533d0 100644 --- a/vite/src/pipedal/ToobPlayerView.tsx +++ b/vite/src/pipedal/ToobPlayerView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel, MonitorPortHandle } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; // import ToobFrequencyResponseView from './ToobFrequencyResponseView'; import ToobPlayerControl from './ToobPlayerControl'; @@ -103,7 +103,7 @@ const ToobPlayerView = fullScreen() { return true; } - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { let extraControls: React.ReactElement[] = []; let mixPanel = controls[3] as ControlGroup; let iKey = 0; diff --git a/vite/src/pipedal/ToobPowerStage2View.tsx b/vite/src/pipedal/ToobPowerStage2View.tsx index 664747d..7bf5a50 100644 --- a/vite/src/pipedal/ToobPowerStage2View.tsx +++ b/vite/src/pipedal/ToobPowerStage2View.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel, State,ListenHandle } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup,ControlViewCustomization } from './PluginControlView'; import ToobWaveShapeView, {BidirectionalVuPeak} from './ToobWaveShapeView'; @@ -130,7 +130,7 @@ const ToobPowerstage2View = return false; } - modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] + modifyControls(host: ICustomizationHost, controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[] { let time = Date.now()*0.001; diff --git a/vite/src/pipedal/ToobSpectrumAnalyzerView.tsx b/vite/src/pipedal/ToobSpectrumAnalyzerView.tsx index 269f958..8380f25 100644 --- a/vite/src/pipedal/ToobSpectrumAnalyzerView.tsx +++ b/vite/src/pipedal/ToobSpectrumAnalyzerView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; import ToobSpectrumResponseView from './ToobSpectrumResponseView'; @@ -64,7 +64,7 @@ const ToobSpectrumAnalyzerView = return false; } - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { controls.splice(0, 0, () ); diff --git a/vite/src/pipedal/ToobToneStackView.tsx b/vite/src/pipedal/ToobToneStackView.tsx index bf7dfd7..ef93226 100644 --- a/vite/src/pipedal/ToobToneStackView.tsx +++ b/vite/src/pipedal/ToobToneStackView.tsx @@ -28,7 +28,7 @@ import { withStyles } from "tss-react/mui"; import IControlViewFactory from './IControlViewFactory'; import { PiPedalModelFactory, PiPedalModel, ControlValueChangedHandle } from "./PiPedalModel"; import { PedalboardItem } from './Pedalboard'; -import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView'; +import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; import ToobFrequencyResponseView from './ToobFrequencyResponseView'; @@ -86,7 +86,7 @@ const ToobToneStackView = return false; } - modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { + modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { if (this.state.isBaxandall) { controls.splice(0, 0, ( \ No newline at end of file diff --git a/vite/src/pipedal/svg/expand_circle_up_24dp.svg b/vite/src/pipedal/svg/expand_circle_up_24dp.svg new file mode 100644 index 0000000..00cf575 --- /dev/null +++ b/vite/src/pipedal/svg/expand_circle_up_24dp.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file