diff --git a/.vscode/launch.json b/.vscode/launch.json index b110eb1..e719d51 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -172,51 +172,6 @@ ] }, - { - "name": "(gdb) pipedal Launch", - "type": "cppdbg", - "request": "launch", - // Resolved by CMake Tools: - "program": "${command:cmake.launchTargetPath}", - ///"args": [ "[lv2host_leak]" ], - - "args": [ - "/etc/pipedal/config", - "/etc/pipedal/react", - "-port", - "0.0.0.0:8080", - "-shutdownPort", - "3147" - ], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [ - { - // add the directory where our target was built to the PATHs - // it gets resolved by CMake Tools: - "name": "PATH", - "value": "$PATH:${command:cmake.launchTargetDirectory}" - }, - { - "name": "ASAN_OPTIONS", - "value": "detect_leaks=0" - - }, - { - "name": "OTHER_VALUE", - "value": "Something something" - } - ], - "externalConsole": false, - "MIMode": "gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ] - }, { "name": "(gdb) processCopyrights Launch", "type": "cppdbg", diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 66490c3..406338d 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -38,7 +38,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -49,7 +49,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment "TooB amplifier cabinet simulator." ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl new file mode 100644 index 0000000..a9231d6 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -0,0 +1,364 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . +@prefix pipedal_ui: . +@prefix ui: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . + + +@prefix toobConvolutionReverb: . +@prefix toobImpulse: . + +toobImpulse:#impulseFile + a rdf:property; + rdfs:label "File"; + rdfs:range atom:Path. + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:ReverbPlugin ; + doap:name "TooB Convolution Reverb" , + "TooB Convolution Reverb"@en-gb + ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 14 ; + rdfs:comment """ + +Convolution reverb is a notoriously compute-intensive effect. For best results, use the Max T control to constrain the length of the impulse file to +under two seconds. + +The Max T control affects how much of the impulse file is used. If the length of the impulse file is longer than the specified time, samples occuring after +the specified time are discarded, and the tail of the impulse convolution is fed back into the imput of the convolution. This ensures a smooth reverb tail, +and has little effect on the quality of the resulting reverb. If the impulse file is shorter than the specified time, then all samples in the impulse +file are used, and no feedback occurs. + +Because reverb impulse files typically include the original sound, you will often get strange comb filter effects if you use the Direct mix control; +but this varies depending on the impulse file in question. Usually, the Direct mix control should be set to -INF. + +The Predelay control determines whether Toob Convolution Reverb will strip leading silence from the impulse data. Turning it on preserves silence in the +impulse signal; turning it off removes leading silence. If the the Direct mix control is set to -INF dB, there's no real point in +delaying the convolution reverb output; you want responses as quickly as possible; the Predelay control should be set to off. + The leading silence in impulse files can sometimes be quite significant. + +If the input file has 4 channels, Toob Convolution Reverb assumes the signal is in Ambisonic b-format, and downmixes the channels accordingly. Unfortunately, +it currently dowmixes the Ambisonic audio to mono. Exepct a stereo version of Toob Convolution Reverb in the near future, which will make better use of +Ambisonic source material. + +Toob Convolution Reverb comes with a nominal set of reverb impulse files to get you started. You are strongly encouraged to seek out reverb impulse files +on the internet. + +Special thanks are extended to the the OpenAir project, wich graciously provided the base set of impulse files distributed with Toob Convolution Reverb under +licensing terms that are compatible with the Toob Amp project's licensing terms. + +--- + +Toob Convolution Reverb's impulse files were provided by the OpenAir project. +https://www.openair.hosted.york.ac.uk, under CC-By-4.0 licenses. + + + +Arthur Sykes Rymer Auditorium.wav + +--------------------------------------- + +By: www.openairlib.net, University of York + +This work is provided under a Creative Commons Attribution 4.0 International License. + +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=425 + + +Jack Lyons Hall, University of York.wav + +--------------------------------------- + +By: www.openairlib.net, Audiolab University of York, Alex Duffell, Aishwarya Sridhar, Zhong Li + +This work is provided under a Creative Commons Attribution 4.0 International License. + +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=571 + + + +Koli National Park - Winter.wav + +--------------------------------------- + +By: www.openairlib.net, Audio Lab University of York, Andrew Chadwick, Simon Shelley + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=584 + + + +St. Margaret’s Church.wav + +--------------------------------------- + +By: www.openairlib.net, AudioLab University of York, www.ncem.co.uk + +This work is provided under a Creative Commons Attribution 4.0 International License. + +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=702 + + +Genesis 6 Studio Live Room.wav + +--------------------------------------- + +By: www.openairlib.net + +This work is provided under a Creative Commons Attribution 4.0 International License. + +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=483 + +""" ; + + mod:brand "TooB"; + mod:label "Convolution Reverb"; + lv2:requiredFeature urid:map ; + lv2:extensionData state:interface; + + + lv2:optionalFeature lv2:hardRTCapable; + patch:readable + toobImpulse:#impulseFile + ; + patch:writable + toobImpulse:#impulseFile + ; + lv2:extensionData state:interface, + work:interface; + + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0; + lv2:symbol "time" ; + lv2:name "Max T"; + lv2:default 2.0 ; + lv2:minimum 0.1 ; + lv2:maximum 6.0 ; + + lv2:portProperty epp:notAutomatic ; + lv2:portProperty epp:expensive ; + + units:unit units:s ; + + rdfs:comment "Maximum length of impulse data to use." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1; + lv2:symbol "direct_mix" ; + lv2:name "Direct"; + lv2:default -40.0 ; + lv2:minimum -40.0 ; + lv2:maximum 20.0 ; + units:unit units:db ; + + lv2:scalePoint [ + rdfs:label "-INF" ; + rdf:value -40.0 + ]; + rdfs:comment "Direct mix. Set to minimum to disable." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + lv2:symbol "reverb_mix" ; + lv2:name "Reverb"; + + lv2:default -10.0 ; + lv2:minimum -40.0 ; + lv2:maximum 20.0 ; + units:unit units:db ; + + lv2:scalePoint [ + rdfs:label "-INF" ; + rdf:value -40.0 + ]; + rdfs:comment "Reverb mix. Set to minimum to disable." ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3 ; + lv2:symbol "predelay" ; + lv2:name "Predelay"; + + lv2:default 1 ; + lv2:minimum 0 ; + lv2:maximum 1 ; + + lv2:portProperty lv2:enumeration ; + + lv2:scalePoint [ + rdfs:label "Remove" ; + rdf:value 0.0; + ], + [ + rdfs:label "Keep" ; + rdf:value 1.0; + ]; + rdfs:comment "Removes leading silence in impulse when set to false." ; + ], + + [ + a lv2:OutputPort , + lv2:ControlPort ; + + lv2:index 4 ; + lv2:symbol "loading_state" ; + lv2:name "Status"; + + lv2:portProperty lv2:enumeration ; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 5.0 ; + + # NotLoaded = 0, + # Idle = 1, + # Error = 2, + # SentRequest = 3, + # GotResponse = 4, + # CleaningUp = 5, + + lv2:scalePoint [ + rdfs:label "Not loaded" ; + rdf:value 0.0 + ],[ + rdfs:label "Ready" ; + rdf:value 1.0 + ],[ + rdfs:label "Error" ; + rdf:value 2.0 + ],[ + rdfs:label "Loading" ; + rdf:value 3.0 + ],[ + rdfs:label "Loading" ; + rdf:value 4.0 + ],[ + rdfs:label "Loading" ; + rdf:value 5.0 + ]; + rdfs:comment "Plugin status." ; + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 5 ; + lv2:symbol "in" ; + lv2:name "In" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 6 ; + lv2:symbol "out" ; + lv2:name "Out" + ], + [ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + atom:supports patch:Message; + lv2:designation lv2:control ; + atom:supports patch:Message ; + + lv2:index 7 ; + lv2:symbol "control" ; + lv2:name "Control" ; + rdfs:comment "Control" ; + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 8; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rdfs:comment "Notification" ; + ] + . + + + pipedal_ui:ui toobConvolutionReverb:ui . + +toobConvolutionReverb:ui + a pipedal_ui:ui ; + pipedal_ui:fileProperties + [ + a pipedal_ui:fileProperty; + rdfs:label "Impulse File" ; + lv2:index 4 ; + pipedal_ui:patchProperty toobImpulse:impulseFile ; + pipedal_ui:directory "ReverbImpulseFiles"; + pipedal_ui:fileTypes + [ + a pipedal_ui:fileType; + rdfs:label "WAV File"; + pipedal_ui:fileExtension ".wav"; + ]; + ]; + ui:portNotification + [ + lv2:symbol "loading_state"; + ui:plugin ; + ui:protocol ui:floatProtocol; + # pipedal_ui:style pipedal_ui:text ; + # pipedal_ui:redLevel 0; + # pipedal_ui:yellowLevel -12; + # ui:portIndex 3; + + ] + . diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 41b1fb7..93b3c72 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -41,7 +41,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . inputStage:filterGroup a param:ControlGroup , @@ -59,7 +59,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; mod:brand "TooB"; mod:label "Input Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage.ttl deleted file mode 100644 index 9afc00e..0000000 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage.ttl +++ /dev/null @@ -1,624 +0,0 @@ -@prefix doap: . -@prefix lv2: . -@prefix rdf: . -@prefix rdfs: . -@prefix units: . -@prefix urid: . -@prefix atom: . -@prefix midi: . -@prefix epp: . -@prefix uiext: . -@prefix idpy: . -@prefix foaf: . -@prefix mod: . -@prefix param: . -@prefix work: . -@prefix pg: . - -@prefix atom: . -@prefix patch: . -@prefix plug: . -@prefix rdfs: . -@prefix state: . -@prefix urid: . -@prefix xsd: . -@prefix modgui: . -@prefix pstage: . - - -pstage:stage1 - a param:ControlGroup , - pg:InputGroup ; - lv2:name "Stage 1" ; - lv2:symbol "stage1" . - -pstage:stage2 - a param:ControlGroup , - pg:InputGroup ; - lv2:name "Stage 2" ; - lv2:symbol "stage2" . - - -pstage:stage3 - a param:ControlGroup , - pg:InputGroup ; - lv2:name "Stage 3" ; - lv2:symbol "stage3" . - - - - a lv2:Parameter ; - rdfs:label "uiState" ; - rdfs:range atom:Vector . - - - - a foaf:Person ; - foaf:name "Robin Davies" ; - foaf:mbox ; - foaf:homepage . - - - a lv2:Plugin , - lv2:SimulatorPlugin ; - - doap:name "TooB Power Stage"; - - doap:license ; - doap:maintainer ; - lv2:minorVersion 0 ; - lv2:microVersion 15 ; - - mod:brand "TooB"; - mod:label "Power Stage"; - lv2:requiredFeature urid:map ; - lv2:optionalFeature lv2:hardRTCapable; - lv2:extensionData state:interface; - patch:readable - ; - -rdfs:comment """ -The TooB Power Stage provides flexible and nuanced emulations of guitar amp power stages. -The goal of TooB Power Stage is not to inflexibly emulate existing tube power stages, but instead, -to be better. - -TooB Power Stage provides 3 consecutive gain stages, each of which can be overdriven to varying -degrees. Power transformer sag is emulated using the SAG and SAGD controls, which emulate the -volume and distortion effects introduced when power transformer voltage sags. - -Each gain section expects an input level that is nominally in the range of 12db to 0db. -It's ok if the levels are slightly over 0db, but they should not be massively over. Use the -Trim control to adjust input signal levels. - -The Gain controls use oversampled atan wave shaping to emulate the effect of overdriving -gain stages. Atan wave shaping provides rich even-order harmonics similar to those produced by a tube -amp power stage. A modest amount of gain will produce the characteristically "warm" sound of a tube -gain stage. More extreme settings produce harder overdrive. - -Judicious use of the LO CUT and HI CUT controls have interesting and subtle effects on the overall sound. -You are encouraged to play with these controls to see what happens. Tightening up low and high frequencies, -in early gain sections can allow you to drive the signal much harder in later gain sections without totally -breaking up. Real amps will usually cut low frequency response at somewhere between 80Hz (Fender) -and 140Hz (Marshall). - -General experience seems to suggest that gradually increasing the gain in each section allows you to drive -the signal harder without introducing undesirable effects in the output. However, there definitely interesting -and subtle differences in tone that can be acheived by not adhering to that rule of thumb. - -The SAG and SAGD controls are important controls. You need to undertand how they work. SAG controls how much -the overall volume of the output decreases as the transform sags. It provides the subtle "compression" that -tube amps provide, which is quite different from the compression provided by compressors. SAGD controls -the amount of additional distortion that occurs as the gain stages receive reduced voltage. It provides -the "forgiveness" of a good tube amp. Specifically, because SAGD operates in a feedback loop that runs through -all three gain stages. Increasing the distortion in the gain stages increases the power use of the gain stages; -increasing the power use of the gain stages increases the effect of SAG and SAGD which has an opposing effect -on power use. The net result is that using SAG and SAGD causes signals to break up more gradually -(hence the perceived "forgiveness"). The effects are non-linear, so not easy to describe completely. You -are strongly urged to experiment with SAG and SAGD settings in order to get best tone results. - -The TooB Power Stage design is based on an experimental design, that unexpectedly produced extraordinarily -good amp tones. To be perfectly honest, we have not completely explored the sonic possibilities of the -TooB Power Stage. What's known so far: it produces exceptionally pretty clean tones; it can be driven really -hard, and seems to preserve an unusual amount of detail in the signal even when being driven hard; and it -can definitely produce signals that have many of the most prized features of tube amplifiers: warmth, detail, -subtle "compression" without coloration, and forgiving break up. - -Explore! - -The TooB Input Stage is part of the TooB Amp Emulation Toolkit. Typically, you would -use the following plugins in the following order. - -- TooB Input Stage - -- TooB Tone Stack - -- Toob Power Stage - -- Toob Cabinet Simulator -"""; - lv2:port - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 0 ; - pg:group pstage:stage1 ; - lv2:symbol "trim1" ; - lv2:name "Trim"; - lv2:default 0.0 ; - lv2:minimum -20.0 ; - lv2:maximum 20.0 ; - units:unit units:db ; - lv2:scalePoint [ - rdfs:label "+20" ; - rdf:value 20.0 - ] , [ - rdfs:label "+10" ; - rdf:value 10.0 - ] , [ - rdfs:label "0" ; - rdf:value 0.0 - ] , [ - rdfs:label "-10" ; - rdf:value -10.0 - ], [ - rdfs:label "-20" ; - rdf:value -20.0 - ]; - rdfs:comment "Trim input level (stage 1)" ; - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 1 ; - pg:group pstage:stage1 ; - lv2:symbol "gain1" ; - lv2:name "Gain"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0 ; - rdfs:comment "Gain (stage 1)" - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage1 ; - - lv2:index 2 ; - lv2:symbol "shape1" ; - lv2:name "Shape1"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0; - lv2:rangeStep 2; - - lv2:portProperty lv2:enumeration ; - rdfs:comment "Wave Shape" ; - lv2:scalePoint [ - rdfs:label "atan" ; - rdf:value 0.0 - ], - [ - rdfs:label "triode" ; - rdf:value 1.0 - ] - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage1 ; - - lv2:index 3 ; - lv2:symbol "bias1" ; - lv2:name "Bias1"; - lv2:default 0.0 ; - lv2:minimum -2.0 ; - lv2:maximum 2.0; - ], - - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 4 ; - pg:group pstage:stage1 ; - lv2:symbol "locut1" ; - lv2:name "Lo Cut"; - lv2:default 30.0 ; - lv2:minimum 30.0 ; - lv2:maximum 300.0 ; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 30.0 - ]; - rdfs:comment "Low frequency cutoff (stage 1). Set to minimum to disable." ; - - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 5 ; - pg:group pstage:stage1 ; - lv2:symbol "hicut1" ; - lv2:name "Hi Cut"; - lv2:default 19000.0 ; - lv2:minimum 1000.0 ; - lv2:maximum 19000.0; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 19000.0 - ]; - rdfs:comment "High cutoff frequency (stage 1). Set to maximum to disable." ; - - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 6 ; - pg:group pstage:stage2 ; - lv2:symbol "gain2_enable" ; - lv2:name "Enable2"; - lv2:default 1 ; - lv2:minimum 0 ; - lv2:maximum 1; - lv2:portProperty lv2:toggled ; - rdfs:comment "Enable gain stage 2" ; - ], - - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 7 ; - pg:group pstage:stage2 ; - lv2:symbol "trim2" ; - lv2:name "Trim"; - lv2:default 0.0 ; - lv2:minimum -20.0 ; - lv2:maximum 20.0 ; - units:unit units:db ; - lv2:scalePoint [ - rdfs:label "+20" ; - rdf:value 20.0 - ] , [ - rdfs:label "+10" ; - rdf:value 10.0 - ] , [ - rdfs:label "0" ; - rdf:value 0.0 - ] , [ - rdfs:label "-10" ; - rdf:value -10.0 - ], [ - rdfs:label "-20" ; - rdf:value -20.0 - ]; - rdfs:comment "Trim input (stage 2)" ; - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 8 ; - pg:group pstage:stage2 ; - lv2:symbol "gain2" ; - lv2:name "Gain"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0 ; - rdfs:comment "Gain (stage 2)" - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage2 ; - - lv2:index 9 ; - lv2:symbol "shape2" ; - lv2:name "Shape2"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0; - lv2:rangeStep 2; - - lv2:portProperty lv2:enumeration ; - rdfs:comment "Wave Shape" ; - lv2:scalePoint [ - rdfs:label "atan" ; - rdf:value 0.0 - ], - [ - rdfs:label "triode" ; - rdf:value 1.0 - ] - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage2 ; - - lv2:index 10 ; - lv2:symbol "bias2" ; - lv2:name "Bias2"; - lv2:default 0.0 ; - lv2:minimum -2.0 ; - lv2:maximum 2.0; - ], - - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 11 ; - pg:group pstage:stage2 ; - lv2:symbol "locut2" ; - lv2:name "Lo Cut"; - lv2:default 30.0 ; - lv2:minimum 30.0 ; - lv2:maximum 300.0 ; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 30.0 - ] - ; - rdfs:comment "Low frequency cutoff (stage 2). Set to minimum to disable." ; - - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 12 ; - pg:group pstage:stage2 ; - lv2:symbol "hicut2" ; - lv2:name "Hi Cut"; - lv2:default 19000.0 ; - lv2:minimum 1000.0 ; - lv2:maximum 19000.0; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 19000.0 - ]; - rdfs:comment "High cutoff frequency (stage 2). Set to maximum to disable." ; - - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 13 ; - pg:group pstage:stage3 ; - lv2:symbol "gain3_enable" ; - lv2:name "Enable3"; - lv2:default 1 ; - lv2:minimum 0 ; - lv2:maximum 1; - lv2:portProperty lv2:toggled ; - rdfs:comment "Enable gain stage 3" ; - ], - - - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 14 ; - pg:group pstage:stage3 ; - lv2:symbol "trim3" ; - lv2:name "Trim"; - lv2:default 0.0 ; - lv2:minimum -20.0 ; - lv2:maximum 20.0 ; - units:unit units:db ; - lv2:scalePoint [ - rdfs:label "+20" ; - rdf:value 20.0 - ] , [ - rdfs:label "+10" ; - rdf:value 10.0 - ] , [ - rdfs:label "0" ; - rdf:value 0.0 - ] , [ - rdfs:label "-10" ; - rdf:value -10.0 - ], [ - rdfs:label "-20" ; - rdf:value -20.0 - ]; - rdfs:comment "Trim input level (stage 3)." ; - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 15 ; - pg:group pstage:stage3 ; - lv2:symbol "gain3" ; - lv2:name "Gain"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0 ; - rdfs:comment "Gain (stage 3)." - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage3 ; - - lv2:index 16 ; - lv2:symbol "shape3" ; - lv2:name "Shape3"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0; - lv2:rangeStep 2; - - lv2:portProperty lv2:enumeration ; - rdfs:comment "Wave Shape" ; - lv2:scalePoint [ - rdfs:label "atan" ; - rdf:value 0.0 - ], - [ - rdfs:label "triode" ; - rdf:value 1.0 - ] - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - pg:group pstage:stage3 ; - - lv2:index 17 ; - lv2:symbol "bias3" ; - lv2:name "Bias3"; - lv2:default 0.0 ; - lv2:minimum -2.0 ; - lv2:maximum 2.0; - ], - - - - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 18 ; - pg:group pstage:stage3 ; - lv2:symbol "locut3" ; - lv2:name "Lo Cut"; - lv2:default 30.0 ; - lv2:minimum 30.0 ; - lv2:maximum 300.0 ; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 30.0 - ]; - rdfs:comment "Low frequency cutoff (stage 3). Set to minimum to disable." ; - - ], [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 19 ; - pg:group pstage:stage3 ; - lv2:symbol "hicut3" ; - lv2:name "Hi Cut"; - lv2:default 19000.0 ; - lv2:minimum 1000.0 ; - lv2:maximum 19000.0; - units:unit units:hz; - lv2:designation param:cutoffFrequency ; - lv2:scalePoint [ - rdfs:label "OFF" ; - rdf:value 19000 - ]; - rdfs:comment "High cutoff frequency (stage 3). Set to minimum to disable." ; - - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 20 ; - lv2:symbol "sag" ; - lv2:name "Sag"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0; - rdfs:comment "Transformer sag (volume)." ; - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 21 ; - lv2:symbol "sagd" ; - lv2:name "Sag D"; - lv2:default 0.0 ; - lv2:minimum 0.0 ; - lv2:maximum 1.0; - rdfs:comment "Transformer sag (distortion)." ; - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 22 ; - lv2:symbol "master" ; - lv2:name "Master"; - lv2:default -0.0 ; - lv2:minimum -60.0 ; - lv2:maximum 30.0; - units:unit units:db; - lv2:scalePoint [ - rdfs:label "-60db" ; - rdf:value -60.0 - ] , [ - rdfs:label "0db" ; - rdf:value 0.0 - ], [ - rdfs:label "30db" ; - rdf:value -30.0 - ]; - rdfs:comment "Master volume." ; - - ], - [ - a lv2:AudioPort , - lv2:InputPort ; - lv2:index 23 ; - lv2:symbol "in" ; - lv2:name "In" - ] , [ - a lv2:AudioPort , - lv2:OutputPort ; - lv2:index 24 ; - lv2:symbol "out" ; - lv2:name "Out" - ],[ - a atom:AtomPort , - lv2:InputPort; - atom:bufferType atom:Sequence ; - atom:supports patch:Message; - lv2:designation lv2:control ; - lv2:index 25 ; - lv2:symbol "control" ; - lv2:name "Control" ; - rdfs:comment "Plugin to GUI communication" ; - ] , [ - a atom:AtomPort , - lv2:OutputPort ; - atom:bufferType atom:Sequence ; - atom:supports patch:Message; - lv2:designation lv2:control ; - lv2:index 26 ; - lv2:symbol "notify" ; - lv2:name "Notify" ; - rdfs:comment "Plugin to GUI communication" ; - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 27 ; - lv2:symbol "sagf" ; - lv2:name "SagF"; - lv2:default 13.0 ; - lv2:minimum 5.0 ; - lv2:maximum 25.0; - rdfs:comment "Sag filter Fc." ; - ] - . - - diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl index d929a9b..5fd6c36 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl @@ -56,7 +56,7 @@ pstage:stage3 a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -67,7 +67,7 @@ pstage:stage3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 1 ; + lv2:microVersion 14 ; mod:brand "TooB"; mod:label "Power Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 2e3705a..b0ea77d 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -28,7 +28,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; @@ -91,37 +91,7 @@ Displays a spectral plot of the input signal. a lv2:InputPort , lv2:ControlPort ; - lv2:index 4 ; - lv2:symbol "blockSize" ; - lv2:name "Block Size"; - lv2:default 4096.0 ; - lv2:minimum 1024.0 ; - lv2:maximum 8192.0; - - lv2:portProperty lv2:enumeration ; - rdfs:comment "Wave Shape" ; - lv2:scalePoint [ - rdfs:label "1024" ; - rdf:value 1024.0 - ], - [ - rdfs:label "2048" ; - rdf:value 2048.0 - ], - [ - rdfs:label "4096" ; - rdf:value 4096.0 - ], - [ - rdfs:label "8192" ; - rdf:value 8192.0 - ]; - ], - [ - a lv2:InputPort , - lv2:ControlPort ; - - lv2:index 5 ; + lv2:index 4 ; lv2:symbol "minF" ; lv2:name "Min F"; lv2:default 60.0 ; @@ -135,7 +105,7 @@ Displays a spectral plot of the input signal. a lv2:InputPort , lv2:ControlPort ; - lv2:index 6 ; + lv2:index 5 ; lv2:symbol "maxF" ; lv2:name "Max F"; lv2:default 16000.0 ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index 042818a..3aa9a99 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -38,7 +38,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -49,7 +49,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment """ The TooB Tone Stack emulates the tone controls of either a 59' Bassman or a JCM8000 guitar amplifier. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 index 4a691c8..fdd96dc 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0.1.7 differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index c3247ea..cc255ce 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -28,7 +28,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 1 ; + lv2:microVersion 14 ; rdfs:comment """ Emulation of Boss CE-2 Chorus. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index d94421a..dc244ca 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -28,7 +28,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment """ A straighforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index c6578a3..6afa215 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -28,7 +28,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment """ ToobFreeverb is an Lv2 implementation of the famous Freeverb reverb effect. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index 74fa372..426261c 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -36,7 +36,7 @@ toobml:filterGroup a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -47,7 +47,7 @@ toobml:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 13 ; + lv2:microVersion 14 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using artifical intelligence/machine learning modelling of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index cab7f72..cd8f957 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -28,7 +28,7 @@ a foaf:Person ; foaf:name "Robin Davies" ; foaf:mbox ; - foaf:homepage . + foaf:homepage . a lv2:Plugin , @@ -37,7 +37,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 1 ; + lv2:microVersion 14 ; rdfs:comment """ A chromatic guitar tuner. """ ; diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1.convolutionPlan.gz new file mode 100644 index 0000000..4f827ae Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1024.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1024.convolutionPlan.gz new file mode 100644 index 0000000..c08f74d Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/1024.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/128.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/128.convolutionPlan.gz new file mode 100644 index 0000000..7234e51 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/128.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/16.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/16.convolutionPlan.gz new file mode 100644 index 0000000..d8aa6f4 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/16.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2.convolutionPlan.gz new file mode 100644 index 0000000..e5032cf Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2048.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2048.convolutionPlan.gz new file mode 100644 index 0000000..37034fd Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/2048.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/256.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/256.convolutionPlan.gz new file mode 100644 index 0000000..6b52aea Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/256.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/32.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/32.convolutionPlan.gz new file mode 100644 index 0000000..68c1ab8 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/32.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4.convolutionPlan.gz new file mode 100644 index 0000000..baec56e Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4096.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4096.convolutionPlan.gz new file mode 100644 index 0000000..be750e0 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/4096.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/512.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/512.convolutionPlan.gz new file mode 100644 index 0000000..98a9b52 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/512.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/64.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/64.convolutionPlan.gz new file mode 100644 index 0000000..76bf78c Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/64.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8.convolutionPlan.gz new file mode 100644 index 0000000..ef5b69d Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8192.convolutionPlan.gz b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8192.convolutionPlan.gz new file mode 100644 index 0000000..811145d Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/fftplans.gz/8192.convolutionPlan.gz differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Arthur Sykes Rymer Auditorium.wav b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Arthur Sykes Rymer Auditorium.wav new file mode 100644 index 0000000..0542eed Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Arthur Sykes Rymer Auditorium.wav differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Genesis 6 Studio Live Room.wav b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Genesis 6 Studio Live Room.wav new file mode 100644 index 0000000..d0998ee Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Genesis 6 Studio Live Room.wav differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Jack Lyons Hall, University of York.wav b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Jack Lyons Hall, University of York.wav new file mode 100644 index 0000000..2f9b672 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Jack Lyons Hall, University of York.wav differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Koli National Park - Winter.wav b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Koli National Park - Winter.wav new file mode 100644 index 0000000..17cc8fe Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Koli National Park - Winter.wav differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Licenses.txt b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Licenses.txt new file mode 100644 index 0000000..f8e43f3 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/Licenses.txt @@ -0,0 +1,52 @@ +Toob Convolution Reverb's impulse files were provided by the OpenAir project. +https://www.openair.hosted.york.ac.uk/?page_id=571, under CC-By-4.0 licenses. + + +Arthur Sykes Rymer Auditorium.wav +--------------------------------------- +By: www.openairlib.net, University of York + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=425 + + +Jack Lyons Hall, University of York.wav +--------------------------------------- +By: www.openairlib.net, Audiolab University of York, Alex Duffell, Aishwarya Sridhar, Zhong Li + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=571 + + +Koli National Park - Winter.wav +--------------------------------------- +By: www.openairlib.net, Audio Lab University of York, Andrew Chadwick, Simon Shelley + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=584 + + +St. Margaret’s Church.wav +--------------------------------------- +By: www.openairlib.net, AudioLab University of York, www.ncem.co.uk + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=702 + + +Genesis 6 Studio Live Room.wav +--------------------------------------- +By: www.openairlib.net + +This work is provided under a Creative Commons Attribution 4.0 International License. +https://creativecommons.org/licenses/by/4.0/ + +Source: https://www.openair.hosted.york.ac.uk/?page_id=483 diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/St. Margaret's Church.wav b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/St. Margaret's Church.wav new file mode 100644 index 0000000..ad4e0fe Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/St. Margaret's Church.wav differ diff --git a/lv2/aarch64/ToobAmp.lv2/manifest.ttl b/lv2/aarch64/ToobAmp.lv2/manifest.ttl index e94dfc3..b24ec53 100644 --- a/lv2/aarch64/ToobAmp.lv2/manifest.ttl +++ b/lv2/aarch64/ToobAmp.lv2/manifest.ttl @@ -7,6 +7,11 @@ lv2:binary ; rdfs:seeAlso . + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + + a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . diff --git a/react/CMakeLists.txt b/react/CMakeLists.txt index 467075d..ce21af3 100644 --- a/react/CMakeLists.txt +++ b/react/CMakeLists.txt @@ -79,7 +79,7 @@ add_custom_command( src/Banks.tsx src/PluginPresetsDialog.tsx src/BankDialog.tsx - src/UploadDialog.tsx + src/UploadPresetDialog.tsx src/ToobPowerStage2View.tsx src/index.tsx src/Units.tsx diff --git a/react/src/AppThemed.css b/react/src/AppThemed.css index 24f7141..d169d81 100644 --- a/react/src/AppThemed.css +++ b/react/src/AppThemed.css @@ -12,3 +12,6 @@ padding-left: 36px; border-bottom: 1px #CCC solid } +div { + min-width: 0; + } diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index e0fbbbd..702d74f 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -665,7 +665,6 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< size="large"> -
diff --git a/react/src/ControlViewFactory.tsx b/react/src/ControlViewFactory.tsx index 55c7449..bfedd23 100644 --- a/react/src/ControlViewFactory.tsx +++ b/react/src/ControlViewFactory.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; -import {PedalboardItem, PedalboardSplitItem} from './Pedalboard'; +import { PedalboardItem, PedalboardSplitItem } from './Pedalboard'; import PluginControlView from './PluginControlView'; import SplitControlView from './SplitControlView'; import Typography from '@mui/material/Typography'; @@ -29,10 +29,10 @@ import IControlViewFactory from './IControlViewFactory'; import ToobInputStageViewFactory from './ToobInputStageView'; import ToobToneStackViewFactory from './ToobToneStackView'; import ToobCabSimViewFactory from './ToobCabSimView'; -import ToobPowerStage2Factory from './ToobPowerStage2View'; +import ToobPowerStage2Factory from './ToobPowerStage2View'; import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView'; import ToobMLViewFactory from './ToobMLView'; -import {ToobTunerViewFactory,GxTunerViewFactory} from './GxTunerView'; +import { ToobTunerViewFactory, GxTunerViewFactory } from './GxTunerView'; let pluginFactories: IControlViewFactory[] = [ @@ -47,32 +47,32 @@ let pluginFactories: IControlViewFactory[] = [ ]; -export function GetControlView(pedalboardItem?: PedalboardItem| null): React.ReactNode -{ +export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode { let model: PiPedalModel = PiPedalModelFactory.getInstance(); if (!pedalboardItem) { - return (
); + return (
); } - if (pedalboardItem.isSplit()) - { + if (pedalboardItem.isStart() || pedalboardItem.isEnd()) { + return ( + + ); + } + if (pedalboardItem.isSplit()) { return ( ); } else { - for (let i = 0; i < pluginFactories.length; ++i) - { + for (let i = 0; i < pluginFactories.length; ++i) { let factory = pluginFactories[i]; - if (factory.uri === pedalboardItem.uri) - { - return factory.Create(model,pedalboardItem); + if (factory.uri === pedalboardItem.uri) { + return factory.Create(model, pedalboardItem); } } let uiPlugin = model.getUiPlugin(pedalboardItem.uri); - if (!uiPlugin) - { -
+ if (!uiPlugin) { +
Missing plugin. The plugin '{pedalboardItem.pluginName}' ({pedalboardItem.uri}) is not currently installed.
diff --git a/react/src/Draggable.tsx b/react/src/Draggable.tsx index 7443881..81751dc 100644 --- a/react/src/Draggable.tsx +++ b/react/src/Draggable.tsx @@ -36,8 +36,12 @@ const LONG_PRESS_TIME_MS = 250; const styles = (theme: Theme) => createStyles({ frame: { - position: "relative", - margin: "12px" + width: "100%", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "relative" } }); @@ -479,7 +483,7 @@ const Draggable = render() { return ( -
{ @@ -66,7 +70,9 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent { - if (this.mounted) - { - this.setState({files: files,hasSelection: this.isFileInList(files,this.state.selectedFile)}); - } - }).catch((error)=>{ - this.model.showAlert(error.toString()) - }); + .then((files) => { + if (this.mounted) { + this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) }); + } + }).catch((error) => { + this.model.showAlert(error.toString()) + }); } onWindowSizeChanged(width: number, height: number): void { @@ -116,11 +112,17 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent, prevState: Readonly, snapshot?: any): void { - super.componentDidUpdate?.(prevProps,prevState,snapshot); - if (prevProps.fileProperty !== this.props.fileProperty) + super.componentDidUpdate?.(prevProps, prevState, snapshot); + if (prevProps.open !== this.props.open) { + if (this.props.open) + { + this.requestFiles() + } + } else if (prevProps.fileProperty !== this.props.fileProperty) { this.requestFiles() } + } private fileNameOnly(path: string): string { @@ -139,7 +141,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent= 0 && position < this.state.files.length-1) + { + newSelection = this.state.files[position+1]; + } else if (position === this.state.files.length-1) + { + if (position !== 0) { + newSelection = this.state.files[position-1]; + } + + } + + this.model.deleteUserFile(this.state.selectedFile) + .then( + ()=> { + this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""}); + this.requestFiles(); + } + ).catch( + (e: any)=> + { + this.model.showAlert(e + ""); + } + ); + } } mapKey: number = 0; @@ -160,63 +207,90 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }} - PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }} - > - -
- {this.props.onCancel();}} aria-label="back" - > - - - {this.props.fileProperty.name} -
-
-
 
-
-
- { - this.state.files.map( - (value: string, index: number) => { - let selected = value === this.state.selectedFile; - let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; - return ( - this.onSelect(value)} - > -
-
- - {this.fileNameOnly(value)} -
- - ); - } - ) - } + this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }} + PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }} + > + +
+ { this.props.onCancel(); }} aria-label="back" + > + + + {this.props.fileProperty.name} +
+
+
 
+
+
+ { + this.state.files.map( + (value: string, index: number) => { + let selected = value === this.state.selectedFile; + let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; + return ( + this.onSelect(value)} + > +
+
+ + {this.fileNameOnly(value)} +
+ + ); + } + ) + }
-
-
 
- -
- - - - -
 
- -
-
- - ); +
 
+ +
+ this.handleDelete()} > + + + + +
 
+ + + +
+
+ { + this.setState({ openUploadFileDialog: false }); + } + } + uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)} + onUploaded={()=> { this.requestFiles();}} + fileProperty={this.props.fileProperty} + + + /> + this.handleConfirmDelete()} + onClose={()=>{ + this.setState({openConfirmDeleteDialog: false}); + }} + + /> + + ); } } \ No newline at end of file diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index e1ae1ed..5dd1080 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -17,7 +17,7 @@ // 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, { ReactNode, SyntheticEvent, CSSProperties } from 'react'; +import React, { ReactNode, SyntheticEvent, CSSProperties, Fragment, ReactElement } from 'react'; import { PiPedalModel, PiPedalModelFactory, FavoritesList } from './PiPedalModel'; import { UiPlugin, PluginType } from './Lv2Plugin'; @@ -42,7 +42,6 @@ import SearchFilter from './SearchFilter'; import { FixedSizeGrid } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import StarBorderIcon from '@mui/icons-material/StarBorder'; -import Slide, { SlideProps } from '@mui/material/Slide'; import { createStyles, Theme } from '@mui/material/styles'; import { WithStyles, withStyles } from '@mui/styles'; import FilterListIcon from '@mui/icons-material/FilterList'; @@ -102,10 +101,10 @@ const pluginGridStyles = (theme: Theme) => createStyles({ height: "100%'" }, content: { - marginTop: "8px", - marginBottom: "8px", - marginLeft: "12px", - marginRight: "12px", + marginTop: "4px", + marginBottom: "4px", + marginLeft: "8px", + marginRight: "8px", width: "100%", overflow: "hidden", textAlign: "left", @@ -118,7 +117,7 @@ const pluginGridStyles = (theme: Theme) => createStyles({ }, content2: { display: "flex", flexDirection: "column", flex: "1 1 auto", width: "100%", - paddingLeft: 16, whiteSpace: "nowrap" + paddingLeft: 12, whiteSpace: "nowrap", textOverflow: "ellipsis" }, favoriteDecoration: { @@ -134,7 +133,8 @@ const pluginGridStyles = (theme: Theme) => createStyles({ opacity: "0.6" }, iconBorder: { - flex: "0 0 auto" + flex: "0 0 auto", + paddingTop: "4px" }, label: { width: "100%" @@ -173,14 +173,11 @@ type PluginGridState = { grid_cell_columns: number, minimumItemWidth: number, favoritesList: FavoritesList + gridItems: UiPlugin[]; + } -const Transition = React.forwardRef(function Transition( - props: SlideProps, ref: React.Ref -) { - return (); -}); export const LoadPluginDialog = withStyles(pluginGridStyles, { withTheme: true })( @@ -210,7 +207,9 @@ export const LoadPluginDialog = grid_cell_width: this.getCellWidth(window.innerWidth), grid_cell_columns: this.getCellColumns(window.innerWidth), minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220, - favoritesList: this.model.favorites.get() + favoritesList: this.model.favorites.get(), + gridItems: this.getFilteredPlugins("", filterType_, this.model.favorites.get()) + }; this.updateWindowSize = this.updateWindowSize.bind(this); @@ -219,6 +218,8 @@ export const LoadPluginDialog = this.handleSearchStringReady = this.handleSearchStringReady.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this); + + this.requestScrollTo(); } nominal_column_width: number = 250; @@ -252,9 +253,9 @@ export const LoadPluginDialog = let newValue = searchInput.value + e.key; searchInput.value = newValue; // add the key. to the value. + this.handleSearchStringChanged(newValue); searchInput.focus(); this.setState({ - search_string: newValue, search_collapsed: false }) @@ -275,9 +276,12 @@ export const LoadPluginDialog = } handleFavoritesChanged() { + let favorites = this.model.favorites.get(); + this.requestScrollTo(); this.setState( { - favoritesList: this.model.favorites.get() + favoritesList: favorites, + gridItems: this.getFilteredPlugins(null, null, favorites) }); } componentDidMount() { @@ -285,7 +289,12 @@ export const LoadPluginDialog = this.updateWindowSize(); window.addEventListener('resize', this.updateWindowSize); this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged); - this.setState({ favoritesList: this.model.favorites.get() }); + let favorites = this.model.favorites.get(); + this.setState({ + favoritesList: favorites, + gridItems: this.getFilteredPlugins(null, null, favorites) + + }); } componentWillUnmount() { @@ -297,7 +306,15 @@ export const LoadPluginDialog = componentDidUpdate(oldProps: PluginGridProps) { if (oldProps.open !== this.props.open) { if (this.props.open) { - this.setState({ search_string: "", search_collapsed: true }); + // reset state now. + if (this.state.search_string !== "" || !this.state.search_collapsed) { + this.setState({ + search_string: "", + search_collapsed: true, + gridItems: this.getFilteredPlugins("", null, null) + }); + } + this.requestScrollTo(); } } } @@ -308,17 +325,26 @@ export const LoadPluginDialog = } onFilterChange(e: any) { - let value = e.target.value as PluginType; + let filterValue = e.target.value as PluginType; - window.localStorage.setItem(FILTER_STORAGE_KEY, value as string); + window.localStorage.setItem(FILTER_STORAGE_KEY, filterValue as string); - this.setState({ filterType: value }); + this.requestScrollTo(); + this.setState({ + filterType: filterValue, + gridItems: this.getFilteredPlugins(null, filterValue, null) + }); } onClearFilter(): void { let value = PluginType.Plugin; window.localStorage.setItem(FILTER_STORAGE_KEY, value as string); - - this.setState({ filterType: value }); + if (this.state.filterType !== value) { + this.requestScrollTo(); + this.setState({ + filterType: value, + gridItems: this.getFilteredPlugins(null, value, null) + }); + } } selectItem(item: number): void { @@ -389,11 +415,9 @@ export const LoadPluginDialog = // let selectedUri = this.state.selected_uri; } - vst3_indicator(uiPlugin? : UiPlugin) - { - if (uiPlugin?.is_vst3) - { - return (vst) + vst3_indicator(uiPlugin?: UiPlugin) { + if (uiPlugin?.is_vst3) { + return (vst) } return null; } @@ -404,14 +428,36 @@ export const LoadPluginDialog = } return ""; } - info_string(uiPlugin?: UiPlugin): string { - if (uiPlugin === undefined) return ""; - let result = uiPlugin.name; - if (uiPlugin.author_name !== "") { - result += ", " + uiPlugin.author_name; + info_string(uiPlugin?: UiPlugin): ReactElement { + if (!uiPlugin) { + return (); + } else { + let stereoIndicator = this.stereo_indicator(uiPlugin) + if (uiPlugin.author_name !== "") { + if (uiPlugin.author_homepage !== "") { + return ( +
+ {uiPlugin.name + ",\u00A0"} +
+ +
); + + } else { + return ( + {uiPlugin.name + ", " + uiPlugin.author_name} + ); + } + } + return ( + + {uiPlugin?.name ?? ""} + ); + } - result += this.stereo_indicator(uiPlugin); - return result; } createFilterChildren(result: ReactNode[], classNode: PluginClass, level: number): void { for (let i = 0; i < classNode.children.length; ++i) { @@ -432,29 +478,35 @@ export const LoadPluginDialog = return result; } - getFilteredPlugins(): UiPlugin[] { + getFilteredPlugins(searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] { + if (searchString === null) { + searchString = this.state.search_string; + } + if (filterType === null) { + filterType = this.state.filterType; + } + if (favoritesList === null) { + favoritesList = this.state.favoritesList; + } let plugins = this.model.ui_plugins.get(); - let searchString = this.state.search_string; let results: { score: number; plugin: UiPlugin }[] = []; let searchFilter = new SearchFilter(searchString); - let filterType = this.state.filterType; let rootClass = this.model.plugin_classes.get(); for (let i = 0; i < plugins.length; ++i) { let plugin = plugins[i]; if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) { - let score:number = 0; - if (plugin.is_vst3) - { - score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name,"vst3"); + let score: number = 0; + if (plugin.is_vst3) { + score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name, "vst3"); } else { score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name); } if (score !== 0) { - if (this.state.favoritesList[plugin.uri]) { + if (favoritesList[plugin.uri]) { score += 32768; } results.push({ score: score, plugin: plugin }); @@ -476,9 +528,38 @@ export const LoadPluginDialog = changedSearchString?: string = undefined; hSearchTimeout?: NodeJS.Timeout; + private scrollToRequested = false; + requestScrollTo() { + this.scrollToRequested = true; + } + + handleScrollToCallback(element: FixedSizeGrid): void { + if (element) { + if (this.scrollToRequested) { + this.scrollToRequested = false; + let position = -1; + let gridItems = this.state.gridItems; + for (let i = 0; i < gridItems.length; ++i) { + if (this.state.selected_uri === gridItems[i].uri) { + position = i; + break; + } + } + if (position != -1) { + element.scrollToItem({ rowIndex: Math.floor(position / this.state.grid_cell_columns) }); + } + } + } + } + + handleSearchStringReady() { if (this.changedSearchString !== undefined) { - this.setState({ search_string: this.changedSearchString }); + this.requestScrollTo(); + this.setState({ + search_string: this.changedSearchString, + gridItems: this.getFilteredPlugins(this.changedSearchString, null, null) + }); this.changedSearchString = undefined; } this.hSearchTimeout = undefined; @@ -498,14 +579,15 @@ export const LoadPluginDialog = } renderItem(row: number, column: number): React.ReactNode { let item: number = (row) * this.gridColumnCount + (column); - - if (item >= this.gridItems.length) { + let gridItems = this.state.gridItems; + if (item >= gridItems.length) { return (
); } - let value = this.gridItems[item]; + let value = gridItems[item]; let classes = this.props.classes; let isFavorite: boolean = this.state.favoritesList[value.uri] ?? false; + return (
{ this.onDoubleClick(e, value.uri) }} @@ -520,16 +602,19 @@ export const LoadPluginDialog =
-
- - {value.name} - { - isFavorite && ( - +
- ) - } + + {value.name} + { + isFavorite && ( +
+ +
+ ) + } +
{value.plugin_display_type} {this.stereo_indicator(value)} @@ -549,7 +634,6 @@ export const LoadPluginDialog = if (!pluginUri) return; this.model.setFavorite(pluginUri, isFavorite); } - gridItems: UiPlugin[] = []; gridColumnCount: number = 1; render() { @@ -565,7 +649,6 @@ export const LoadPluginDialog = if (this.state.client_width < 500) { showSearchIcon = this.state.search_collapsed } - this.gridItems = this.getFilteredPlugins(); let gridColumnCount = this.state.grid_cell_columns; this.gridColumnCount = gridColumnCount; let isFavorite = this.state.favoritesList[this.state.selected_uri ?? ""]; @@ -574,7 +657,7 @@ export const LoadPluginDialog = { this.handleKeyPress(e); }} fullScreen={true} - TransitionComponent={Transition} + TransitionComponent={undefined} maxWidth={false} open={this.props.open} scroll="body" @@ -603,13 +686,34 @@ export const LoadPluginDialog = this.handleSearchStringChanged(text); }} onClearFilterClick={() => { - this.setState({ search_collapsed: true, search_string: "" }); + if (this.state.search_string !== "") { + this.requestScrollTo(); + this.setState({ + search_collapsed: true, + search_string: "", + gridItems: this.getFilteredPlugins("", null, null) + }); + + } else { + this.setState({ search_collapsed: true }); + } }} onClick={() => { if (this.state.search_collapsed) { this.setState({ search_collapsed: false }); } else { - this.setState({ search_collapsed: true, search_string: "" }); + + if (this.state.search_string !== "") { + this.requestScrollTo(); + this.setState({ + search_collapsed: true, + search_string: "", + gridItems: this.getFilteredPlugins("", null, null) + }); + + } else { + this.setState({ search_collapsed: true }); + } } }} @@ -624,13 +728,13 @@ export const LoadPluginDialog = )}
-
+
{ this.onFilterChange(e); }} - sx={{ minWidth: 160}} + sx={{ minWidth: 160 }} > {this.createFilterOptions()} @@ -643,22 +747,33 @@ export const LoadPluginDialog = flex: "1 1 100px", }} > - {(arg: { height: number, width: number }) => { + {(arg) => { + if ((!arg.width) || (!arg.height)) { + return (
); + } + let width = arg.width ?? 1; + let height = arg.height ?? 1; + let gridItems = this.state.gridItems; + + let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); } + + return ( { let index = args.columnIndex + this.gridColumnCount * args.rowIndex; - if (index >= this.gridItems.length) { + if (index >= gridItems.length) { return "blank-" + args.columnIndex + "-" + args.rowIndex; } - let plugin = this.gridItems[index]; + let plugin = gridItems[index]; return plugin.uri + "-" + args.rowIndex + "-" + args.columnIndex; }} @@ -680,11 +795,9 @@ export const LoadPluginDialog = {(this.state.client_width >= NARROW_DISPLAY_THRESHOLD) ? (
-
+
- - {this.info_string(selectedPlugin)} - + {this.info_string(selectedPlugin)} {this.vst3_indicator(selectedPlugin)}
- - {this.info_string(selectedPlugin)} - + {this.info_string(selectedPlugin)}
{ this.setFavorite(this.state.selected_uri, !isFavorite); }} @@ -722,11 +833,11 @@ export const LoadPluginDialog =
-
+
- - + +
diff --git a/react/src/Lv2Plugin.tsx b/react/src/Lv2Plugin.tsx index 855ae5e..2d52d97 100644 --- a/react/src/Lv2Plugin.tsx +++ b/react/src/Lv2Plugin.tsx @@ -167,6 +167,37 @@ export class PiPedalFileProperty { return result; } + private static getFileExtension(name: string): string { + let pos = name.lastIndexOf('.'); + let filenamePos = name.lastIndexOf('/') +1; + filenamePos = Math.max(name.lastIndexOf('\\')+1); + filenamePos = Math.max(name.lastIndexOf(':')+1); + if (pos < filenamePos) + { + return ""; + } + + if (pos !== -1) { + return name.substring(pos); + } + return ""; + } + + wantsFile(filename: string) : boolean { + if (this.fileTypes.length === 0) { + return true; + } + let extension = PiPedalFileProperty.getFileExtension(filename); + for (let fileType of this.fileTypes) + { + if (fileType.fileExtension === extension ) + { + return true; + } + } + return false; + } + name: string = ""; fileTypes: PiPedalFileType[] = []; patchProperty: string = ""; @@ -290,7 +321,7 @@ export enum PluginType { export enum ControlType { Dial, OnOffSwitch, - Toggle, + ABSwitch, Select } @@ -300,6 +331,7 @@ export class UiControl implements Deserializable { this.symbol = input.symbol; this.name = input.name; this.index = input.index; + this.is_input = input.is_input; this.min_value = input.min_value; this.max_value = input.max_value; this.default_value = input.default_value; @@ -320,33 +352,55 @@ export class UiControl implements Deserializable { this.is_program_controller = input.is_program_controller? true: false; this.custom_units = input.custom_units ?? ""; - this.controlType = ControlType.Dial; if (this.is_bypass) { this.not_on_gui = true; } + this.controlType = ControlType.Dial; - if (this.enumeration_property && this.scale_points.length === 2) - { - this.controlType = ControlType.Toggle; - } else { - if (this.min_value === 0 && this.max_value === 1) - { - if (this.toggled_property || this.integer_property || this.range_steps === 2) - { - this.controlType = ControlType.OnOffSwitch; - } - } - } - if (this.controlType === ControlType.Dial && this.enumeration_property) + if (this.isValidEnumeration()) { this.controlType = ControlType.Select; + if (this.scale_points.length === 2) + { + this.controlType = ControlType.ABSwitch; + } + } else { + if (this.toggled_property || (this.integer_property && this.min_value === 0 && this.max_value === 1)) + { + this.controlType = ControlType.OnOffSwitch; + } } return this; } + private hasScalePoint(value: number): boolean { + for (let scale_point of this.scale_points) + { + if (scale_point.value === value) return true; + } + return false; + } + private isValidEnumeration() : boolean { + if (this.enumeration_property) return true; + if (this.toggled_property && this.min_value === 0 && this.max_value === 1) + { + if (this.hasScalePoint(this.min_value) && this.hasScalePoint(this.max_value)) + { + return true; + } + } + if (this.integer_property && this.min_value === 0 && this.max_value === 1) + { + if (this.hasScalePoint(this.min_value) && this.hasScalePoint(this.max_value)) + { + return true; + } + } + return false; + } controlType: ControlType = ControlType.Dial; // non-serializable. @@ -361,6 +415,7 @@ export class UiControl implements Deserializable { symbol: string = ""; name: string = ""; index: number = -1; + is_input: boolean = true; min_value: number = 0; max_value: number = 1; default_value:number = 0.5; @@ -406,7 +461,7 @@ export class UiControl implements Deserializable { } isAbToggle(): boolean { - return this.controlType === ControlType.Toggle; + return this.controlType === ControlType.ABSwitch; } isSelect() : boolean { return this.controlType === ControlType.Select; @@ -426,9 +481,6 @@ export class UiControl implements Deserializable { if (range > 1) range = 1; if (range < 0) range = 0; - if (this.range_steps !== 0) { - range = Math.round(range*this.range_steps)/this.range_steps; - } return range; } @@ -438,9 +490,7 @@ export class UiControl implements Deserializable { if (range > 1) range = 1; if (this.toggled_property) return range === 0? 0: 1; - if (this.range_steps !== 0) { - range = Math.round(range * this.range_steps) / this.range_steps; - } + let value = range * (this.max_value - this.min_value) + this.min_value; if (this.integer_property || this.enumeration_property) { value = Math.round(value); diff --git a/react/src/MainPage.tsx b/react/src/MainPage.tsx index 2677dbe..aea72a1 100644 --- a/react/src/MainPage.tsx +++ b/react/src/MainPage.tsx @@ -45,6 +45,7 @@ import PluginInfoDialog from './PluginInfoDialog'; import { GetControlView } from './ControlViewFactory'; import MidiBindingsDialog from './MidiBindingsDialog'; import PluginPresetSelector from './PluginPresetSelector'; +import SaveIconOutline from '@mui/icons-material/Save'; const SPLIT_CONTROLBAR_THRESHHOLD = 650; @@ -75,7 +76,7 @@ const styles = ({ palette }: Theme) => createStyles({ flex: "0 0 auto", width: "100%", height: 48 }, splitControlBar: { - flex: "0 0 48px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16 + flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16 }, controlContent: { flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240 @@ -83,8 +84,8 @@ const styles = ({ palette }: Theme) => createStyles({ controlContentSmall: { flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden", }, - title: { fontSize: "1.1em", fontWeight: 700, marginRight: 8 }, - author: { fontWeight: 500, marginRight: 8 } + title: { fontSize: "1.1em", fontWeight: 700, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 }, + author: { fontWeight: 500, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap",opacity: 0.75 } }); @@ -103,7 +104,7 @@ interface MainState { horizontalScrollLayout: boolean; showMidiBindingsDialog: boolean; screenHeight: number; - + } @@ -128,7 +129,7 @@ export const MainPage = horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, showMidiBindingsDialog: false, screenHeight: this.windowSize.height - + }; this.onSelectionChanged = this.onSelectionChanged.bind(this); this.onPedalDoubleClick = this.onPedalDoubleClick.bind(this); @@ -193,16 +194,17 @@ export const MainPage = } onPedalboardChanged(value: Pedalboard) { let selectedItem = -1; - if (value.hasItem(this.state.selectedPedal)) - { + if (this.state.selectedPedal == Pedalboard.START_CONTROL || this.state.selectedPedal == Pedalboard.END_CONTROL) { + selectedItem = this.state.selectedPedal; + } else if (value.hasItem(this.state.selectedPedal)) { selectedItem = this.state.selectedPedal; } else { selectedItem = value.getFirstSelectableItem(); } - this.setState({ + this.setState({ pedalboard: value, selectedPedal: selectedItem - }); + }); } onDeletePedal(instanceId: number): void { let result = this.model.deletePedalboardPedal(instanceId); @@ -274,6 +276,15 @@ export const MainPage = let pedalboard = this.model.pedalboard.get(); if (!pedalboard) return null; + + if (selectedId === Pedalboard.START_CONTROL) // synthetic input volume item. + { + return pedalboard.makeStartItem(); + } else if (selectedId === Pedalboard.END_CONTROL) // synthetic output volume. + { + return pedalboard.makeEndItem(); + } + let it = pedalboard.itemsGenerator(); if (!selectedId) return null; while (true) { @@ -297,7 +308,7 @@ export const MainPage = } getSelectedUri(): string { let pedalboardItem = this.getSelectedPedalboardItem(); - if (pedalboardItem == null) return ""; + if (pedalboardItem === null) return ""; return pedalboardItem.uri; } titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode { @@ -311,7 +322,13 @@ export const MainPage = title = ""; } else if (pedalboardItem.isSplit()) { title = "Split"; - } else { + } else if (pedalboardItem.isSyntheticItem()) { + title = pedalboardItem.pluginName ?? "#error"; + author = ""; + presetsUri = ""; + pluginUri = ""; + } + else { let uiPlugin = this.model.getUiPlugin(pedalboardItem.uri); if (!uiPlugin) { missing = true; @@ -327,46 +344,41 @@ export const MainPage = } } let classes = this.props.classes; - if (missing) - { + if (missing) { return ( -
-
- - {title} - +
+
+ + {title} + +
-
); - + } else { - return ( -
-
- + return ( +
+
{title} - - {author} - +
+
+ +
+
+ +
-
- -
-
- -
-
- ); + ); } } @@ -379,7 +391,9 @@ export const MainPage = let bypassVisible = false; let bypassChecked = false; let canDelete = false; - let canAdd = false; + let canInsert = false; + let canAppend = false; + let canLoad = true; let instanceId = -1; let missing = false; let pluginUri = "#error"; @@ -388,13 +402,26 @@ export const MainPage = canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId); instanceId = pedalboardItem.instanceId; if (pedalboardItem.isEmpty()) { - canAdd = true; + canInsert = true; + canAppend = true; + + } else if (pedalboardItem.isStart()) { + canAppend = true; + canDelete = false; + canLoad = false; + } else if (pedalboardItem.isEnd()) { + canInsert = true; + canDelete = false; + canLoad = false; } else if (pedalboardItem.isSplit()) { - canAdd = true; + canInsert = true; + canAppend = true; + canLoad = false; } else { pluginUri = pedalboardItem.uri; uiPlugin = this.model.getUiPlugin(pluginUri); - canAdd = true; + canInsert = true; + canAppend = true; if (uiPlugin) { bypassVisible = true; bypassChecked = pedalboardItem.isEnabled; @@ -418,7 +445,7 @@ export const MainPage =
@@ -432,7 +459,7 @@ export const MainPage =
-
+
{ this.onAddClick(e) }} size="large"> @@ -444,11 +471,11 @@ export const MainPage = onClose={() => this.handleAddClose()} TransitionComponent={Fade} > - this.onInsertPedal(instanceId)}>Insert pedal - this.onAppendPedal(instanceId)}>Append pedal + {canInsert && ( this.onInsertPedal(instanceId)}>Insert pedal)} + {canAppend && ( this.onAppendPedal(instanceId)}>Append pedal)} - this.onInsertSplit(instanceId)}>Insert split - this.onAppendSplit(instanceId)}>Append split + {canInsert && ( this.onInsertSplit(instanceId)}>Insert split)} + {canAppend && ( this.onAppendSplit(instanceId)}>Append split)}
@@ -464,9 +491,9 @@ export const MainPage = color="primary" size="small" onClick={this.onLoadClick} - disabled={this.state.selectedPedal === -1 || (this.getSelectedPedalboardItem()?.isSplit() ?? true)} + disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)} startIcon={} - style={{ borderRadius: 24, paddingLeft: 18, paddingRight: 18, textTransform: "none"}} + style={{ textTransform: "none" }} > Load @@ -493,19 +520,19 @@ export const MainPage =
{ missing ? ( -
+
Error: Plugin is not installed. - {pluginUri} + {pluginUri}
- ): - ( - GetControlView(pedalboardItem) - ) + ) : + ( + GetControlView(pedalboardItem) + ) }
this.setState({showMidiBindingsDialog: false} ) } + onClose={() => this.setState({ showMidiBindingsDialog: false })} /> { (this.state.loadDialogOpen) && ( diff --git a/react/src/OldDeleteIcon.tsx b/react/src/OldDeleteIcon.tsx new file mode 100644 index 0000000..e5abb01 --- /dev/null +++ b/react/src/OldDeleteIcon.tsx @@ -0,0 +1,9 @@ + +import { createSvgIcon } from '@mui/material/utils'; + +const OldDeleteIcon = createSvgIcon( + , + "Delete", +); + +export default OldDeleteIcon; \ No newline at end of file diff --git a/react/src/Pedalboard.tsx b/react/src/Pedalboard.tsx index 38c222c..093bf2e 100644 --- a/react/src/Pedalboard.tsx +++ b/react/src/Pedalboard.tsx @@ -24,7 +24,6 @@ import MidiBinding from './MidiBinding'; const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Split"; const EMPTY_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Empty"; - interface Deserializable { deserialize(input: any): T; } @@ -92,6 +91,10 @@ export class PedalboardItem implements Deserializable { return result; } + isSyntheticItem(): boolean { + return this.instanceId === Pedalboard.START_CONTROL + || this.instanceId === Pedalboard.END_CONTROL; + } getInstanceId() : number { if (this.instanceId === undefined) { @@ -105,6 +108,12 @@ export class PedalboardItem implements Deserializable { isSplit(): boolean { return this.uri === SPLIT_PEDALBOARD_ITEM_URI; } + isStart(): boolean { + return this.uri === Pedalboard.START_PEDALBOARD_ITEM_URI; + } + isEnd(): boolean { + return this.uri === Pedalboard.END_PEDALBOARD_ITEM_URI; + } getControl(key: string): ControlValue { for (let i = 0; i < this.controlValues.length; ++i) { @@ -186,7 +195,7 @@ export class PedalboardItem implements Deserializable { controlValues: ControlValue[] = ControlValue.EmptyArray; midiBindings: MidiBinding[] = []; vstState: string = ""; - lv2State: any = {}; + lv2State: [boolean,any] = [false,{}] }; @@ -253,8 +262,15 @@ export class PedalboardSplitItem extends PedalboardItem { export class Pedalboard implements Deserializable { + static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume. + static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume. + static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start"; + static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End"; + deserialize(input: any): Pedalboard { this.name = input.name; + this.input_volume_db = input.input_volume_db; + this.output_volume_db = input.output_volume_db; this.items = PedalboardItem.deserializeArray(input.items); this.nextInstanceId = input.nextInstanceId ?? -1; return this; @@ -264,6 +280,8 @@ export class Pedalboard implements Deserializable { return new Pedalboard().deserialize(this); } name: string = ""; + input_volume_db: number = 0; + output_volume_db: number = 0; items: PedalboardItem[] = []; nextInstanceId: number = -1; @@ -316,6 +334,27 @@ export class Pedalboard implements Deserializable { } return null; } + makeStartItem(): PedalboardItem { + let result = new PedalboardItem(); + result.pluginName = "Input"; + result.instanceId = Pedalboard.START_CONTROL; + result.uri = Pedalboard.START_PEDALBOARD_ITEM_URI; + result.isEnabled = true; + result.controlValues = [new ControlValue("volume_db",this.input_volume_db)]; + return result; + + } + makeEndItem(): PedalboardItem { + let result = new PedalboardItem(); + result.pluginName = "Output"; + result.instanceId = Pedalboard.END_CONTROL; + result.uri = Pedalboard.END_PEDALBOARD_ITEM_URI; + result.isEnabled = true; + result.controlValues = [new ControlValue("volume_db",this.output_volume_db)]; + return result; + + } + getItem(instanceId: number): PedalboardItem { let it = this.itemsGenerator(); while (true) @@ -561,7 +600,7 @@ export class Pedalboard implements Deserializable { throw new PiPedalArgumentError("instanceId not found."); } } - _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean) + private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean) { for (let i = 0; i < items.length; ++i) { diff --git a/react/src/PedalboardView.tsx b/react/src/PedalboardView.tsx index 59245bf..41ebb8e 100644 --- a/react/src/PedalboardView.tsx +++ b/react/src/PedalboardView.tsx @@ -33,14 +33,16 @@ import Draggable from './Draggable' import Rect from './Rect'; import {PiPedalStateError} from './PiPedalError'; import Utility from './Utility' - - import { - Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType, + Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType } from './Pedalboard'; -const START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start"; -const END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End"; +const START_CONTROL = Pedalboard.START_CONTROL; +const END_CONTROL = Pedalboard.END_CONTROL; + + +const START_PEDALBOARD_ITEM_URI = Pedalboard.START_PEDALBOARD_ITEM_URI; +const END_PEDALBOARD_ITEM_URI = Pedalboard.END_PEDALBOARD_ITEM_URI; const ENABLED_CONNECTOR_COLOR = "#666"; const DISABLED_CONNECTOR_COLOR = "#CCC"; @@ -77,26 +79,6 @@ const pedalboardStyles = (theme: Theme) => createStyles({ position: "relative", overflow: "visible", - }, - startItem: { - position: "absolute", - display: "flex", - alignItems: "center", - justifyContent: "center", - - width: CELL_WIDTH, - height: CELL_HEIGHT - - }, - endItem: { - position: "absolute", - display: "flex", - alignItems: "center", - justifyContent: "center", - - width: CELL_WIDTH, - height: CELL_HEIGHT - }, splitItem: { position: "absolute", @@ -125,7 +107,13 @@ const pedalboardStyles = (theme: Theme) => createStyles({ right: 0, top: 0 }, - + buttonDraggable: { + display: "flex", + alignItem: "center", + justifyContent: "center", + width: "100%", + height: "100%" + }, pedalItem: { position: "absolute", width: CELL_WIDTH, @@ -150,6 +138,23 @@ const pedalboardStyles = (theme: Theme) => createStyles({ border: "1pt #666 solid", borderRadius: 6 }, + borderlessIconFrame: { + + display: "flex", + alignItems: "center", + justifyContent: "center", + + background: "transparent", + marginLeft: (CELL_WIDTH - FRAME_SIZE) / 2, + marginRight: (CELL_WIDTH - FRAME_SIZE) / 2, + marginTop: (CELL_HEIGHT - FRAME_SIZE) / 2, + marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, + width: FRAME_SIZE, + height: FRAME_SIZE, + border: "0pt #666 solid", + borderRadius: 6 + }, + pedalIcon: { width: 24, height: 24, @@ -632,7 +637,7 @@ const PedalboardView = if (bounds.height < TWO_ROW_HEIGHT) { let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height))/2); - this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra )); + this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra/2 )); bounds.height += extra; } else { @@ -677,7 +682,6 @@ const PedalboardView = } - // XXX set keys on output objects !! renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { // let classes = this.props.classes; let x_ = item.bounds.x + CELL_WIDTH / 2; @@ -688,14 +692,14 @@ const PedalboardView = if (numberOfOutputs === 2) { output.push(( - + )); output.push(( - + )); } else if (numberOfOutputs === 1) { output.push(( - + )); } } @@ -718,18 +722,18 @@ const PedalboardView = let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); + output.push(()); + output.push(()); } else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) { - output.push(()); + output.push(()); } if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); + output.push(()); + output.push(()); } else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) { - output.push(()); + output.push(()); } let lastTop = item.topChildren[item.topChildren.length - 1]; @@ -813,26 +817,26 @@ const PedalboardView = // display stereo strokes with cutoff line. if (firstPathStereo) { output.push(( - + )); output.push(( - + )); } else if (!firstPathAbsent) { output.push(( - + )); } if (secondPathStereo) { output.push(( - + )); output.push(( - + )); } else if (!secondPathAbsent) { output.push(( - + )); } @@ -840,32 +844,32 @@ const PedalboardView = // stereo strokes merge. if (firstPathStereo) { output.push(( - + )); } else if (!firstPathAbsent) { output.push(( - + )); } if (secondPathStereo) { output.push(( - + )); } else if (!secondPathAbsent) { output.push(( - + )); } // draw stereo inner lines. if (firstPathStereo) { output.push(( - + )); } if (secondPathStereo) { output.push(( - + )); } @@ -874,10 +878,10 @@ const PedalboardView = { // stereo output of L/R splitter output.push(( - + )); output.push(( - + )); @@ -898,25 +902,24 @@ const PedalboardView = throw new PiPedalStateError("scroll container not found."); } - pedalButton(instanceId: number, iconUrl: string, draggable: boolean, enabled: boolean): ReactNode { + pedalButton(instanceId: number, iconUrl: string, draggable: boolean, enabled: boolean,hasBorder: boolean = true): ReactNode { let classes = this.props.classes; return ( -
{ e.preventDefault(); }}> +
{ e.preventDefault(); }}> { this.onItemClick(e, instanceId); }} onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} > - - this.getScrollContainer()} - onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} - > -
+ + this.getScrollContainer()} + onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} + > Pedal -
-
+ style={{opacity: enabled? 0.99: 0.6}} /> + +
); @@ -945,6 +948,7 @@ const PedalboardView = let outputs: ReactNode[] = []; this.renderConnectors(outputs, layoutChain, true, false); return ( +
@@ -954,6 +958,7 @@ const PedalboardView = +
); } @@ -973,19 +978,25 @@ const PedalboardView = let item = v.value; switch (item.uri) { case START_PEDALBOARD_ITEM_URI: - result.push(
- + result.push(
+
+ + {this.pedalButton(START_CONTROL, item.iconUrl, false,true,false)} +
); break; case END_PEDALBOARD_ITEM_URI: - result.push(
- + result.push(
+
+ + {this.pedalButton(END_CONTROL, item.iconUrl, false,true,false)} +
); break; default: if (item.isSplitter()) { - result.push(
+ result.push(
{this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false,true)}
@@ -993,7 +1004,7 @@ const PedalboardView = } else { result.push( -
@@ -1002,7 +1013,7 @@ const PedalboardView = >{item.pedalItem?.pluginName}
) - result.push(
+ result.push(
{this.pedalButton(item.pedalItem?.instanceId ?? -1, item.iconUrl, !item.isEmpty(), item.pedalItem?.isEnabled ?? false)}
); @@ -1151,10 +1162,10 @@ const PedalboardView = } currentLayout?: PedalLayout[]; - + private renderKey: number = 0; render() { const { classes } = this.props; - + this.renderKey = 0; let layoutChain = makeChain(this.model, this.state.pedalboard?.items); let start = PedalLayout.Start(); let end = PedalLayout.End(); diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index da8e1bc..ffe201f 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -38,6 +38,7 @@ import GovernorSettings from './GovernorSettings'; import WifiChannel from './WifiChannel'; import AlsaDeviceInfo from './AlsaDeviceInfo'; import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost'; +import Xxx from './XxxSnippet'; export enum State { @@ -338,153 +339,6 @@ export interface FavoritesList { [url: string]: boolean; } -// export interface PiPedalModel { -// clientId: number; -// countryCodes: Object; -// debug: boolean; -// serverVersion?: PiPedalVersion; -// errorMessage: ObservableProperty; -// alertMessage: ObservableProperty; -// showStatusMonitor: ObservableProperty; -// setShowStatusMonitor(show: boolean): void; -// state: ObservableProperty; -// visibilityState: ObservableProperty; - -// ui_plugins: ObservableProperty; -// plugin_classes: ObservableProperty; -// jackConfiguration: ObservableProperty; -// jackSettings: ObservableProperty; -// banks: ObservableProperty; -// jackServerSettings: ObservableProperty; -// wifiConfigSettings: ObservableProperty; -// wifiDirectConfigSettings: ObservableProperty; -// governorSettings: ObservableProperty; -// favorites: ObservableProperty; - -// presets: ObservableProperty; -// systemMidiBindings: ObservableProperty; - - -// zoomedUiControl: ObservableProperty; - -// setJackServerSettings(jackServerSettings: JackServerSettings): void; -// setMidiBinding(instanceId: number, midiBinding: MidiBinding): void; - - -// getUiPlugin(uri: string): UiPlugin | null; - -// pedalboard: ObservableProperty; -// loadPedalboardPlugin(itemId: number, selectedUri: string): number; - -// setPedalboardControl(instanceId: number, key: string, value: number): void; -// setPedalboardItemEnabled(instanceId: number, value: boolean): void; -// previewPedalboardValue(instanceId: number, key: string, value: number): void; -// deletePedalboardPedal(instanceId: number): number | null; - -// movePedalboardItem(fromInstanceId: number, toInstanceId: number): void; -// movePedalboardItemToStart(fromInstanceId: number): void; -// movePedalboardItemToEnd(fromInstanceId: number): void; -// movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void; -// movePedalboardItemAfter(fromInstanceId: number, toInstanceId: number): void; - -// addPedalboardItem(instanceId: number, append: boolean): number; -// addPedalboardSplitItem(instanceId: number, append: boolean): number; -// setPedalboardItemEmpty(instanceId: number): number; - -// saveCurrentPreset(): void; -// saveCurrentPresetAs(newName: string): void; - -// saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): void; - -// loadPreset(instanceId: number): void; -// updatePresets(presets: PresetIndex): Promise; -// deletePresetItem(instanceId: number): Promise; -// renamePresetItem(instanceId: number, name: string): Promise; -// copyPreset(fromId: number, toId: number): Promise; -// duplicatePreset(instanceId: number): Promise; - -// moveBank(from: number, to: number): Promise; -// deleteBankItem(instanceId: number): Promise; - - -// showAlert(message: string): void; - -// setJackSettings(jackSettings: JackChannelSelection): void; - -// svgImgUrl(svgImage: string): string; - -// removeVuSubscription(handle: VuSubscriptionHandle): void; -// addVuSubscription(instanceId: number, vuChangedHandler: VuChangedHandler): VuSubscriptionHandle; - -// monitorPort(instanceid: number, key: string, updateRateSeconds: number, onUpdated: (value: number) => void): MonitorPortHandle; -// unmonitorPort(handle: MonitorPortHandle): void; - -// getPatchProperty(instanceId: number, uri: string): Promise; -// setPatchProperty(instanceId: number, uri: string, value: any): Promise; - -// renameBank(instanceId: number, newName: string): Promise; -// saveBankAs(instanceId: number, newName: string): Promise; -// openBank(bankId: number): Promise; - -// getJackStatus(): Promise; - -// getPluginPresets(uri: string): Promise; -// loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void; -// updatePluginPresets(uri: string, presets: PluginUiPresets): Promise; -// duplicatePluginPreset(uri: string, instanceId: number): Promise; - -// setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void; - -// shutdown(): Promise; -// restart(): Promise; - -// listenForMidiEvent(listenForControlsOnly: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle; -// cancelListenForMidiEvent(listenHandle: ListenHandle): void; - -// addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle -// removeControlValueChangeListener(handle: ControlValueChangedHandle): void; - -// addPluginPresetsChangedListener(onPluginPresetsChanged: PluginPresetsChangedHandler): PluginPresetsChangedHandle; -// removePluginPresetsChangedListener(handle: PluginPresetsChangedHandle): void; - -// listenForAtomOutput(instanceId: number, onComplete: (instanceId: number, atomOutput: any) => void): ListenHandle; -// cancelListenForAtomOutput(listenHandle: ListenHandle): void; - - - -// download(targetType: string, isntanceId: number | string): void; - -// uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise; -// uploadBank(file: File, uploadAfter: number): Promise; - -// setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise; -// setWifiDirectConfigSettings(wifiDirectConfigSettings: WifiDirectConfigSettings): Promise; -// setGovernorSettings(governor: string): Promise; - -// getWifiChannels(countryIso3661: string): Promise; - -// getAlsaDevices(): Promise; - -// zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void; - -// onPreviousZoomedControl(): void; -// onNextZoomedControl(): void; -// clearZoomedControl(): void; - -// setFavorite(pluginUrl: string, isFavorite: boolean): void; - -// isAndroidHosted(): boolean; -// showAndroidDonationActivity(): void; -// getAndroidHostVersion(): string; -// chooseNewDevice(): void; - -// isOnboarding(): boolean; -// setOnboarding(value: boolean): void; - - - -// requestFileList(piPedalFileProperty: PiPedalFileProperty): Promise; -// }; export class PiPedalModel //implements PiPedalModel { @@ -608,6 +462,14 @@ export class PiPedalModel //implements PiPedalModel controlChangedBody.value, false // do NOT notify the server of the change. ); + } + else if (message === "onOutputVolumeChanged") { + let value = body as number; + this._setOutputVolume(value,false); + } + else if (message === "onInputVolumeChanged") { + let value = body as number; + this._setInputVolume(value,false); } else if (message === "onLv2StateChanged") { let instanceId = body as number; this.onLv2StateChanged(instanceId); @@ -755,6 +617,7 @@ export class PiPedalModel //implements PiPedalModel } + requestPluginClasses(): Promise { const myRequest = new Request(this.varRequest('plugin_classes.json')); return fetch(myRequest) @@ -881,7 +744,8 @@ export class PiPedalModel //implements PiPedalModel } - maxUploadSize: number = 512 * 1024; + maxFileUploadSize: number = 100000000; + maxPresetUploadSize: number = 512 * 1024; debug: boolean = false; @@ -895,7 +759,7 @@ export class PiPedalModel //implements PiPedalModel ) .then(data => { if (data.max_upload_size) { - this.maxUploadSize = data.max_upload_size; + this.maxPresetUploadSize = data.max_upload_size; } if (data.fakeAndroid) { this.androidHost = new FakeAndroidHost(); @@ -1295,13 +1159,93 @@ export class PiPedalModel //implements PiPedalModel }; this.webSocket?.send("setPatchProperty", body); } + + setInputVolume(volume_db: number) : void { + this._setInputVolume(volume_db,true); + } + previewInputVolume(volume_db: number) : void { + nullCast(this.webSocket).send("previewInputVolume",volume_db); + + } + previewOutputVolume(volume_db: number) : void { + nullCast(this.webSocket).send("previewOutputVolume",volume_db); + + } + + private _setInputVolume(volume_db: number, notifyServer: boolean) : void { + let changed: boolean = false; + + let pedalboard = this.pedalboard.get(); + if (pedalboard.input_volume_db != volume_db) + { + let newPedalboard = pedalboard.clone(); + newPedalboard.input_volume_db = volume_db; + this.pedalboard.set(newPedalboard); + changed = true; + } + if (changed) + { + if (notifyServer) { + nullCast(this.webSocket).send("setInputVolume",volume_db); + } + + for (let i = 0; i < this._controlValueChangeItems.length; ++i) { + let item = this._controlValueChangeItems[i]; + if (Pedalboard.START_CONTROL === item.instanceId) { + item.onValueChanged("volume_db", volume_db); + } + } + } + } + + setOutputVolume(volume_db: number, notifyServer: boolean) : void + { + this._setOutputVolume(volume_db,true); + } + + private _setOutputVolume(volume_db: number, notifyServer: boolean) : void { + let changed: boolean = false; + + let pedalboard = this.pedalboard.get(); + if (pedalboard.output_volume_db != volume_db) + { + let newPedalboard = pedalboard.clone(); + newPedalboard.output_volume_db = volume_db; + this.pedalboard.set(newPedalboard); + changed = true; + } + if (changed) + { + if (notifyServer) { + nullCast(this.webSocket).send("setOutputVolume",volume_db); + } + for (let i = 0; i < this._controlValueChangeItems.length; ++i) { + let item = this._controlValueChangeItems[i]; + if (Pedalboard.END_CONTROL === item.instanceId) { + item.onValueChanged("volume_db", volume_db); + } + } + } + } + + private _setPedalboardControlValue(instanceId: number, key: string, value: number, notifyServer: boolean): void { let pedalboard = this.pedalboard.get(); if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready."); let newPedalboard = pedalboard.clone(); + if (instanceId === Pedalboard.START_CONTROL && key == "volume_db") + { + this._setInputVolume(value,notifyServer); + return; + } else if (instanceId === Pedalboard.END_CONTROL) + { + this._setOutputVolume(value,notifyServer); + return; + } let item = newPedalboard.getItem(instanceId); let changed = item.setControlValue(key, value); + if (changed) { this.pedalboard.set(newPedalboard); if (notifyServer) { @@ -1427,6 +1371,16 @@ export class PiPedalModel //implements PiPedalModel // mouse is down. Don't update EVERYBODY, but we must change // the control on the running audio plugin. // TODO: respect "expensive" port attribute. + if (instanceId === Pedalboard.START_CONTROL && key == "volume_db") + { + this.previewInputVolume(value); + return; + } else if (instanceId === Pedalboard.END_CONTROL) + { + this.previewOutputVolume(value); + return; + } + this._setServerControl("previewControl", instanceId, key, value); } @@ -1642,6 +1596,12 @@ export class PiPedalModel //implements PiPedalModel .request('requestFileList', piPedalFileProperty); } + deleteUserFile(fileName: string) : Promise + { + return nullCast(this.webSocket).request('deleteUserFile',fileName) + } + + saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): Promise { // default behaviour is to save after the currently selected preset. let request: any = { @@ -2101,10 +2061,66 @@ export class PiPedalModel //implements PiPedalModel window.open(url, "_blank"); } + uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { + let result = new Promise((resolve, reject) => { + try { + if (file.size > this.maxFileUploadSize) { + reject("File is too large."); + } + let url = this.varServerUrl + uploadPage; + let parsedUrl = new URL(url); + let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop(); + let query = parsedUrl.search; + if (query.length === 0) + { + query += '?'; + } else { + query += '&'; + } + if (!fileNameOnly) + { + reject("Invalid filename."); + } + query += "filename=" + encodeURIComponent(fileNameOnly??""); + parsedUrl.search = query; + url = parsedUrl.toString(); + fetch( + url, + { + method: "POST", + body: file, + headers: { + 'Content-Type': contentType, + }, + signal: abortController?.signal + } + ) + .then((response: Response) => { + if (!response.ok) { + reject("Upload failed. " + response.statusText); + return; + } else { + return response.json(); + } + }) + .then((json) => { + resolve(json as string); + }) + .catch((error) => { + reject("Upload failed. " + error); + }) + ; + } catch (error) { + reject("Upload failed. " + error); + } + }); + return result; + } + uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise { let result = new Promise((resolve, reject) => { try { - if (file.size > this.maxUploadSize) { + if (file.size > this.maxPresetUploadSize) { reject("File is too large."); } let url = this.varServerUrl + uploadPage; @@ -2146,7 +2162,7 @@ export class PiPedalModel //implements PiPedalModel let result = new Promise((resolve, reject) => { try { console.log("File: " + file.name + " Size: " + file.size); - if (file.size > this.maxUploadSize) { + if (file.size > this.maxPresetUploadSize) { reject("File is too large."); } let url = this.varServerUrl + "uploadBank"; @@ -2387,6 +2403,11 @@ export class PiPedalModel //implements PiPedalModel ++ix; if (ix >= uiPlugin.controls.length) return; + while (!uiPlugin.controls[ix].is_input) + { + ++ix; + if (ix >= uiPlugin.controls.length) return; + } this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]); } @@ -2416,6 +2437,11 @@ export class PiPedalModel //implements PiPedalModel --ix; if (ix < 0) return; + while (!uiPlugin.controls[ix].is_input) + { + --ix; + if (ix < 0) return; + } this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]); } @@ -2471,6 +2497,11 @@ export class PiPedalModel //implements PiPedalModel } } + // returns the ID of the new preset. + newPresetItem(createAfter: number): Promise + { + return nullCast(this.webSocket).request("newPreset"); + } }; diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index b5c5d70..38feddd 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -1,4 +1,4 @@ - // Copyright (c) 2022 Robin Davies + // Copyright (c) 2022 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index 8010040..831025a 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -23,7 +23,7 @@ import { WithStyles } from '@mui/styles'; import createStyles from '@mui/styles/createStyles'; import withStyles from '@mui/styles/withStyles'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; -import { UiPlugin, UiControl, PiPedalFileProperty} from './Lv2Plugin'; +import { UiPlugin, UiControl, PiPedalFileProperty,ScalePoint} from './Lv2Plugin'; import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'; @@ -37,6 +37,8 @@ import FullScreenIME from './FullScreenIME'; import FilePropertyControl from './FilePropertyControl'; import FilePropertyDialog from './FilePropertyDialog'; import JsonAtom from './JsonAtom'; +import PluginOutputControl from './PluginOutputControl'; +import Units from './Units'; export const StandardItemSize = { width: 80, height: 110 }; @@ -45,6 +47,37 @@ export const StandardItemSize = { width: 80, height: 110 }; const LANDSCAPE_HEIGHT_BREAK = 500; + +function makeIoPluginInfo(name: string, uri: string) : UiPlugin { + let result = new UiPlugin(); + result.name = name; + result.uri = uri; + let volumeControl = new UiControl(); + volumeControl.name = "Volume"; + volumeControl.symbol = "volume_db"; + volumeControl.index = 0; + volumeControl.is_input = true; + + volumeControl.min_value = -60; + volumeControl.max_value = 30; + volumeControl.default_value = 0; + volumeControl.units = Units.db; + volumeControl.scale_points = [ + new ScalePoint().deserialize({label: "-INF",value: -60}) + ]; + result.controls = [ + volumeControl + ]; + return result; +} + +let startPluginInfo: UiPlugin = +makeIoPluginInfo("Input",Pedalboard.START_PEDALBOARD_ITEM_URI); + +let endPluginInfo: UiPlugin = +makeIoPluginInfo("Output",Pedalboard.END_PEDALBOARD_ITEM_URI); + + const styles = (theme: Theme) => createStyles({ frame: { display: "block", @@ -255,6 +288,7 @@ const PluginControlView = } + onWindowSizeChanged(width: number, height: number): void { this.setState({ landscapeGrid: height < LANDSCAPE_HEIGHT_BREAK }); } @@ -296,6 +330,13 @@ const PluginControlView = } makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode { let symbol = uiControl.symbol; + if (!uiControl.is_input) + { + return ( + + + ); + } let controlValue: ControlValue | undefined = undefined; for (let i = 0; i < controlValues.length; ++i) { @@ -435,10 +476,26 @@ const PluginControlView = return result; } + static startPluginInfo: UiPlugin = + makeIoPluginInfo("Input",Pedalboard.START_PEDALBOARD_ITEM_URI); + + static endPluginInfo: UiPlugin = + makeIoPluginInfo("Output",Pedalboard.END_PEDALBOARD_ITEM_URI); + render(): ReactNode { let classes = this.props.classes; - let pedalboardItem = this.model.pedalboard.get().getItem(this.props.instanceId); + let pedalboardItem: PedalboardItem; + let pedalboard = this.model.pedalboard.get(); + if (this.props.instanceId == Pedalboard.START_CONTROL) + { + pedalboardItem = pedalboard.makeStartItem(); + } else if (this.props.instanceId == Pedalboard.END_CONTROL) + { + pedalboardItem = pedalboard.makeEndItem(); + } else { + pedalboardItem = pedalboard.getItem(this.props.instanceId); + } if (!pedalboardItem) return (
); @@ -446,7 +503,18 @@ const PluginControlView = let controlValues = pedalboardItem.controlValues; - let plugin: UiPlugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri)); + let plugin: UiPlugin; + if (pedalboardItem.isStart()) + { + plugin = startPluginInfo; + controlValues = [new ControlValue("volume_db",pedalboard.input_volume_db)]; + } else if (pedalboardItem.isEnd()) + { + plugin = endPluginInfo; + controlValues = [new ControlValue("volume_db",pedalboard.output_volume_db)]; + } else { + plugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri)); + } controlValues = this.filterNotOnGui(controlValues, plugin); diff --git a/react/src/PluginOutputControl.tsx b/react/src/PluginOutputControl.tsx new file mode 100644 index 0000000..5ac874c --- /dev/null +++ b/react/src/PluginOutputControl.tsx @@ -0,0 +1,252 @@ +// Copyright (c) 2023 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, { Component } 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 { UiControl } from './Lv2Plugin'; +import Typography from '@mui/material/Typography'; +import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle } from './PiPedalModel'; + + + + + + +const styles = (theme: Theme) => createStyles({ + frame: { + position: "relative", + margin: "12px" + }, + switchTrack: { + height: '100%', + width: '100%', + borderRadius: 14 / 2, + zIndex: -1, + transition: theme.transitions.create(['opacity', 'background-color'], { + duration: theme.transitions.duration.shortest + }), + backgroundColor: theme.palette.secondary.main, + opacity: theme.palette.mode === 'light' ? 0.38 : 0.3 + }, + displayValue: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 4, + textAlign: "center", + background: "white", + color: "#666", + // zIndex: -1, + } +}); + + +export interface PluginOutputControlProps extends WithStyles { + uiControl: UiControl; + instanceId: number; +} +type PluginOutputControlState = { + hasValue: boolean; + value: number; +}; + +const PluginOutputControl = + withStyles(styles, { withTheme: true })( + class extends Component + { + + private model: PiPedalModel; + private vuRef: React.RefObject; + + + constructor(props: PluginOutputControlProps) { + super(props); + this.vuRef = React.createRef(); + this.state = { + hasValue: false, + value: 0 + }; + this.model = PiPedalModelFactory.getInstance(); + } + + private mounted: boolean = false; + componentWillUnmount() { + if (super.componentWillUnmount) { + super.componentWillUnmount(); + } + this.mounted = false; + this.unsubscribe(); + } + componentDidMount(): void { + this.mounted = true; + this.subscribe(); + if (super.componentDidMount) { + super.componentDidMount(); + } + } + + private VU_HEIGHT = 48-2; + private animationHandle: number | undefined = undefined; + + private updateValue(value: number) { + if (this.vuRef.current) { + let control = this.props.uiControl; + let range = (value-control.min_value)/(control.max_value-control.min_value); + let top = this.VU_HEIGHT-range*this.VU_HEIGHT; + + if (!this.animationHandle) + { + this.animationHandle = requestAnimationFrame( + ()=> { + if (this.vuRef.current) + { + this.vuRef.current.style.marginTop = top+"px"; + } + this.animationHandle = undefined; + } + ) + } + + } else { + if (this.mounted) { + this.setState({ hasValue: true, value: value }); + } + } + } + + private monitorPortHandle?: MonitorPortHandle; + + private subscribe(): void { + this.unsubscribe(); + this.monitorPortHandle = this.model.monitorPort( + this.props.instanceId, + this.props.uiControl.symbol, + 1 / 15.0, // update rate. + (value) => { + this.updateValue(value); + } + ); + + } + private unsubscribe(): void { + if (this.monitorPortHandle) { + this.model.unmonitorPort(this.monitorPortHandle) + this.monitorPortHandle = undefined; + } + } + + isShortSelect(control: UiControl) { + for (let scale_point of control.scale_points) { + if (scale_point.label.length > 12) return false; + } + return true; + } + + formatDisplayValue(uiControl: UiControl | undefined, value: number): string { + if (!uiControl) return ""; + + return uiControl.formatDisplayValue(value); + } + + render() { + + let control: UiControl = this.props.uiControl; + + + let text = ""; + if (this.state.hasValue) { + let value = this.state.value; + if (control.isOnOffSwitch()) { + text = value === 0 ? "OFF" : "ON"; + } else { + text = this.formatDisplayValue(control, value); + } + } + + let isSelect = control.isSelect(); + + let item_width: number | undefined = isSelect ? 160 : 80; + if (isSelect) { + if (this.isShortSelect(control)) { + item_width = 80; + } + } + if (control.isDial()) { + item_width = undefined; + return ( +
+ {/* TITLE SECTION */} +
+ {control.name} +
+ {/* CONTROL SECTION */} + +
+
+
+
+
+
+ +
+ + {/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/} +
+   +
+
+ + ); + } else { + return ( +
+ {/* TITLE SECTION */} +
+ {control.name} +
+ {/* CONTROL SECTION */} + +
+ + {text} + +
+ + {/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/} +
+   +
+
+ ); + } + } + } + ); + +export default PluginOutputControl; \ No newline at end of file diff --git a/react/src/PluginPresetsDialog.tsx b/react/src/PluginPresetsDialog.tsx index 1ccd4d1..d7ff77e 100644 --- a/react/src/PluginPresetsDialog.tsx +++ b/react/src/PluginPresetsDialog.tsx @@ -33,7 +33,7 @@ import ListItemText from '@mui/material/ListItemText'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; -import UploadDialog from './UploadDialog'; +import UploadPresetDialog from './UploadPresetDialog'; import {PluginUiPresets,PluginUiPreset} from './PluginPreset'; import SelectHoverBackground from './SelectHoverBackground'; @@ -67,7 +67,7 @@ interface PluginPresetsDialogState { renameOpen: boolean; moreMenuAnchorEl: HTMLElement | null; - openUploadDialog: boolean; + openUploadPresetDialog: boolean; }; @@ -138,7 +138,7 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })( selectedItem: -1, renameOpen: false, moreMenuAnchorEl: null, - openUploadDialog: false + openUploadPresetDialog: false }; @@ -165,7 +165,7 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })( } handleUploadPresets() { this.handleMoreClose(); - this.setState({ openUploadDialog: true }); + this.setState({ openUploadPresetDialog: true }); } handleMoreClose(): void { @@ -466,14 +466,14 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })(
- {} } uploadAfter={this.state.selectedItem} - open={this.state.openUploadDialog} - onClose={() => { this.setState({ openUploadDialog: false }) }} /> + open={this.state.openUploadPresetDialog} + onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> diff --git a/react/src/PresetDialog.tsx b/react/src/PresetDialog.tsx index 9a793c4..c00a93a 100644 --- a/react/src/PresetDialog.tsx +++ b/react/src/PresetDialog.tsx @@ -33,7 +33,7 @@ import ListItemText from '@mui/material/ListItemText'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; -import UploadDialog from './UploadDialog'; +import UploadPresetDialog from './UploadPresetDialog'; import SelectHoverBackground from './SelectHoverBackground'; import CloseIcon from '@mui/icons-material//Close'; @@ -64,7 +64,7 @@ interface PresetDialogState { renameOpen: boolean; moreMenuAnchorEl: HTMLElement | null; - openUploadDialog: boolean; + openUploadPresetDialog: boolean; }; @@ -137,7 +137,7 @@ const PresetDialog = withStyles(styles, { withTheme: true })( selectedItem: presets.selectedInstanceId, renameOpen: false, moreMenuAnchorEl: null, - openUploadDialog: false + openUploadPresetDialog: false }; this.handlePresetsChanged = this.handlePresetsChanged.bind(this); @@ -162,7 +162,7 @@ const PresetDialog = withStyles(styles, { withTheme: true })( } handleUploadPreset() { this.handleMoreClose(); - this.setState({ openUploadDialog: true }); + this.setState({ openUploadPresetDialog: true }); } handleMoreClose(): void { @@ -445,14 +445,14 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
- this.setState({selectedItem: instanceId}) } uploadAfter={this.state.selectedItem} - open={this.state.openUploadDialog} - onClose={() => { this.setState({ openUploadDialog: false }) }} /> + open={this.state.openUploadPresetDialog} + onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> diff --git a/react/src/PresetSelector.tsx b/react/src/PresetSelector.tsx index 011e297..ae96175 100644 --- a/react/src/PresetSelector.tsx +++ b/react/src/PresetSelector.tsx @@ -36,7 +36,7 @@ import Fade from '@mui/material/Fade'; import Divider from '@mui/material/Divider'; import RenameDialog from './RenameDialog' import Select from '@mui/material/Select'; -import UploadDialog from './UploadDialog'; +import UploadPresetDialog from './UploadPresetDialog'; interface PresetSelectorProps extends WithStyles { @@ -55,7 +55,7 @@ interface PresetSelectorState { renameDialogDefaultName: string; renameDialogActionName: string; renameDialogOnOk?: (name: string) => void; - openUploadDialog: boolean; + openUploadPresetDialog: boolean; } @@ -100,7 +100,7 @@ const PresetSelector = renameDialogDefaultName: "", renameDialogActionName: "", renameDialogOnOk: undefined, - openUploadDialog: false + openUploadPresetDialog: false }; @@ -127,7 +127,7 @@ const PresetSelector = this.handlePresetsMenuClose(); e.preventDefault(); - this.setState({ openUploadDialog: true }); + this.setState({ openUploadPresetDialog: true }); } @@ -151,7 +151,7 @@ const PresetSelector = return this.model.saveCurrentPresetAs(newName); }) .then((newInstanceId) => { - + }) .catch((error) => { this.showError(error); @@ -179,6 +179,22 @@ const PresetSelector = }) ; } + handlePresetsMenuNew(e: SyntheticEvent): void { + this.handlePresetsMenuClose(); + e.stopPropagation(); + + let currentPresets = this.model.presets.get(); + let item = currentPresets.getItem(currentPresets.selectedInstanceId); + if (item == null) return; + let name = item.name; + this.model.newPresetItem(currentPresets.selectedInstanceId) + .then((instanceId) => { + this.model.loadPreset(instanceId); + }) + .catch((error) => { + this.showError(error); + }); + } showError(error: string) { this.model.showAlert(error); @@ -282,13 +298,24 @@ const PresetSelector = let presets = this.state.presets; let classes = this.props.classes; return ( -
+
+
+ { this.handleSave(); }} + size="large"> + + +
+
{true ? ( this.handleButtonSelect(e)} + /> + +
+ { + this.state.okEnabled ? + ( + + + ) : ( + + + ) + } + + + ); + } +} \ No newline at end of file diff --git a/react/src/UploadDialog.tsx b/react/src/UploadPresetDialog.tsx similarity index 94% rename from react/src/UploadDialog.tsx rename to react/src/UploadPresetDialog.tsx index 0502c7e..76dce88 100644 --- a/react/src/UploadDialog.tsx +++ b/react/src/UploadPresetDialog.tsx @@ -33,7 +33,7 @@ import Typography from '@mui/material/Typography'; // const BANK_EXTENSION = ".piBank"; // const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets"; -export interface UploadDialogProps { +export interface UploadPresetDialogProps { open: boolean, onClose: () => void, uploadAfter: number, @@ -44,15 +44,15 @@ export interface UploadDialogProps { }; -export interface UploadDialogState { +export interface UploadPresetDialogState { fullScreen: boolean; }; -export default class UploadDialog extends ResizeResponsiveComponent { +export default class UploadPresetDialog extends ResizeResponsiveComponent { model: PiPedalModel; - constructor(props: UploadDialogProps) { + constructor(props: UploadPresetDialogProps) { super(props); this.state = { fullScreen: false @@ -157,7 +157,7 @@ export default class UploadDialog extends ResizeResponsiveComponent this.handleClose()} + this.handleClose()} fullScreen={this.state.fullScreen} style={{userSelect: "none"}} > diff --git a/react/tmp.txt b/react/tmp.txt index 610ee0c..86dae80 100644 --- a/react/tmp.txt +++ b/react/tmp.txt @@ -49,7 +49,7 @@ src/GxTunerControl.tsx src/Banks.tsx src/PluginPresetsDialog.tsx src/BankDialog.tsx -src/UploadDialog.tsx +src/UploadPresetDialog.tsx src/ToobPowerStage2View.tsx src/index.tsx src/Units.tsx diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index c893ad7..22a8459 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -501,6 +501,20 @@ private: this->realtimeActivePedalboard->SetControlValue(body.effectIndex, body.controlIndex, body.value); break; } + case RingBufferCommand::SetInputVolume: + { + SetVolumeBody body; + realtimeReader.readComplete(&body); + this->realtimeActivePedalboard->SetInputVolume(body.value); + break; + } + case RingBufferCommand::SetOutputVolume: + { + SetVolumeBody body; + realtimeReader.readComplete(&body); + this->realtimeActivePedalboard->SetOutputVolume(body.value); + break; + } case RingBufferCommand::ParameterRequest: { RealtimePatchPropertyRequest *pRequest = nullptr; @@ -809,7 +823,7 @@ private: { if (this->realtimeVuBuffers != nullptr) { - pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes); + pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes,inputBuffers,outputBuffers); vuSamplesRemaining -= nframes; if (vuSamplesRemaining <= 0) @@ -1332,6 +1346,7 @@ public: } } } + } void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) @@ -1350,8 +1365,23 @@ public: } } + virtual void SetInputVolume(float value) { + std::lock_guard guard(mutex); + if (active && this->currentPedalboard) + { + hostWriter.SetInputVolume(value); + } + } + virtual void SetOutputVolume(float value) { + std::lock_guard guard(mutex); + if (active && this->currentPedalboard) + { + hostWriter.SetOutputVolume(value); + } + + } virtual void SetVuSubscriptions(const std::vector &instanceIds) { @@ -1372,21 +1402,33 @@ public: { int64_t instanceId = instanceIds[i]; auto effect = this->currentPedalboard->GetEffect(instanceId); - if (!effect) + if (effect) { - throw PiPedalStateException("Effect not found."); + int index = this->currentPedalboard->GetIndexOfInstanceId(instanceIds[i]); + vuConfig->enabledIndexes.push_back(index); + VuUpdate v; + v.instanceId_ = instanceId; + // Display mono VUs if a stereo device is being fed identical L/R inputs. + v.isStereoInput_ = effect->GetNumberOfInputAudioPorts() != 1 && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); + v.isStereoOutput_ = effect->GetNumberOfOutputAudioPorts() != 1; + + vuConfig->vuUpdateWorkingData.push_back(v); + vuConfig->vuUpdateResponseData.push_back(v); + } else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) { + int index = (int)instanceId; + VuUpdate v; + vuConfig->enabledIndexes.push_back(index); + v.instanceId_ = instanceId; + if (instanceId == Pedalboard::INPUT_VOLUME_ID) + { + v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; + } else { + v.isStereoInput_ = v.isStereoOutput_ =this->pHost->GetNumberOfOutputAudioChannels() > 1; + } + vuConfig->vuUpdateWorkingData.push_back(v); + vuConfig->vuUpdateResponseData.push_back(v); + } - - int index = this->currentPedalboard->GetIndexOfInstanceId(instanceIds[i]); - vuConfig->enabledIndexes.push_back(index); - VuUpdate v; - v.instanceId_ = instanceId; - // Display mono VUs if a stereo device is being fed identical L/R inputs. - v.isStereoInput_ = effect->GetNumberOfInputAudioPorts() != 1 && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); - v.isStereoOutput_ = effect->GetNumberOfOutputAudioPorts() != 1; - - vuConfig->vuUpdateWorkingData.push_back(v); - vuConfig->vuUpdateResponseData.push_back(v); } this->hostWriter.SetVuSubscriptions(vuConfig); @@ -1653,7 +1695,12 @@ void AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem) IEffect*effect = currentPedalboard->GetEffect(pedalboardItem.instanceId()); if (effect != nullptr) { - effect->GetLv2State(const_cast(&pedalboardItem.lv2State())); + try { + effect->GetLv2State(const_cast(&pedalboardItem.lv2State())); + } catch(const std::exception &e) + { + Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what())); + } } } diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 7350f91..9c43b91 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -207,7 +207,9 @@ public: virtual void SetPedalboard(const std::shared_ptr &pedalboard) = 0; virtual void SetControlValue(uint64_t instanceId,const std::string&symbol, float value) = 0; - virtual void SetPluginPreset(uint64_t instanceId, const std::vector & values) = 0; + virtual void SetInputVolume(float value) = 0; + virtual void SetOutputVolume(float value) = 0; + virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) = 0; virtual void SetBypass(uint64_t instanceId, bool enabled) = 0; virtual bool IsOpen() const = 0; diff --git a/src/AutoLilvNode.cpp b/src/AutoLilvNode.cpp index 00dd7e3..dffb26a 100644 --- a/src/AutoLilvNode.cpp +++ b/src/AutoLilvNode.cpp @@ -43,12 +43,17 @@ float AutoLilvNode::AsFloat(float defaultValue) } return defaultValue; } -int AutoLilvNode::AsInt(int defaultValue) +int32_t AutoLilvNode::AsInt(int defaultValue) { if (node == nullptr) return defaultValue; if (lilv_node_is_int(node)) return lilv_node_as_int(node); + if (lilv_node_is_float(node)) + { + return (int32_t)lilv_node_as_float(node); + } + return defaultValue; } bool AutoLilvNode::AsBool(bool defaultValue) diff --git a/src/AutoLilvNode.hpp b/src/AutoLilvNode.hpp index 684f48d..8d9522b 100644 --- a/src/AutoLilvNode.hpp +++ b/src/AutoLilvNode.hpp @@ -37,9 +37,9 @@ namespace pipedal { } AutoLilvNode(const LilvNode *node) - : node(const_cast(node)) + : node(const_cast(node)) { - isConst = true; + isConst = true; } AutoLilvNode(LilvNode *node) : node(node) @@ -66,15 +66,17 @@ namespace pipedal return *this; } - operator LilvNode**() { + operator LilvNode **() + { Free(); this->isConst = false; return &node; } - operator const LilvNode**() { + operator const LilvNode **() + { Free(); this->isConst = true; - return (const LilvNode**)&node; + return (const LilvNode **)&node; } AutoLilvNode &operator=(AutoLilvNode &&other) { @@ -83,7 +85,7 @@ namespace pipedal } float AsFloat(float defaultValue = 0); - int AsInt(int defaultValue = 0); + int32_t AsInt(int defaultValue = 0); bool AsBool(bool defaultValue = false); std::string AsUri(); std::string AsString(); @@ -99,4 +101,71 @@ namespace pipedal LilvNode *node = nullptr; bool isConst = true; }; -}; \ No newline at end of file + + class AutoLilvNodes + { + + public: + AutoLilvNodes() + { + } + AutoLilvNodes(const LilvNodes *nodes) + : nodes(const_cast(nodes)) + { + isConst = true; + } + AutoLilvNodes(LilvNodes *nodes) + : nodes(nodes) + { + isConst = false; + } + + ~AutoLilvNodes() { Free(); } + + operator const LilvNodes *() + { + return this->nodes; + } + operator bool() + { + return this->nodes != nullptr; + } + const LilvNodes *Get() { return nodes; } + + AutoLilvNodes &operator=(LilvNodes *node) + { + Free(); + this->nodes = nodes; + return *this; + } + + operator LilvNodes **() + { + Free(); + this->isConst = false; + return &nodes; + } + operator const LilvNodes **() + { + Free(); + this->isConst = true; + return (const LilvNodes **)&nodes; + } + AutoLilvNodes &operator=(AutoLilvNodes &&other) + { + std::swap(this->nodes, other.nodes); + return *this; + } + + void Free() + { + if (nodes != nullptr && !isConst) + lilv_nodes_free(nodes); + nodes = nullptr; + } + + private: + LilvNodes *nodes = nullptr; + bool isConst = true; + }; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bfdbfef..0bcb15f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,9 @@ else() endif() set (PIPEDAL_SOURCES + DbDezipper.hpp DbDezipper.cpp + WebServerLog.hpp + util.hpp util.cpp PiPedalUI.hpp PiPedalUI.cpp diff --git a/src/DbDezipper.cpp b/src/DbDezipper.cpp new file mode 100644 index 0000000..26d44ab --- /dev/null +++ b/src/DbDezipper.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022 Robin E. R. Davies + * All rights reserved. + + * 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. + */ + +#include "DbDezipper.hpp" + +using namespace pipedal; + +static const int SEGMENT_SIZE = 64; + +void DbDezipper::SetSampleRate(double sampleRate) +{ + this->sampleRate = sampleRate; + this->dbPerSegment = 96/rate * SEGMENT_SIZE / sampleRate; +} + +void DbDezipper::SetRate(float seconds) +{ + this->rate = seconds; + this->dbPerSegment = 96/rate * SEGMENT_SIZE / sampleRate; + +} + + +void DbDezipper::SetMinDb(float minDb) +{ + this->minDb = minDb; + if (targetDb < minDb) + { + targetDb = minDb; + count = 0; + } + if (currentDb < minDb) + { + currentDb = minDb; + x = targetX = 0; + dx = 0; + count = 0; + } +} + +void DbDezipper::NextSegment() +{ + if (targetDb == currentDb) + { + x = targetX; + dx = 0; + if (targetDb <= minDb) + { + x = 0; + } + count = -1; + return; + } + else if (targetDb < currentDb) + { + currentDb -= dbPerSegment; + if (currentDb < targetDb) + { + currentDb = targetDb; + } + } + else + { + currentDb += dbPerSegment; + if (currentDb > targetDb) + { + currentDb = targetDb; + } + } + this->targetX = db2a(currentDb); + this->dx = (targetX - x) / SEGMENT_SIZE; + this->count = SEGMENT_SIZE; +} + +void DbDezipper::Reset(float db) +{ + float value; + if (db <= minDb) + { + value = 0; + } else { + value = db2a(db); + } + x = targetX = value; + dx = 0; + currentDb = db; + targetDb = db; + count = -1; +} diff --git a/src/DbDezipper.hpp b/src/DbDezipper.hpp new file mode 100644 index 0000000..57f6ae2 --- /dev/null +++ b/src/DbDezipper.hpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Robin E. R. Davies + * All rights reserved. + + * 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. + */ + +#pragma once + +#include "PiPedalMath.hpp" + +namespace pipedal +{ + class DbDezipper + { + public: + void SetSampleRate(double rate); + void SetRate(float seconds); + void Reset(float db = -96); + void SetMinDb(float db = -96); + + void SetTarget(float db) + { + if (db < minDb) + db = minDb; + if (db != targetDb) + { + targetDb = db; + count = 0; + } + } + + bool IsIdle() const { return count < 0;} + inline float Tick() + { + if (count >= 0) + { + if (count-- <= 0) + { + NextSegment(); + } + float result = x; + x += dx; + + return result; + } + return x; + } + + private: + float minDb = -96; + double sampleRate = 44100; + float rate = 0.1; + float targetDb = -96; + float currentDb = -96; + float targetX = 0; + float x = 0; + float dx = 0; + int32_t count = -1; + float dbPerSegment; + void NextSegment(); + }; +} \ No newline at end of file diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index d247219..f6325a8 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -55,6 +55,7 @@ namespace pipedal void JackInitialize(); // from jack server instance. ~JackConfiguration(); bool isValid() const { return isValid_;} + void isValid(bool value) { isValid_ = value; } bool isOnboarding() const { return isOnboarding_;} void isOnboarding(bool value) { isOnboarding_ = value; } bool isRestarting() const { return isRestarting_; } diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index 5a63047..e0d4768 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -148,7 +148,16 @@ void Lv2Effect::RestoreState(const PedalboardItem&pedalboardItem) // Restore state if present. if (this->stateInterface) { - this->stateInterface->Restore(pedalboardItem.lv2State()); + try { + if (pedalboardItem.lv2State().isValid_) + { + this->stateInterface->Restore(pedalboardItem.lv2State()); + } + } catch (const std::exception &e) + { + std::string name = pedalboardItem.pluginName(); + Lv2Log::warning(SS(name << ": " << e.what())); + } } } @@ -613,7 +622,7 @@ void Lv2Effect::GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) LV2_ATOM_SEQUENCE_FOREACH(controlInput, ev) { - // frame_offset = ev->time.frames; // not really interested. + auto frame_offset = ev->time.frames; // not really interested. if (lv2_atom_forge_is_object_type(&this->outputForgeRt, ev->body.type)) { @@ -649,9 +658,21 @@ void Lv2Effect::GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) bool Lv2Effect::GetLv2State(Lv2PluginState*state) { if (!this->stateInterface) return false; - - *state = this->stateInterface->Save(); - return true; + try { + if (this->stateInterface == nullptr) + { + state->Erase(); + return false; + } + *state = this->stateInterface->Save(); + state->isValid_ = true; + return true; + } + catch (const std::exception&e) + { + state->Erase(); + throw; + } } diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 99ce8fa..e423a1f 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -44,7 +44,6 @@ namespace pipedal class Lv2Effect : public IEffect { private: - std::unique_ptr stateInterface; void RestoreState(const PedalboardItem&pedalboardItem); diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 68f7bdf..717af99 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -210,6 +210,15 @@ void Lv2Pedalboard::Prepare(IHost *pHost, Pedalboard &pedalboard) { this->pHost = pHost; + inputVolume.SetSampleRate((float)(this->pHost->GetSampleRate())); + outputVolume.SetSampleRate((float)(this->pHost->GetSampleRate())); + inputVolume.SetMinDb(-60); + outputVolume.SetMinDb(-60); + + inputVolume.SetTarget(pedalboard.input_volume_db()); + outputVolume.SetTarget(pedalboard.output_volume_db()); + + for (int i = 0; i < pHost->GetNumberOfInputAudioChannels(); ++i) { this->pedalboardInputBuffers.push_back(bufferPool.AllocateBuffer(pHost->GetMaxAudioBufferSize())); @@ -368,21 +377,32 @@ static void Copy(float *input, float *output, uint32_t samples) bool Lv2Pedalboard::Run(float **inputBuffers, float **outputBuffers, uint32_t samples, RealtimeRingBufferWriter*ringBufferWriter) { this->ringBufferWriter = ringBufferWriter; - for (int i = 0; i < this->pedalboardInputBuffers.size(); ++i) + for (size_t i = 0; i < this->pedalboardInputBuffers.size(); ++i) { if (inputBuffers[i] == nullptr) - return false; - Copy(inputBuffers[i], this->pedalboardInputBuffers[i], samples); + { + return false; + } + } + for (size_t i = 0; i < samples; ++i) + { + float volume = this->inputVolume.Tick(); + for (int c = 0; c < this->pedalboardInputBuffers.size(); ++c) + { + this->pedalboardInputBuffers[c][i] = inputBuffers[c][i]*volume; + } } for (int i = 0; i < this->processActions.size(); ++i) { processActions[i](samples); } - for (int i = 0; i < this->pedalboardOutputBuffers.size(); ++i) + for (size_t i = 0; i < samples; ++i) { - if (outputBuffers[i] == nullptr) - return false; - Copy(this->pedalboardOutputBuffers[i], outputBuffers[i], samples); + float volume = outputVolume.Tick(); + for (size_t c = 0; c < this->pedalboardOutputBuffers.size(); ++c) + { + outputBuffers[c][i] = this->pedalboardOutputBuffers[c][i]*volume; + } } return true; } @@ -404,35 +424,59 @@ void Lv2Pedalboard::SetBypass(int effectIndex, bool enabled) effect->SetBypass(enabled); } -void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples) +void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float**inputBuffers, float**outputBuffers) { for (size_t i = 0; i < vuConfiguration->enabledIndexes.size(); ++i) { int index = vuConfiguration->enabledIndexes[i]; VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; + if (index == Pedalboard::INPUT_VOLUME_ID) + { + if (this->pedalboardInputBuffers.size() > 1) + { + GetInputBuffers(); + pUpdate->AccumulateInputs(inputBuffers[0],inputBuffers[1],samples); + pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]),&(this->pedalboardInputBuffers[1][0]),samples); // after outputVolume applied. + } else { + pUpdate->AccumulateInputs(inputBuffers[0],samples); + pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]),samples); // after input volume applied. + } + } else if (index == Pedalboard::OUTPUT_VOLUME_ID) + { + if (this->pedalboardOutputBuffers.size() > 1) + { + pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]),&(this->pedalboardOutputBuffers[1][0]),samples); + pUpdate->AccumulateOutputs(outputBuffers[0],outputBuffers[1],samples); + } else { + pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]),samples); + pUpdate->AccumulateOutputs(outputBuffers[0],samples); + } - auto effect = this->realtimeEffects[index]; + } + else { + auto effect = this->realtimeEffects[index]; - if (effect->GetNumberOfInputAudioPorts() == 1) - { - pUpdate->AccumulateInputs(effect->GetAudioInputBuffer(0), samples); - } - else - { - pUpdate->AccumulateInputs( - effect->GetAudioInputBuffer(0), - effect->GetAudioInputBuffer(1), samples); - } - if (effect->GetNumberOfOutputAudioPorts() == 1) - { - pUpdate->AccumulateOutputs(effect->GetAudioOutputBuffer(0), samples); - } - else - { - pUpdate->AccumulateOutputs( - effect->GetAudioOutputBuffer(0), - effect->GetAudioOutputBuffer(1), - samples); + if (effect->GetNumberOfInputAudioPorts() == 1) + { + pUpdate->AccumulateInputs(effect->GetAudioInputBuffer(0), samples); + } + else + { + pUpdate->AccumulateInputs( + effect->GetAudioInputBuffer(0), + effect->GetAudioInputBuffer(1), samples); + } + if (effect->GetNumberOfOutputAudioPorts() == 1) + { + pUpdate->AccumulateOutputs(effect->GetAudioOutputBuffer(0), samples); + } + else + { + pUpdate->AccumulateOutputs( + effect->GetAudioOutputBuffer(0), + effect->GetAudioOutputBuffer(1), + samples); + } } } } diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index 8ab01e4..fda1555 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -25,6 +25,7 @@ #include #include #include +#include "DbDezipper.hpp" namespace pipedal { @@ -37,6 +38,9 @@ class RealtimeRingBufferWriter; class Lv2Pedalboard { IHost *pHost = nullptr; + DbDezipper inputVolume; + DbDezipper outputVolume; + BufferPool bufferPool; std::vector pedalboardInputBuffers; std::vector pedalboardOutputBuffers; @@ -133,9 +137,11 @@ public: int GetControlIndex(uint64_t instanceId,const std::string&symbol); void SetControlValue(int effectIndex, int portIndex, float value); + void SetInputVolume(float value) { this->inputVolume.SetTarget(value);} + void SetOutputVolume(float value) { this->outputVolume.SetTarget(value);} void SetBypass(int effectIndex,bool enabled); - void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples); + void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float**inputBuffers, float**outputBuffers); float GetControlOutputValue(int effectIndex, int portIndex); diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 93b0b33..9b4ab2f 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -165,19 +165,6 @@ Pedalboard Pedalboard::MakeDefault() // copy insanity. but it happens so rarely. Pedalboard result; - auto split = result.MakeSplit(); - split.topChain().push_back(result.MakeEmptyItem()); - split.bottomChain().push_back(result.MakeEmptyItem()); - - - result.items().push_back(result.MakeEmptyItem()); - result.items().push_back(result.MakeEmptyItem()); - result.items().push_back(result.MakeEmptyItem()); - - result.items().push_back(split); - - result.items().push_back(result.MakeEmptyItem()); - result.items().push_back(result.MakeEmptyItem()); result.items().push_back(result.MakeEmptyItem()); result.name("Default Preset"); @@ -218,6 +205,8 @@ JSON_MAP_END() JSON_MAP_BEGIN(Pedalboard) JSON_MAP_REFERENCE(Pedalboard,name) + JSON_MAP_REFERENCE(Pedalboard,input_volume_db) + JSON_MAP_REFERENCE(Pedalboard,output_volume_db) JSON_MAP_REFERENCE(Pedalboard,items) JSON_MAP_REFERENCE(Pedalboard,nextInstanceId) JSON_MAP_END() diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 631ecdb..79befbf 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -90,7 +90,7 @@ public: ControlValue*GetControlValue(const std::string&symbol); bool hasLv2State() const { - return lv2State_.values_.size() != 0; + return lv2State_.isValid_ != 0; } GETTER_SETTER(instanceId) GETTER_SETTER_REF(uri) @@ -130,11 +130,16 @@ public: class Pedalboard { std::string name_; + float input_volume_db_ = 0; + float output_volume_db_ = 0; + std::vector items_; uint64_t nextInstanceId_ = 0; uint64_t NextInstanceId() { return ++nextInstanceId_; } public: + static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. + static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. bool SetControlValue(long pedalItemId, const std::string &symbol, float value); bool SetItemEnabled(long pedalItemId, bool enabled); @@ -146,6 +151,8 @@ public: GETTER_SETTER_REF(name) GETTER_SETTER_VEC(items) + GETTER_SETTER(input_volume_db) + GETTER_SETTER(output_volume_db) DECLARE_JSON_MAP(Pedalboard); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 052336c..46062a5 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -137,7 +137,7 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) storage.SetConfigRoot(configuration.GetDocRoot()); storage.SetDataRoot(configuration.GetLocalStoragePath()); storage.Initialize(); - lv2Host.SetPluginStoragePath(storage.GetPluginStorageDirectory()); + lv2Host.SetPluginStoragePath(storage.GetPluginAudioFileDirectory()); this->systemMidiBindings = storage.GetSystemMidiBindings(); @@ -233,7 +233,30 @@ void PiPedalModel::Load() this->jackConfiguration = this->jackConfiguration.JackInitialize(); #else this->jackServerSettings = storage.GetJackServerSettings(); - this->jackConfiguration.AlsaInitialize(this->jackServerSettings); + if (jackServerSettings.IsValid()) + { + this->jackConfiguration.AlsaInitialize(this->jackServerSettings); + if (jackServerSettings.IsValid()) + { + for (size_t retry = 0; retry < 5; ++retry) + { + // retry until the device comes online. + sleep(3); + Lv2Log::info("Retrying AlsaInitialize"); + jackConfiguration.isValid(true); + this->jackConfiguration.AlsaInitialize(this->jackServerSettings); + if (jackConfiguration.isValid()) + { + Lv2Log::info("Retry succeeded."); + break; + + } + } + + } + } else { + this->jackConfiguration.AlsaInitialize(this->jackServerSettings); + } #endif if (this->jackConfiguration.isValid()) { @@ -312,9 +335,12 @@ void PiPedalModel::RemoveNotificationSubsription(IPiPedalModelSubscriber *pSubsc } } + + + + void PiPedalModel::PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) { - IEffect *effect = lv2Pedalboard->GetEffect(pedalItemId); if (effect->IsVst3()) { @@ -354,6 +380,67 @@ void PiPedalModel::OnNotifyLv2StateChanged(uint64_t instanceId) } } +void PiPedalModel::SetInputVolume(float value) +{ + PreviewInputVolume(value); + { + std::lock_guard lock(mutex); + + this->pedalboard.input_volume_db(value); + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + { + for (size_t i = 0; i < n; ++i) + { + t[i]->OnInputVolumeChanged(value); + } + } + delete[] t; + + this->SetPresetChanged(-1, true); + } + +} +void PiPedalModel::SetOutputVolume(float value) +{ + PreviewOutputVolume(value); + { + std::lock_guard lock(mutex); + this->pedalboard.output_volume_db(value); + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + { + for (size_t i = 0; i < n; ++i) + { + t[i]->OnOutputVolumeChanged(value); + } + } + delete[] t; + + this->SetPresetChanged(-1, true); + } + +} +void PiPedalModel::PreviewInputVolume(float value) +{ + audioHost->SetInputVolume(value); +} +void PiPedalModel::PreviewOutputVolume(float value) +{ + audioHost->SetOutputVolume(value); + +} + void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) { { @@ -613,17 +700,12 @@ void PiPedalModel::UpdatePluginPresets(const PluginUiPresets &pluginPresets) } int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string &name) { - auto *item = this->pedalboard.GetItem(instanceId); + PedalboardItem *item = this->pedalboard.GetItem(instanceId); if (!item) { throw PiPedalException("Plugin not found."); } - std::map values; - for (const auto &controlValue : item->controlValues()) - { - values[controlValue.key()] = controlValue.value(); - } - uint64_t presetId = storage.SavePluginPreset(item->uri(), name, values); + uint64_t presetId = storage.SavePluginPreset(name, *item); FirePluginPresetsChanged(item->uri()); return presetId; } @@ -1250,7 +1332,7 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() for (int i = 0; i < activeVuSubscriptions.size(); ++i) { auto instanceId = activeVuSubscriptions[i].instanceid; - if (pedalboard.HasItem(instanceId)) + if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) { addedInstances.insert(activeVuSubscriptions[i].instanceid); } @@ -1629,26 +1711,34 @@ void PiPedalModel::LoadPluginPreset(int64_t pluginInstanceId, uint64_t presetIns PedalboardItem *pedalboardItem = this->pedalboard.GetItem(pluginInstanceId); if (pedalboardItem != nullptr) { - std::vector controlValues = storage.GetPluginPresetValues(pedalboardItem->uri(), presetInstanceId); - audioHost->SetPluginPreset(pluginInstanceId, controlValues); + PluginPresetValues presetValues = storage.GetPluginPresetValues(pedalboardItem->uri(), presetInstanceId); + // if the plugin has state, we have to rebuild the pedalboard, since setting state is not thread-safe. - for (size_t i = 0; i < controlValues.size(); ++i) + for (auto&control : presetValues.controls) { - const ControlValue &value = controlValues[i]; - this->pedalboard.SetControlValue(pluginInstanceId, value.key(), value.value()); + this->pedalboard.SetControlValue(pluginInstanceId, control.key(), control.value()); } - IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; - for (size_t i = 0; i < subscribers.size(); ++i) + if (!presetValues.state.isValid_) { - t[i] = this->subscribers[i]; + audioHost->SetPluginPreset(pluginInstanceId, presetValues.controls); + + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + for (size_t i = 0; i < n; ++i) + { + t[i]->OnLoadPluginPreset(pluginInstanceId, presetValues.controls); + } + delete[] t; + } else { + pedalboardItem->lv2State(presetValues.state); + FirePedalboardChanged(-1); // does a complete reload of both client and audio server. } - size_t n = this->subscribers.size(); - for (size_t i = 0; i < n; ++i) - { - t[i]->OnLoadPluginPreset(pluginInstanceId, controlValues); - } - delete[] t; + this->SetPresetChanged(-1, true); } } @@ -1875,3 +1965,25 @@ std::vector PiPedalModel::GetFileList(const UiFileProperty &filePro return std::vector(); // don't disclose to users what the problem is. } } + +void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) +{ + std::lock_guard lock(mutex); + storage.DeleteSampleFile(fileName); +} + + +std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody) +{ + return storage.UploadUserFile(directory,patchProperty,filename,fileBody); +} + +uint64_t PiPedalModel::CreateNewPreset() +{ + std::lock_guard lock(mutex); + + return storage.CreateNewPreset(); +} + + + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 18d9e60..f3db2be 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -52,6 +52,9 @@ namespace pipedal virtual int64_t GetClientId() = 0; virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0; + virtual void OnInputVolumeChanged(float value) = 0; + virtual void OnOutputVolumeChanged(float value) = 0; + virtual void OnLv2StateChanged(int64_t pedalItemId) = 0; virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0; virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0; @@ -215,6 +218,13 @@ namespace pipedal void SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value); void PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value); + void SetInputVolume(float value); + void SetOutputVolume(float value); + void PreviewInputVolume(float value); + void PreviewOutputVolume(float value); + + + void SetPedalboard(int64_t clientId, Pedalboard &pedalboard); void UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard); @@ -310,6 +320,10 @@ namespace pipedal void SetFavorites(const std::map &favorites); std::vector GetFileList(const UiFileProperty&fileProperty); + + void DeleteSampleFile(const std::filesystem::path &fileName); + std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); + uint64_t CreateNewPreset(); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 79e55b3..e321053 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -31,6 +31,7 @@ #include #include "Ipv6Helpers.hpp" #include "Promise.hpp" +#include #include "AdminClient.hpp" #include "WifiConfigSettings.hpp" @@ -418,16 +419,45 @@ private: struct PortMonitorSubscription { + PortMonitorSubscription( + int64_t subscriptionHandle, + int64_t instanceId, + const std::string& key) + :subscriptionHandle(subscriptionHandle), + instanceId(instanceId), + key(key) + { + + } + ~PortMonitorSubscription() { + Close(); + } + PortMonitorSubscription() {} + PortMonitorSubscription(const PortMonitorSubscription&) = delete; + PortMonitorSubscription& operator=(const PortMonitorSubscription&) = delete; + void Close() { + std::lock_guard lock {pmMutex}; + closed = true; + } + + + std::mutex pmMutex; + + bool closed = false; int64_t subscriptionHandle = -1; int64_t instanceId = -1; std::string key; - float lastSentValue = 0; + static constexpr float INVALID_VALUE = std::numeric_limits::max(); + float lastValue = INVALID_VALUE; + float currentValue = INVALID_VALUE; + bool pendingValue = false; bool waitingForAck = false; }; - std::vector activePortMonitors; + std::mutex activePortMonitorsMutex; + std::vector> activePortMonitors; public: virtual int64_t GetClientId() { return clientId; } @@ -436,7 +466,7 @@ public: { for (int i = 0; i < this->activePortMonitors.size(); ++i) { - model.UnmonitorPort(activePortMonitors[i].subscriptionHandle); + model.UnmonitorPort(activePortMonitors[i]->subscriptionHandle); } for (int i = 0; i < this->activeVuSubscriptions.size(); ++i) { @@ -701,22 +731,48 @@ public: Reply(replyTo, "error", what); } - PortMonitorSubscription *getPortMonitorSubscription(uint64_t subscriptionId) + std::shared_ptr getPortMonitorSubscription(uint64_t subscriptionId) { - for (int i = 0; i < this->activePortMonitors.size(); ++i) + std::shared_ptr result; { - auto *portMonitor = &activePortMonitors[i]; - if (portMonitor->subscriptionHandle == subscriptionId) + std::lock_guard lock (activePortMonitorsMutex); + + for (int i = 0; i < this->activePortMonitors.size(); ++i) { - return portMonitor; + if (activePortMonitors[i]->subscriptionHandle == subscriptionId) + { + result = activePortMonitors[i]; + break; + } } } - return nullptr; + return result; } - void SendMonitorPortMessage(PortMonitorSubscription *subscription, float value) + void SendMonitorPortMessage(std::shared_ptr &subscription, float value) { + // running on RT_output thread, or on Socket thread. + { + std::lock_guard lock {subscription->pmMutex}; + if (subscription->closed) return; + if (value == subscription->currentValue) + return; + if (subscription->waitingForAck) + { + subscription->pendingValue = true; + subscription->currentValue = value; + return; + } + subscription->currentValue = value; + subscription->lastValue = value; + subscription->waitingForAck = true; + } + SendMonitorPortMessage_Inner(subscription,value); + } + // post-sync send. + void SendMonitorPortMessage_Inner(std::shared_ptr &subscription, float value) + { auto subscriptionHandle_ = subscription->subscriptionHandle; MonitorResultBody body; body.subscriptionHandle_ = subscriptionHandle_; @@ -726,17 +782,30 @@ public: body, [this, subscriptionHandle_](const bool &result) { - PortMonitorSubscription *subscription = getPortMonitorSubscription(subscriptionHandle_); - if (subscription == nullptr) return; + // running on PiPedalSocket thread. + std::shared_ptr subscription = getPortMonitorSubscription(subscriptionHandle_); + if (!subscription) return; - if (subscription->pendingValue) + float value; { - subscription->pendingValue = false; - SendMonitorPortMessage(subscription, subscription->lastSentValue); - } - else - { - subscription->waitingForAck = false; + std::unique_lock lock {subscription->pmMutex}; + if (subscription->closed) return; + if (subscription->pendingValue) + { + value = subscription->currentValue; + subscription->lastValue = value; + subscription->pendingValue = false; + subscription->waitingForAck = true; + + lock.unlock(); + + SendMonitorPortMessage_Inner(subscription, value); + return; + } + else + { + subscription->waitingForAck = false; + } } }, [](const std::exception &e) @@ -753,31 +822,19 @@ public: body.updateRate_, [this](int64_t subscriptionHandle_, float value) { - PortMonitorSubscription *subscription = getPortMonitorSubscription(subscriptionHandle_); + std::shared_ptr subscription = getPortMonitorSubscription(subscriptionHandle_); if (subscription) { - if (subscription->waitingForAck) - { - if (subscription->lastSentValue != value) - { - subscription->lastSentValue = value; - subscription->pendingValue = true; - } - } - else - { - subscription->lastSentValue = value; - - subscription->waitingForAck = true; - SendMonitorPortMessage(subscription, value); - } + SendMonitorPortMessage(subscription, value); } } ); - activePortMonitors.push_back( - PortMonitorSubscription{subscriptionHandle, body.instanceId_, body.key_}); + { + std::lock_guard lock(activePortMonitorsMutex); + activePortMonitors.push_back(std::make_shared(subscriptionHandle, body.instanceId_, body.key_)); + } this->Reply(replyTo, "monitorPort", subscriptionHandle); } @@ -831,7 +888,37 @@ public: ControlChangedBody message; pReader->read(&message); this->model.PreviewControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); + } else if (message == "setControl") + { + ControlChangedBody message; + pReader->read(&message); + this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); + } + else if (message == "setInputVolume") + { + float value; + pReader->read(&value); + this->model.SetInputVolume(value); } + else if (message == "setOutputVolume") + { + float value; + pReader->read(&value); + this->model.SetOutputVolume(value); + } + else if (message == "previewInputVolume") + { + float value; + pReader->read(&value); + this->model.PreviewInputVolume(value); + } + else if (message == "previewOutputVolume") + { + float value; + pReader->read(&value); + this->model.PreviewOutputVolume(value); + } + else if (message == "listenForMidiEvent") { ListenForMidiEventBody body; @@ -1233,11 +1320,14 @@ public: pReader->read(&subscriptionHandle); { { - std::lock_guard guard(subscriptionMutex); + std::lock_guard guard(activePortMonitorsMutex); for (auto i = this->activePortMonitors.begin(); i != this->activePortMonitors.end(); ++i) { - if ((*i).subscriptionHandle == subscriptionHandle) + if ((*i)->subscriptionHandle == subscriptionHandle) { + auto subscription = (*i); + subscription->Close(); + this->activePortMonitors.erase(i); break; } @@ -1311,6 +1401,19 @@ public: pReader->read(&fileProperty); std::vector list = this->model.GetFileList(fileProperty); this->Reply(replyTo,"requestFileList",list); + } + else if (message == "newPreset") + { + int64_t presetId = this->model.CreateNewPreset(); + this->Reply(replyTo,"newPreset",presetId); + } + else if (message == "deleteUserFile") + { + std::string fileName; + pReader->read(&fileName); + + this->model.DeleteSampleFile(fileName); + this->Reply(replyTo,"deleteUserFile",true); } else if (message == "setOnboarding") { @@ -1518,6 +1621,16 @@ private: body.value_ = value; Send("onControlChanged", body); } + virtual void OnInputVolumeChanged(float value) + { + ControlChangedBody body; + Send("onInputVolumeChanged", value); + } + virtual void OnOutputVolumeChanged(float value) + { + ControlChangedBody body; + Send("onOutputVolumeChanged", value); + } class DeferredValue { diff --git a/src/PiPedalUI.cpp b/src/PiPedalUI.cpp index 5a53093..b54f11f 100644 --- a/src/PiPedalUI.cpp +++ b/src/PiPedalUI.cpp @@ -39,7 +39,7 @@ PiPedalUI::PiPedalUI(PluginHost *pHost, const LilvNode *uiNode, const std::files try { UiFileProperty::ptr fileUI = std::make_shared(pHost, fileNode, resourcePath); - this->fileProperites_.push_back(std::move(fileUI)); + this->fileProperties_.push_back(std::move(fileUI)); } catch (const std::exception &e) { @@ -69,18 +69,18 @@ PiPedalFileType::PiPedalFileType(PluginHost *pHost, const LilvNode *node) { auto pWorld = pHost->getWorld(); - AutoLilvNode name = lilv_world_get( + AutoLilvNode label = lilv_world_get( pWorld, node, - pHost->lilvUris.lv2Core__name, + pHost->lilvUris.rdfs__label, nullptr); - if (name) + if (label) { - this->name_ = name.AsString(); + this->label_ = label.AsString(); } else { - throw std::logic_error("pipedal_ui:fileType is missing name property."); + throw std::logic_error("pipedal_ui:fileType is missing label property."); } AutoLilvNode fileExtension = lilv_world_get( pWorld, @@ -103,7 +103,7 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st AutoLilvNode name = lilv_world_get( pWorld, node, - pHost->lilvUris.lv2Core__name, + pHost->lilvUris.lv2core__name, nullptr); if (name) { @@ -113,6 +113,19 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st { this->name_ = "File"; } + AutoLilvNode index = lilv_world_get( + pWorld, + node, + pHost->lilvUris.lv2core__index, + nullptr); + if (index) + { + this->index_ = name.AsInt(); + } + else + { + this->index_ = -1; + } AutoLilvNode directory = lilv_world_get( pWorld, @@ -192,11 +205,31 @@ bool UiFileProperty::IsDirectoryNameValid(const std::string &value) { if (value.length() == 0) return false; - return IsAlphaNumeric(value); + if (value.find_first_of('/') != std::string::npos) + { + return false; + } + if (value.find_first_of('\\') != std::string::npos) + { + return false; + } + if (value.find_first_of("::") != std::string::npos) + { + return false; + } + if (value.find_first_of(":") != std::string::npos) + { + return false; + } + return true; } bool UiFileProperty::IsValidExtension(const std::string &extension) const { + if (fileTypes_.size() == 0) + { + return true; + } for (auto &fileType : fileTypes_) { @@ -221,39 +254,57 @@ UiPortNotification::UiPortNotification(PluginHost *pHost, const LilvNode *node) // ] LilvWorld *pWorld = pHost->getWorld(); - AutoLilvNode portIndex = lilv_world_get(pWorld,node,pHost->lilvUris.ui__portIndex,nullptr); + AutoLilvNode portIndex = lilv_world_get(pWorld, node, pHost->lilvUris.ui__portIndex, nullptr); if (!portIndex) { this->portIndex_ = -1; - } else { + } + else + { this->portIndex_ = (uint32_t)lilv_node_as_int(portIndex); } - AutoLilvNode symbol = lilv_world_get(pWorld,node,pHost->lilvUris.lv2__symbol,nullptr); + AutoLilvNode symbol = lilv_world_get(pWorld, node, pHost->lilvUris.lv2__symbol, nullptr); if (!symbol) { this->symbol_ = ""; - } else { + } + else + { this->symbol_ = symbol.AsString(); } - AutoLilvNode plugin = lilv_world_get(pWorld,node,pHost->lilvUris.ui__plugin,nullptr); + AutoLilvNode plugin = lilv_world_get(pWorld, node, pHost->lilvUris.ui__plugin, nullptr); if (!plugin) { this->plugin_ = ""; - } else { + } + else + { this->plugin_ = plugin.AsUri(); } - AutoLilvNode protocol = lilv_world_get(pWorld,node,pHost->lilvUris.ui__protocol,nullptr); + AutoLilvNode protocol = lilv_world_get(pWorld, node, pHost->lilvUris.ui__protocol, nullptr); if (!protocol) { this->protocol_ = ""; - } else { + } + else + { this->protocol_ = protocol.AsUri(); } - if (this->portIndex_ == -1 &&this->symbol_ == "") + if (this->portIndex_ == -1 && this->symbol_ == "") { pHost->LogWarning("ui:portNotification specifies neither a ui:portIndex nor an lv2:symbol."); } +} +UiFileProperty::UiFileProperty(const std::string &name, const std::string &patchProperty, const std::string &directory) + : name_(name), + patchProperty_(patchProperty), + directory_(directory) +{ +} +PiPedalUI::PiPedalUI(std::vector &&fileProperties) +{ + this->fileProperties_ = std::move(fileProperties); } JSON_MAP_BEGIN(UiPortNotification) @@ -264,12 +315,13 @@ JSON_MAP_REFERENCE(UiPortNotification, protocol) JSON_MAP_END() JSON_MAP_BEGIN(PiPedalFileType) -JSON_MAP_REFERENCE(PiPedalFileType, name) +JSON_MAP_REFERENCE(PiPedalFileType, label) JSON_MAP_REFERENCE(PiPedalFileType, fileExtension) JSON_MAP_END() JSON_MAP_BEGIN(UiFileProperty) JSON_MAP_REFERENCE(UiFileProperty, name) +JSON_MAP_REFERENCE(UiFileProperty, index) JSON_MAP_REFERENCE(UiFileProperty, directory) JSON_MAP_REFERENCE(UiFileProperty, fileTypes) JSON_MAP_REFERENCE(UiFileProperty, patchProperty) diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index be16944..66718a5 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -57,7 +57,7 @@ namespace pipedal { class PiPedalFileType { private: - std::string name_; + std::string label_; std::string fileExtension_; public: PiPedalFileType() { } @@ -65,7 +65,7 @@ namespace pipedal { static std::vector GetArray(PluginHost*pHost, const LilvNode*node,const LilvNode*uri); - const std::string& name() const { return name_;} + const std::string& label() const { return label_;} const std::string &fileExtension() const { return fileExtension_; } public: @@ -94,6 +94,7 @@ namespace pipedal { class UiFileProperty { private: std::string name_; + std::int64_t index_ = -1; std::string directory_; std::vector fileTypes_; std::string patchProperty_; @@ -101,9 +102,11 @@ namespace pipedal { using ptr = std::shared_ptr; UiFileProperty() { } UiFileProperty(PluginHost*pHost, const LilvNode*node, const std::filesystem::path&resourcePath); + UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); const std::string &name() const { return name_; } + int64_t index() const { return index_; } const std::string &directory() const { return directory_; } const std::vector &fileTypes() const { return fileTypes_; } @@ -120,9 +123,12 @@ namespace pipedal { public: using ptr = std::shared_ptr; PiPedalUI(PluginHost*pHost, const LilvNode*uiNode, const std::filesystem::path&resourcePath); + + PiPedalUI(std::vector &&fileProperites); + const std::vector& fileProperties() const { - return fileProperites_; + return fileProperties_; } const std::vector &portNotifications() const { return portNotifications_; } @@ -140,7 +146,7 @@ namespace pipedal { } private: - std::vector fileProperites_; + std::vector fileProperties_; std::vector portNotifications_; }; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 21244c0..10b489d 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -66,26 +66,31 @@ using namespace pipedal; namespace pipedal { - static const char *LV2_AUDIO_PORT = P_LV2_CORE_URI "AudioPort"; - static const char *LV2_PLUGIN = P_LV2_CORE_URI "Plugin"; - static const char *LV2_CONTROL_PORT = P_LV2_CORE_URI "ControlPort"; - static const char *LV2_INPUT_PORT = P_LV2_CORE_URI "InputPort"; - static const char *LV2_OUTPUT_PORT = P_LV2_CORE_URI "OutputPort"; - static const char *LV2_INTEGER = P_LV2_CORE_URI "integer"; - static const char *LV2_ENUMERATION = P_LV2_CORE_URI "enumeration"; - static const char *LV2_PORT_LOGARITHMIC = P_LV2_PPROPS "logarithmic"; - static const char *LV2_PORT_RANGE_STEPS = P_LV2_PPROPS "rangeSteps"; - static const char *LV2_PORT_TRIGGER = P_LV2_PPROPS "trigger"; - static const char *LV2_PORT_DISPLAY_PRIORITY = P_LV2_PPROPS "displayPriority"; + + // in ttl files, but not header files. + static const char *LV2_MIDI_PLUGIN = "http://lv2plug.in/ns/lv2core#MIDIPlugin"; - static const char *LV2_ATOM_PORT = "http://lv2plug.in/ns/ext/atom#AtomPort"; - static const char *LV2_ATOM_SEQUENCE = "http://lv2plug.in/ns/ext/atom#Sequence"; - static const char *LV2_ATOM_SOUND = "http://lv2plug.in/ns/ext/atom#Sound"; - static const char *LV2_ATOM_VECTOR = "http://lv2plug.in/ns/ext/atom#Vector"; - static const char *LV2_ATOM_STRING = "http://lv2plug.in/ns/ext/atom#String"; - static const char *LV2_MIDI_EVENT = "http://lv2plug.in/ns/ext/midi#MidiEvent"; - static const char *LV2_DESIGNATION = "http://lv2plug.in/ns/lv2core#Designation"; + // static const char *LV2_AUDIO_PORT = P_LV2_CORE_URI "AudioPort"; + // static const char *LV2_PLUGIN = P_LV2_CORE_URI "Plugin"; + // static const char *LV2_CONTROL_PORT = P_LV2_CORE_URI "ControlPort"; + // static const char *LV2_INPUT_PORT = P_LV2_CORE_URI "InputPort"; + // static const char *LV2_OUTPUT_PORT = P_LV2_CORE_URI "OutputPort"; + // static const char *LV2_INTEGER = P_LV2_CORE_URI "integer"; + // static const char *LV2_ENUMERATION = P_LV2_CORE_URI "enumeration"; + // static const char *LV2_PORT_LOGARITHMIC = P_LV2_PPROPS "logarithmic"; + // static const char *LV2_PORT_RANGE_STEPS = P_LV2_PPROPS "rangeSteps"; + // static const char *LV2_PORT_TRIGGER = P_LV2_PPROPS "trigger"; + // static const char *LV2_PORT_DISPLAY_PRIORITY = P_LV2_PPROPS "displayPriority"; + + + // static const char *LV2_ATOM_PORT = "http://lv2plug.in/ns/ext/atom#AtomPort"; + // static const char *LV2_ATOM_SEQUENCE = "http://lv2plug.in/ns/ext/atom#Sequence"; + // static const char *LV2_ATOM_SOUND = "http://lv2plug.in/ns/ext/atom#Sound"; + // static const char *LV2_ATOM_VECTOR = "http://lv2plug.in/ns/ext/atom#Vector"; + // static const char *LV2_ATOM_STRING = "http://lv2plug.in/ns/ext/atom#String"; + // static const char *LV2_MIDI_EVENT = "http://lv2plug.in/ns/ext/midi#MidiEvent"; + // static const char *LV2_DESIGNATION = "http://lv2plug.in/ns/lv2core#Designation"; class PluginHost::Urids { @@ -117,25 +122,35 @@ void PluginHost::SetConfiguration(const PiPedalConfiguration &configuration) void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) { - rdfsComment = lilv_new_uri(pWorld, PluginHost::RDFS_COMMENT_URI); - logarithic_uri = lilv_new_uri(pWorld, LV2_PORT_LOGARITHMIC); - display_priority_uri = lilv_new_uri(pWorld, LV2_PORT_DISPLAY_PRIORITY); - range_steps_uri = lilv_new_uri(pWorld, LV2_PORT_RANGE_STEPS); - integer_property_uri = lilv_new_uri(pWorld, LV2_INTEGER); - enumeration_property_uri = lilv_new_uri(pWorld, LV2_ENUMERATION); - toggle_property_uri = lilv_new_uri(pWorld, LV2_CORE__toggled); - not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); - midiEventNode = lilv_new_uri(pWorld, LV2_MIDI_EVENT); - designationNode = lilv_new_uri(pWorld, LV2_DESIGNATION); - portGroupUri = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); - unitsUri = lilv_new_uri(pWorld, LV2_UNITS__unit); - bufferType_uri = lilv_new_uri(pWorld, LV2_ATOM__bufferType); - pset_Preset = lilv_new_uri(pWorld, LV2_PRESETS__Preset); - rdfs_label = lilv_new_uri(pWorld, LILV_NS_RDFS "label"); - symbolUri = lilv_new_uri(pWorld, LV2_CORE__symbol); - nameUri = lilv_new_uri(pWorld, LV2_CORE__name); + rdfs__Comment = lilv_new_uri(pWorld, PluginHost::RDFS__comment); + rdfs__range = lilv_new_uri(pWorld,PluginHost::RDFS__range); + port_logarithmic = lilv_new_uri(pWorld, LV2_PORT_PROPS__logarithmic); + port__display_priority = lilv_new_uri(pWorld, LV2_PORT_PROPS__displayPriority); + port_range_steps = lilv_new_uri(pWorld, LV2_PORT_PROPS__rangeSteps); + integer_property_uri = lilv_new_uri(pWorld, LV2_CORE__integer); + enumeration_property_uri = lilv_new_uri(pWorld, LV2_CORE__enumeration); + core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); + core__connectionOptional = lilv_new_uri(pWorld,LV2_CORE__connectionOptional); + portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); + midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); + 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); + + 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. + + atom__bufferType = lilv_new_uri(pWorld, LV2_ATOM__bufferType); + atom__Path = lilv_new_uri(pWorld, LV2_ATOM__Path); + presets__preset = lilv_new_uri(pWorld, LV2_PRESETS__Preset); + rdfs__label = lilv_new_uri(pWorld, LILV_NS_RDFS "label"); + + lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol); + lv2core__name = lilv_new_uri(pWorld, LV2_CORE__name); + lv2core__index = lilv_new_uri(pWorld, LV2_CORE__index); + lv2core__Parameter = lilv_new_uri(pWorld,LV2_CORE_PREFIX "Parameter"); + - lv2Core__name = lilv_new_uri(pWorld, LV2_CORE__name); pipedalUI__ui = lilv_new_uri(pWorld, PIPEDAL_UI__ui); pipedalUI__fileProperties = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperties); pipedalUI__patchProperty = lilv_new_uri(pWorld, PIPEDAL_UI__patchProperty); @@ -173,27 +188,30 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) time_speed = lilv_new_uri(pWorld, LV2_TIME__speed); appliesTo = lilv_new_uri(pWorld, LV2_CORE__appliesTo); + + patch__writable = lilv_new_uri(pWorld,LV2_PATCH__writable); + patch__readable = lilv_new_uri(pWorld,LV2_PATCH__readable); } void PluginHost::LilvUris::Free() { - rdfsComment.Free(); - logarithic_uri.Free(); - display_priority_uri.Free(); - range_steps_uri.Free(); + rdfs__Comment.Free(); + port_logarithmic.Free(); + port__display_priority.Free(); + port_range_steps.Free(); integer_property_uri.Free(); enumeration_property_uri.Free(); - toggle_property_uri.Free(); - not_on_gui_property_uri.Free(); - midiEventNode.Free(); - designationNode.Free(); - portGroupUri.Free(); - unitsUri.Free(); - bufferType_uri.Free(); - pset_Preset.Free(); - rdfs_label.Free(); - symbolUri.Free(); - nameUri.Free(); + core__toggled.Free(); + portprops__not_on_gui_property_uri.Free(); + midi__event.Free(); + core__designation.Free(); + portgroups__group.Free(); + units__unit.Free(); + atom__bufferType.Free(); + presets__preset.Free(); + rdfs__label.Free(); + lv2core__symbol.Free(); + lv2core__name.Free(); time_Position.Free(); time_barBeat.Free(); @@ -574,13 +592,18 @@ static std::vector nodeAsStringArray(const LilvNodes *nodes) return result; } -const char *PluginHost::RDFS_COMMENT_URI = "http://www.w3.org/2000/01/rdf-schema#" +const char *PluginHost::RDFS__comment = "http://www.w3.org/2000/01/rdf-schema#" "comment"; +const char *PluginHost::RDFS__range = "http://www.w3.org/2000/01/rdf-schema#" + "range"; + + + LilvNode *PluginHost::get_comment(const std::string &uri) { AutoLilvNode uriNode = lilv_new_uri(pWorld, uri.c_str()); - LilvNode *result = lilv_world_get(pWorld, uriNode, lilvUris.rdfsComment, nullptr); + LilvNode *result = lilv_world_get(pWorld, uriNode, lilvUris.rdfs__Comment, nullptr); return result; } @@ -591,7 +614,7 @@ static bool ports_sort_compare(std::shared_ptr &p1, const std::shar bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin) { - NodesAutoFree nodes = lilv_plugin_get_related(plugin, lv2Host->lilvUris.pset_Preset); + NodesAutoFree nodes = lilv_plugin_get_related(plugin, lv2Host->lilvUris.presets__preset); bool result = false; LILV_FOREACH(nodes, iNode, nodes) { @@ -601,9 +624,65 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu return result; } +std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host,const LilvPlugin*pPlugin) +{ + // example: + + // + // a lv2:Parameter; + // rdfs:label "Model"; + // rdfs:range atom:Path. + // ... + // patch:writable ; + + LilvWorld* pWorld = lv2Host->getWorld(); + AutoLilvNode pluginUri = lilv_plugin_get_uri(pPlugin); + AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld,pluginUri,lv2Host->lilvUris.patch__writable,nullptr); + + std::vector fileProperties; + + LILV_FOREACH(nodes, iNode, patchWritables) + { + AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode); + if (propertyUri) + { + // isA lv2:Parameter? + if (lilv_world_ask(pWorld,propertyUri,lv2Host->lilvUris.isA,lv2Host->lilvUris.lv2core__Parameter)) + { + // rfs:range atom:Path? + if (lilv_world_ask(pWorld,propertyUri,lv2Host->lilvUris.rdfs__range,lv2Host->lilvUris.atom__Path)) + { + AutoLilvNode label = lilv_world_get(pWorld,propertyUri,lv2Host->lilvUris.rdfs__label,nullptr); + std::string strLabel = label.AsString(); + if (strLabel.length() != 0) + { + std::filesystem::path path = this->bundle_path(); + path = path.parent_path(); + std::string lv2DirectoryName = path.filename().string(); + // we have a valid path property! + fileProperties.push_back( + std::make_shared( + strLabel,propertyUri.AsUri(),lv2DirectoryName) + ); + + } + } + + } + } + + } + if (fileProperties.size() != 0) + { + return std::make_shared(std::move(fileProperties)); + } + + return std::shared_ptr(); + +} + Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvPlugin *pPlugin) { - AutoLilvNode bundleUriNode = lilv_plugin_get_bundle_uri(pPlugin); if (!bundleUriNode) throw std::logic_error("Invalid bundle uri."); @@ -698,15 +777,39 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP if (pipedalUINode) { this->piPedalUI_ = std::make_shared(lv2Host, pipedalUINode, std::filesystem::path(bundlePath)); + } else { + // look for + this->piPedalUI_ = FindWritablePathProperties(lv2Host,pPlugin); + } + + int nInputs = 0; + for (size_t i = 0; i < ports_.size(); ++i) + { + auto port = ports_[i]; + if (port->is_audio_port() && port->is_input()) + { + if (nInputs >= 2 && !port->connection_optional()) + { + isValid = false; + break; + } + ++nInputs; + } + } + int nOutputs = 0; + for (size_t i = 0; i < ports_.size(); ++i) + { + auto port = ports_[i]; + if (port->is_audio_port() && port->is_output()) + { + if (nOutputs >= 2 && !port->connection_optional()) + { + isValid = false; + break; + } + ++nOutputs; + } } - // xxx lilv_world_get(pWorld,pluginUri,); - // for (auto&portInfo: ports_) - // { - // if (portInfo->is_control_port() && portInfo->is_output()) - // { - // std::cout << "Dbg: " << "Has an output control port. " << this->uri_ << std::endl; - // } - // } this->is_valid_ = isValid; } @@ -724,9 +827,11 @@ std::vector supportedFeatures = { LV2_STATE__loadDefaultState, LV2_STATE__makePath, LV2_STATE__mapPath, + LV2_CORE__inPlaceBroken, // UI features that we can ignore, since we won't load their ui. "http://lv2plug.in/ns/extensions/ui#makeResident", + "http://lv2plug.in/ns/ext/port-props#supportsStrictBounds" }; @@ -798,9 +903,12 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP if (default_value_ < min_value_) default_value_ = min_value_; - this->is_logarithmic_ = lilv_port_has_property(plugin, pPort, host->lilvUris.logarithic_uri); + this->is_logarithmic_ = lilv_port_has_property(plugin, pPort, host->lilvUris.port_logarithmic); - NodesAutoFree priority_nodes = lilv_port_get_value(plugin, pPort, host->lilvUris.display_priority_uri); + // typo in invada plugins. + this->is_logarithmic_ |= lilv_port_has_property(plugin, pPort, host->lilvUris.invada_portprops__logarithmic); + + NodesAutoFree priority_nodes = lilv_port_get_value(plugin, pPort, host->lilvUris.port__display_priority); this->display_priority_ = -1; if (priority_nodes) @@ -812,7 +920,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP } } - NodesAutoFree range_steps_nodes = lilv_port_get_value(plugin, pPort, host->lilvUris.range_steps_uri); + NodesAutoFree range_steps_nodes = lilv_port_get_value(plugin, pPort, host->lilvUris.port_range_steps); this->range_steps_ = 0; if (range_steps_nodes) { @@ -826,9 +934,9 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris.enumeration_property_uri); - this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris.toggle_property_uri); - - this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris.not_on_gui_property_uri); + this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris.core__toggled); + this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris.portprops__not_on_gui_property_uri); + this->connection_optional_ = lilv_port_has_property(plugin,pPort,host->lilvUris.core__connectionOptional); LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort); LILV_FOREACH(scale_points, iSP, pScalePoints) @@ -844,30 +952,50 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP std::sort(scale_points_.begin(), scale_points_.end(), scale_points_sort_compare); - is_input_ = is_a(host, LV2_INPUT_PORT); - is_output_ = is_a(host, LV2_OUTPUT_PORT); + is_input_ = is_a(host, LV2_CORE__InputPort); + is_output_ = is_a(host, LV2_CORE__OutputPort); is_control_port_ = is_a(host, LV2_CORE__ControlPort); is_audio_port_ = is_a(host, LV2_CORE__AudioPort); - is_atom_port_ = is_a(host, LV2_ATOM_PORT); + is_atom_port_ = is_a(host, LV2_ATOM__AtomPort); is_cv_port_ = is_a(host, LV2_CORE__CVPort); - supports_midi_ = lilv_port_supports_event(plugin, pPort, host->lilvUris.midiEventNode); + supports_midi_ = lilv_port_supports_event(plugin, pPort, host->lilvUris.midi__event); supports_time_position_ = lilv_port_supports_event(plugin, pPort, host->lilvUris.time_Position); - AutoLilvNode designationValue = lilv_port_get(plugin, pPort, host->lilvUris.designationNode); + AutoLilvNode designationValue = lilv_port_get(plugin, pPort, host->lilvUris.core__designation); designation_ = nodeAsString(designationValue); - AutoLilvNode portGroup_value = lilv_port_get(plugin, pPort, host->lilvUris.portGroupUri); + AutoLilvNode portGroup_value = lilv_port_get(plugin, pPort, host->lilvUris.portgroups__group); port_group_ = nodeAsString(portGroup_value); - AutoLilvNode unitsValueUri = lilv_port_get(plugin, pPort, host->lilvUris.unitsUri); - this->units_ = UriToUnits(nodeAsString(unitsValueUri)); + AutoLilvNode unitsValueUri = lilv_port_get(plugin, pPort, host->lilvUris.units__unit); + if (unitsValueUri) + { + this->units_ = UriToUnits(nodeAsString(unitsValueUri)); + } else { + // invada plugins use the wrong URI. + AutoLilvNode invadaUnitsValueUri = lilv_port_get(plugin, pPort, host->lilvUris.invada_units__unit); + if (invadaUnitsValueUri) + { + std::string uri = nodeAsString(invadaUnitsValueUri); + static const char*INCORRECT_URI = "http://lv2plug.in/ns/extension/units#"; + static const char*CORRECT_URI = "http://lv2plug.in/ns/extensions/units#"; + if (uri.starts_with(INCORRECT_URI)) + { + uri = uri.replace(0,strlen(INCORRECT_URI),CORRECT_URI); + } + this->units_ = UriToUnits(uri); + } else { + this->units(Units::none); + } - AutoLilvNode commentNode = lilv_port_get(plugin, pPort, host->lilvUris.rdfsComment); + } + + AutoLilvNode commentNode = lilv_port_get(plugin, pPort, host->lilvUris.rdfs__Comment); this->comment_ = nodeAsString(commentNode); - AutoLilvNode bufferType = lilv_port_get(plugin, pPort, host->lilvUris.bufferType_uri); + AutoLilvNode bufferType = lilv_port_get(plugin, pPort, host->lilvUris.atom__bufferType); this->buffer_type_ = ""; if (bufferType) @@ -962,11 +1090,12 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) } for (auto port : plugin->ports()) { + if (port->is_input()) { - if (port->is_control_port() && port->is_input()) + if (port->is_control_port()) { - controls_.push_back(Lv2PluginUiControlPort(plugin, port.get())); + controls_.push_back(Lv2PluginUiPort(plugin, port.get())); } else if (port->is_atom_port()) { @@ -982,7 +1111,11 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) } else if (port->is_output()) { - if (port->is_atom_port() && port->supports_midi()) + if (port->is_control_port()) + { + controls_.push_back(Lv2PluginUiPort(plugin, port.get())); + } + else if (port->is_atom_port() && port->supports_midi()) { this->has_midi_output_ = true; } @@ -1007,7 +1140,7 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) std::shared_ptr PluginHost::GetLv2PluginClass() const { - return this->GetPluginClass(LV2_PLUGIN); + return this->GetPluginClass(LV2_CORE__Plugin); } std::shared_ptr PluginHost::GetPluginInfo(const std::string &uri) const @@ -1078,7 +1211,7 @@ std::vector PluginHost::LoadFactoryPluginPreset( throw PiPedalStateException("No such plugin."); } - LilvNodes *presets = lilv_plugin_get_related(plugin, lilvUris.pset_Preset); + LilvNodes *presets = lilv_plugin_get_related(plugin, lilvUris.presets__preset); LILV_FOREACH(nodes, i, presets) { const LilvNode *preset = lilv_nodes_get(presets, i); @@ -1154,7 +1287,7 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri) PluginPresets result; result.pluginUri_ = pluginUri; - LilvNodes *presets = lilv_plugin_get_related(plugin, lilvUris.pset_Preset); + LilvNodes *presets = lilv_plugin_get_related(plugin, lilvUris.presets__preset); LILV_FOREACH(nodes, i, presets) { const LilvNode *preset = lilv_nodes_get(presets, i); @@ -1171,7 +1304,7 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri) if (!cbData.failed) { - result.presets_.push_back(PluginPreset(result.nextInstanceId_++, std::move(label), std::move(controlValues))); + result.presets_.push_back(PluginPreset(result.nextInstanceId_++, label, controlValues,Lv2PluginState())); } } } @@ -1216,9 +1349,9 @@ Lv2PortGroup::Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri) this->uri_ = groupUri; AutoLilvNode uri = lilv_new_uri(pWorld, groupUri.c_str()); - LilvNode *symbolNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris.symbolUri, nullptr); + LilvNode *symbolNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris.lv2core__symbol, nullptr); symbol_ = nodeAsString(symbolNode); - LilvNode *nameNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris.nameUri, nullptr); + LilvNode *nameNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris.lv2core__name, nullptr); name_ = nodeAsString(nameNode); } @@ -1269,6 +1402,7 @@ json_map::storage_type Lv2PortInfo::jmap{ json_map::reference("is_audio_port", &Lv2PortInfo::is_audio_port_), json_map::reference("is_atom_port", &Lv2PortInfo::is_audio_port_), json_map::reference("is_cv_port", &Lv2PortInfo::is_cv_port_), + json_map::reference("connection_optional", &Lv2PortInfo::connection_optional_), json_map::reference("is_valid", &Lv2PortInfo::is_valid_), @@ -1330,29 +1464,31 @@ json_map::storage_type Lv2PluginUiPortGroup::jmap{{ MAP_REF(Lv2PluginUiPortGroup, program_list_id), }}; -json_map::storage_type Lv2PluginUiControlPort::jmap{{ - json_map::reference("symbol", &Lv2PluginUiControlPort::symbol_), - json_map::reference("name", &Lv2PluginUiControlPort::name_), - MAP_REF(Lv2PluginUiControlPort, index), - MAP_REF(Lv2PluginUiControlPort, min_value), - MAP_REF(Lv2PluginUiControlPort, max_value), - MAP_REF(Lv2PluginUiControlPort, default_value), - MAP_REF(Lv2PluginUiControlPort, is_logarithmic), - MAP_REF(Lv2PluginUiControlPort, display_priority), +json_map::storage_type Lv2PluginUiPort::jmap{{ + json_map::reference("symbol", &Lv2PluginUiPort::symbol_), + json_map::reference("name", &Lv2PluginUiPort::name_), + MAP_REF(Lv2PluginUiPort, index), + MAP_REF(Lv2PluginUiPort, is_input), + MAP_REF(Lv2PluginUiPort, min_value), + MAP_REF(Lv2PluginUiPort, max_value), + MAP_REF(Lv2PluginUiPort, default_value), + MAP_REF(Lv2PluginUiPort, is_logarithmic), + MAP_REF(Lv2PluginUiPort, display_priority), - MAP_REF(Lv2PluginUiControlPort, range_steps), - MAP_REF(Lv2PluginUiControlPort, integer_property), - MAP_REF(Lv2PluginUiControlPort, enumeration_property), - MAP_REF(Lv2PluginUiControlPort, not_on_gui), - MAP_REF(Lv2PluginUiControlPort, toggled_property), - MAP_REF(Lv2PluginUiControlPort, scale_points), - MAP_REF(Lv2PluginUiControlPort, port_group), + MAP_REF(Lv2PluginUiPort, range_steps), + MAP_REF(Lv2PluginUiPort, integer_property), + MAP_REF(Lv2PluginUiPort, enumeration_property), + MAP_REF(Lv2PluginUiPort, not_on_gui), + MAP_REF(Lv2PluginUiPort, toggled_property), + MAP_REF(Lv2PluginUiPort, scale_points), + MAP_REF(Lv2PluginUiPort, port_group), - json_map::enum_reference("units", &Lv2PluginUiControlPort::units_, get_units_enum_converter()), - MAP_REF(Lv2PluginUiControlPort, comment), - MAP_REF(Lv2PluginUiControlPort, is_bypass), - MAP_REF(Lv2PluginUiControlPort, is_program_controller), - MAP_REF(Lv2PluginUiControlPort, custom_units), + json_map::enum_reference("units", &Lv2PluginUiPort::units_, get_units_enum_converter()), + 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 e3e6b90..ebe5437 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -206,6 +206,7 @@ namespace pipedal bool is_audio_port_ = false; bool is_atom_port_ = false; bool is_cv_port_ = false; + bool connection_optional_ = false; bool is_valid_ = false; bool supports_midi_ = false; @@ -275,6 +276,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(is_control_port); LV2_PROPERTY_GETSET_SCALAR(is_audio_port); LV2_PROPERTY_GETSET_SCALAR(is_atom_port); + LV2_PROPERTY_GETSET_SCALAR(connection_optional); LV2_PROPERTY_GETSET_SCALAR(is_cv_port); LV2_PROPERTY_GETSET_SCALAR(is_valid); LV2_PROPERTY_GETSET_SCALAR(supports_midi); @@ -338,6 +340,8 @@ namespace pipedal Lv2PluginInfo() {} private: + std::shared_ptr FindWritablePathProperties(PluginHost *lv2Host,const LilvPlugin*pPlugin); + bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin); std::string bundle_path_; std::string uri_; @@ -467,14 +471,20 @@ namespace pipedal static json_map::storage_type jmap; }; - class Lv2PluginUiControlPort + class Lv2PluginUiPort { public: - Lv2PluginUiControlPort() + Lv2PluginUiPort() { } - Lv2PluginUiControlPort(const Lv2PluginInfo *pPlugin, const Lv2PortInfo *pPort) - : symbol_(pPort->symbol()), index_(pPort->index()), name_(pPort->name()), min_value_(pPort->min_value()), max_value_(pPort->max_value()), default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()), is_logarithmic_(pPort->is_logarithmic()), integer_property_(pPort->integer_property()), enumeration_property_(pPort->enumeration_property()), toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), comment_(pPort->comment()), units_(pPort->units()) + Lv2PluginUiPort(const Lv2PluginInfo *pPlugin, const Lv2PortInfo *pPort) + : symbol_(pPort->symbol()), index_(pPort->index()), + is_input_(pPort->is_input()), name_(pPort->name()), min_value_(pPort->min_value()), max_value_(pPort->max_value()), + default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()), + is_logarithmic_(pPort->is_logarithmic()), integer_property_(pPort->integer_property()), enumeration_property_(pPort->enumeration_property()), + toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), + comment_(pPort->comment()), units_(pPort->units()), + connection_optional_(pPort->connection_optional()) { // Use symbols to index port groups, instead of uris. // symbols are guaranteed to be unique. @@ -489,31 +499,40 @@ namespace pipedal break; } } + is_bypass_ = name_ == "bypass" + || name_ == "Bypass" + || symbol_ == "bypass" + || symbol_ == "Bypass"; } private: std::string symbol_; int index_; std::string name_; + bool is_input_ = true; float min_value_ = 0, max_value_ = 1, default_value_ = 0; - int range_steps_ = 0; - int display_priority_ = -1; bool is_logarithmic_ = false; + int display_priority_ = -1; + + int range_steps_ = 0; bool integer_property_ = false; bool enumeration_property_ = false; - bool toggled_property_ = false; bool not_on_gui_ = false; + bool toggled_property_ = false; std::vector scale_points_; std::string port_group_; + Units units_ = Units::none; std::string comment_; bool is_bypass_ = false; bool is_program_controller_ = false; std::string custom_units_; + bool connection_optional_ = false; public: LV2_PROPERTY_GETSET(symbol); LV2_PROPERTY_GETSET_SCALAR(index); + LV2_PROPERTY_GETSET_SCALAR(is_input); LV2_PROPERTY_GETSET(name); LV2_PROPERTY_GETSET(port_group); LV2_PROPERTY_GETSET_SCALAR(min_value); @@ -532,9 +551,10 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(is_bypass); LV2_PROPERTY_GETSET_SCALAR(is_program_controller); LV2_PROPERTY_GETSET(custom_units); + LV2_PROPERTY_GETSET(connection_optional); public: - static json_map::storage_type jmap; + static json_map::storage_type jmap; }; class Lv2PluginUiInfo @@ -557,7 +577,7 @@ namespace pipedal std::string description_; bool is_vst3_ = false; - std::vector controls_; + std::vector controls_; std::vector port_groups_; std::vector fileProperties_; std::vector uiPortNotifications_; @@ -601,7 +621,8 @@ namespace pipedal #endif friend class pipedal::AutoLilvNode; friend class pipedal::PiPedalUI; - static const char *RDFS_COMMENT_URI; + static const char *RDFS__comment; + static const char *RDFS__range; public: class LilvUris @@ -610,25 +631,31 @@ namespace pipedal void Initialize(LilvWorld *pWorld); void Free(); - AutoLilvNode rdfsComment; - AutoLilvNode logarithic_uri; - AutoLilvNode display_priority_uri; - AutoLilvNode range_steps_uri; + AutoLilvNode rdfs__Comment; + AutoLilvNode rdfs__range; + AutoLilvNode port_logarithmic; + AutoLilvNode port__display_priority; + AutoLilvNode port_range_steps; AutoLilvNode integer_property_uri; AutoLilvNode enumeration_property_uri; - AutoLilvNode toggle_property_uri; - AutoLilvNode not_on_gui_property_uri; - AutoLilvNode midiEventNode; - AutoLilvNode designationNode; - AutoLilvNode portGroupUri; - AutoLilvNode unitsUri; - AutoLilvNode bufferType_uri; - AutoLilvNode pset_Preset; - AutoLilvNode rdfs_label; - AutoLilvNode symbolUri; - AutoLilvNode nameUri; + AutoLilvNode core__toggled; + AutoLilvNode core__connectionOptional; + AutoLilvNode portprops__not_on_gui_property_uri; + AutoLilvNode midi__event; + AutoLilvNode core__designation; + AutoLilvNode portgroups__group; + AutoLilvNode units__unit; + AutoLilvNode invada_units__unit; // typo in invada plugins. + AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins. - AutoLilvNode lv2Core__name; + AutoLilvNode atom__bufferType; + AutoLilvNode atom__Path; + AutoLilvNode presets__preset; + AutoLilvNode rdfs__label; + AutoLilvNode lv2core__symbol; + AutoLilvNode lv2core__name; + AutoLilvNode lv2core__index; + AutoLilvNode lv2core__Parameter; AutoLilvNode pipedalUI__ui; AutoLilvNode pipedalUI__fileProperties; AutoLilvNode pipedalUI__directory; @@ -657,6 +684,10 @@ namespace pipedal AutoLilvNode ui__peakProtocol; AutoLilvNode ui__portIndex; AutoLilvNode lv2__symbol; + + AutoLilvNode patch__writable; + AutoLilvNode patch__readable; + }; LilvUris lilvUris; diff --git a/src/PluginPreset.hpp b/src/PluginPreset.hpp index 2244cf1..c139e59 100644 --- a/src/PluginPreset.hpp +++ b/src/PluginPreset.hpp @@ -25,6 +25,7 @@ #include #include #include "StateInterface.hpp" +#include "Pedalboard.hpp" namespace pipedal { @@ -69,20 +70,30 @@ public: }; - class PluginPreset { public: PluginPreset() { } - PluginPreset(uint64_t instanceId, std::string&&label, std::map && controlValues) - : instanceId_(instanceId), - label_(std::forward(label)), - controlValues_(std::forward>(controlValues)) - { + PluginPreset(uint64_t instanceId, const std::string&label,const PedalboardItem&pedalboardItem) + : instanceId_(instanceId) + , label_(label) + , state_(pedalboardItem.lv2State()) + { + for (auto & controlValue : pedalboardItem.controlValues()) + { + this->controlValues_[controlValue.key()] = controlValue.value(); + } + } - PluginPreset(uint64_t instanceId, const std::string&label, const std::map & controlValues) + PluginPreset( + uint64_t instanceId, + const std::string&label, + const std::map & controlValues, + const Lv2PluginState &state) : instanceId_(instanceId), label_(label), - controlValues_(controlValues) + controlValues_((controlValues)), + state_(state) { } diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 3c2d5f0..4d193fc 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -30,35 +30,38 @@ namespace pipedal enum class RingBufferCommand : int64_t { - ReplaceEffect = 0, - EffectReplaced = 1, - SetValue = 2, - SetBypass = 3, - AudioStopped = 4, - SetVuSubscriptions = 5, - FreeVuSubscriptions = 6, - SendVuUpdate = 7, - AckVuUpdate = 8, + Invalid = 0, + ReplaceEffect, + EffectReplaced, + SetValue, + SetBypass, + AudioStopped, + SetVuSubscriptions, + FreeVuSubscriptions, + SendVuUpdate, + AckVuUpdate, - SetMonitorPortSubscription = 9, - FreeMonitorPortSubscription = 10, - SendMonitorPortUpdate = 11, - AckMonitorPortUpdate = 12, - ParameterRequest = 13, - ParameterRequestComplete = 14, + SetMonitorPortSubscription, + FreeMonitorPortSubscription, + SendMonitorPortUpdate, + AckMonitorPortUpdate, + ParameterRequest, + ParameterRequestComplete, - MidiValueChanged = 15, + MidiValueChanged, - OnMidiListen = 16, + OnMidiListen, - AtomOutput = 17, + AtomOutput, - MidiProgramChange = 18, // program change requested via midi. - AckMidiProgramChange = 19, + MidiProgramChange, // program change requested via midi. + AckMidiProgramChange, - NextMidiProgram = 20, + NextMidiProgram, - Lv2StateChanged = 21, + Lv2StateChanged, + SetInputVolume, + SetOutputVolume, }; @@ -154,6 +157,11 @@ namespace pipedal int controlIndex; float value; }; + class SetVolumeBody + { + public: + float value; + }; class Lv2Pedalboard; @@ -369,6 +377,19 @@ namespace pipedal body.value = value; write(RingBufferCommand::SetValue,body); } + void SetInputVolume(float value) + { + SetVolumeBody body; + body.value = value; + write(RingBufferCommand::SetInputVolume,body); + } + void SetOutputVolume(float value) + { + SetVolumeBody body; + body.value = value; + write(RingBufferCommand::SetOutputVolume,body); + } + void FreeVuSubscriptions(RealtimeVuBuffers *configuration) { diff --git a/src/StateInterface.cpp b/src/StateInterface.cpp index 8edae1e..c23bc7e 100644 --- a/src/StateInterface.cpp +++ b/src/StateInterface.cpp @@ -153,7 +153,7 @@ void StateInterface::Restore(const Lv2PluginState &state) } catch (const std::exception &e) { - throw std::logic_error(SS("State save failed. " << e.what())); + throw std::logic_error(SS("State restore failed. " << e.what())); } } /*static*/ const void *StateInterface::FnStateRetreiveFunction( @@ -199,10 +199,18 @@ const void *StateInterface::StateRetrieveFunction( void Lv2PluginState::write_json(json_writer &writer) const { + writer.start_array(); + writer.write(isValid_); + writer.write_raw(","); writer.write(values_); + writer.end_array(); } void Lv2PluginState::read_json(json_reader &reader) { + reader.consume('['); + reader.read(&isValid_); + reader.consume(','); reader.read(&values_); + reader.consume(']'); } diff --git a/src/StateInterface.hpp b/src/StateInterface.hpp index ea582c4..f12fa24 100644 --- a/src/StateInterface.hpp +++ b/src/StateInterface.hpp @@ -41,7 +41,12 @@ namespace pipedal class Lv2PluginState: public JsonSerializable { public: + bool isValid_ = false; std::map values_; + void Erase() { + isValid_ = false; + values_.clear(); + } std::string ToString() const; private: diff --git a/src/Storage.cpp b/src/Storage.cpp index 3b47ce3..eb039ff 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -264,7 +264,7 @@ std::filesystem::path Storage::GetPluginPresetsDirectory() const { return this->dataRoot / "plugin_presets"; } -std::filesystem::path Storage::GetPluginStorageDirectory() const +std::filesystem::path Storage::GetPluginAudioFileDirectory() const { return this->dataRoot / "audio_uploads"; } @@ -635,6 +635,29 @@ std::string Storage::GetPresetCopyName(const std::string &name) ++copyNumber; } } + +int64_t Storage::CreateNewPreset() +{ + Pedalboard newPedalboard = Pedalboard::MakeDefault(); + std::string name = "New"; + if (currentBank.hasName(name)) + { + int copyNumber = 2; + while (true) + { + name = SS("New (" << copyNumber << ")"); + if (!currentBank.hasName(name)) + { + break; + } + ++copyNumber; + } + } + newPedalboard.name(name); + return this->currentBank.addPreset(newPedalboard,-1); + +} + int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &fromItem = this->currentBank.getItem(fromId); @@ -1189,28 +1212,64 @@ PluginUiPresets Storage::GetPluginUiPresets(const std::string &pluginUri) const return result; } -std::vector Storage::GetPluginPresetValues(const std::string &pluginUri, uint64_t instanceId) +PluginPresetValues Storage::GetPluginPresetValues(const std::string &pluginUri, uint64_t instanceId) { auto presets = GetPluginPresets(pluginUri); for (const auto &preset : presets.presets_) { if (preset.instanceId_ == instanceId) { - std::vector result; + PluginPresetValues result; + for (const auto &valuePair : preset.controlValues_) { - result.push_back(ControlValue(valuePair.first.c_str(), valuePair.second)); + result.controls.push_back(ControlValue(valuePair.first.c_str(), valuePair.second)); } + result.state = preset.state_; return result; } } throw PiPedalException("Plugin preset not found."); } +uint64_t Storage::SavePluginPreset( + const std::string &name, + const PedalboardItem &pedalboardItem) +{ + const std::string&pluginUri = pedalboardItem.uri(); + auto presets = GetPluginPresets(pluginUri); + uint64_t result = -1; + bool existing = false; + for (size_t i = 0; i < presets.presets_.size(); ++i) + { + auto &preset = presets.presets_[i]; + if (preset.label_ == name) + { + existing = true; + result = preset.instanceId_; + presets.presets_[i] = PluginPreset(preset.instanceId_,preset.label_,pedalboardItem); + break; + } + } + if (!existing) + { + result = presets.nextInstanceId_++; + presets.presets_.push_back( + PluginPreset(result, + name, + pedalboardItem)); + } + this->SavePluginPresets(pluginUri, presets); + return result; +} + uint64_t Storage::SavePluginPreset( const std::string &pluginUri, const std::string &name, - const std::map &values) + float inputVolume, + float outputVolume, + const std::map &values, + const Lv2PluginState &lv2State) { auto presets = GetPluginPresets(pluginUri); uint64_t result = -1; @@ -1220,8 +1279,10 @@ uint64_t Storage::SavePluginPreset( auto &preset = presets.presets_[i]; if (preset.label_ == name) { - preset.controlValues_ = values; existing = true; + preset.controlValues_ = values; + preset.state_ = lv2State; + result = preset.instanceId_; break; } @@ -1233,7 +1294,8 @@ uint64_t Storage::SavePluginPreset( PluginPreset( result, name, - values)); + values, + lv2State)); } this->SavePluginPresets(pluginUri, presets); return result; @@ -1446,7 +1508,7 @@ std::vector Storage::GetFileList(const UiFileProperty &fileProperty // if fileProperty has a user-accessible directory, push the entire file path. if (fileProperty.directory().size() != 0) { - std::filesystem::path audioFileDirectory = this->GetPluginStorageDirectory() / fileProperty.directory(); + std::filesystem::path audioFileDirectory = this->GetPluginAudioFileDirectory() / fileProperty.directory(); try { for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) @@ -1474,6 +1536,100 @@ std::vector Storage::GetFileList(const UiFileProperty &fileProperty return result; } +bool Storage::IsValidSampleFile(const std::filesystem::path &fileName) +{ + if (!fileName.is_absolute()) + { + return false; + } + std::filesystem::path audioFilePath = this->GetPluginAudioFileDirectory(); + + std::filesystem::path parentDirectory = fileName.parent_path(); + while (true) + { + if (!parentDirectory.has_parent_path()) + { + return false; + } + std::string name = parentDirectory.filename().string(); + if (name == ".." || name == ".") + return false; + + if (parentDirectory == audioFilePath) + return true; + parentDirectory = parentDirectory.parent_path(); + if (parentDirectory.string().length() < audioFilePath.string().length()) + { + return false; + } + } +} +void Storage::DeleteSampleFile(const std::filesystem::path &fileName) +{ + if (!IsValidSampleFile(fileName)) + { + throw std::logic_error("Permission denied."); + } + if (!std::filesystem::exists(fileName)) + { + throw std::logic_error("File not found."); + } + try + { + std::filesystem::remove(fileName); + } + catch (const std::exception &) + { + throw std::logic_error("Permission denied."); + } +} +std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, const std::string &filename) +{ + if (!UiFileProperty::IsDirectoryNameValid(directory)) + { + throw std::logic_error("Permission denied."); + } + std::filesystem::path filePath{filename}; + + if (filePath.has_parent_path()) + { + throw std::logic_error("Permission denied."); + } + std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename; + if (!this->IsValidSampleFile(result)) + { + throw std::logic_error("Permission denied."); + } + return result; +} +std::string Storage::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, const std::string &fileBody) +{ + std::filesystem::path path; + if (directory.length() != 0) + { + path = this->MakeUserFilePath(directory, filename); + } + else + { + if (patchProperty.length() == 0) + { + throw std::logic_error("Malformed request."); + } + throw std::logic_error("patchProperty directory not implemented."); + } + { + std::filesystem::create_directories(path.parent_path()); + + std::ofstream f(path, std::ios_base::trunc | std::ios_base::binary); + if (!f.is_open()) + { + throw std::logic_error(SS("Can't create file " << path << ".")); + } + f.write(fileBody.c_str(), fileBody.length()); + } + return path.string(); +} + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index ecafc68..6c0d4b4 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -53,6 +53,12 @@ public: }; +struct PluginPresetValues { + + std::vector controls; + Lv2PluginState state; +}; + // controls user-defined storage. Implmentation hidden to allow to later migration to a database (perhaps) class Storage { @@ -100,7 +106,7 @@ public: void SetDataRoot(const std::filesystem::path& path); void SetConfigRoot(const std::filesystem::path& path); - std::filesystem::path GetPluginStorageDirectory() const; + std::filesystem::path GetPluginAudioFileDirectory() const; std::vector GetPedalboards(); @@ -132,6 +138,8 @@ public: int64_t DeletePreset(int64_t presetId); bool RenamePreset(int64_t presetId, const std::string&name); int64_t CopyPreset(int64_t fromId, int64_t toId = -1); + int64_t CreateNewPreset(); + void RenameBank(int64_t bankId, const std::string&newName); int64_t SaveBankAs(int64_t bankId, const std::string&newName); @@ -166,13 +174,29 @@ private: void LoadPluginPresetIndex(); void SavePluginPresetIndex(); std::filesystem::path GetPluginPresetPath(const std::string &pluginUri) const; + bool IsValidSampleFile(const std::filesystem::path&fileName); + std::filesystem::path MakeUserFilePath(const std::string &directory, const std::string&filename); + public: bool HasPluginPresets(const std::string&pluginUri) const; void SavePluginPresets(const std::string&pluginUri, const PluginPresets&presets); PluginPresets GetPluginPresets(const std::string&pluginUri) const; PluginUiPresets GetPluginUiPresets(const std::string&pluginUri) const; - std::vector GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId); - uint64_t SavePluginPreset(const std::string&pluginUri, const std::string&name, const std::map & values); + PluginPresetValues GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId); + + uint64_t SavePluginPreset( + const std::string&name, + const PedalboardItem&pedalboardEntry + ); + + uint64_t SavePluginPreset( + const std::string &pluginUri, + const std::string &name, + float inputVolume, + float outputVolume, + const std::map &values, + const Lv2PluginState& lv2State); + void UpdatePluginPresets(const PluginUiPresets &pluginPresets); uint64_t CopyPluginPreset(const std::string&pluginUri,uint64_t presetId); @@ -183,7 +207,8 @@ public: bool GetShowStatusMonitor() const; void SetSystemMidiBindings(const std::vector&bindings); std::vector GetSystemMidiBindings(); - + void DeleteSampleFile(const std::filesystem::path &fileName); + std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); }; diff --git a/src/Units.cpp b/src/Units.cpp index ed9b034..c85760c 100644 --- a/src/Units.cpp +++ b/src/Units.cpp @@ -36,32 +36,46 @@ Units UriToUnits(const std::string &uri); std::map unitsToStringMap = { + //LV2_UNITS__conversion + //LV2_UNITS__name CASE(none) CASE(unknown) - CASE(bar) CASE(beat) CASE(bpm) CASE(cent) CASE(cm) + CASE(coef) CASE(db) + CASE(degree) + CASE(frame) CASE(hz) + CASE(inch) CASE(khz) CASE(km) CASE(m) CASE(mhz) CASE(midiNote) + CASE(mile) CASE(min) + CASE(mm) CASE(ms) - CASE(pc) + CASE(oct) CASE(s) + CASE(pc) CASE(semitone12TET) }; +static const std::string emptyString; const std::string& pipedal::UnitsToString(Units units) { - return unitsToStringMap[units]; + if (unitsToStringMap.find(units) != unitsToStringMap.end()) + { + return unitsToStringMap[units]; + } else { + return emptyString; + } } #undef CASE @@ -76,17 +90,24 @@ std::map unitMap = { CASE(bpm) CASE(cent) CASE(cm) + CASE(coef) CASE(db) + CASE(degree) + CASE(frame) CASE(hz) + CASE(inch) CASE(khz) CASE(km) CASE(m) CASE(mhz) CASE(midiNote) + CASE(mile) CASE(min) + CASE(mm) CASE(ms) - CASE(pc) + CASE(oct) CASE(s) + CASE(pc) CASE(semitone12TET) }; #undef CASE @@ -97,38 +118,52 @@ std::map unitMap = { std::map uriToUnitsMap = { + { "", Units::none,}, CASE(bar) CASE(beat) CASE(bpm) CASE(cent) CASE(cm) + CASE(coef) CASE(db) + CASE(degree) + CASE(frame) CASE(hz) + CASE(inch) CASE(khz) CASE(km) CASE(m) CASE(mhz) CASE(midiNote) + CASE(mile) CASE(min) + CASE(mm) CASE(ms) - CASE(pc) + CASE(oct) CASE(s) + CASE(pc) CASE(semitone12TET) }; +#undef CASE Units pipedal::StringToUnits(const std::string &text) { - return unitMap[text]; + if (unitMap.find(text) != unitMap.end()) { + return unitMap[text]; + } + return Units::none; } Units pipedal::UriToUnits(const std::string &text) { if (text.length() == 0) return Units::none; - auto result = uriToUnitsMap[text]; - if (result == Units::none) return Units::unknown; - return result; - + if (uriToUnitsMap.contains(text)) + { + auto result = uriToUnitsMap[text]; + return result; + } + return Units::unknown; } diff --git a/src/Units.hpp b/src/Units.hpp index 2dfe5d0..3ba14f0 100644 --- a/src/Units.hpp +++ b/src/Units.hpp @@ -44,7 +44,14 @@ enum class Units { pc, s, semitone12TET, - custom + custom, + degree, + coef, + frame, + inch, + mile, + mm, + oct, }; diff --git a/src/Vst3Host.cpp b/src/Vst3Host.cpp index bd0e725..76202fe 100644 --- a/src/Vst3Host.cpp +++ b/src/Vst3Host.cpp @@ -272,7 +272,7 @@ void Vst3Host::Private::UpdateControlInfo(IEditController *controller, Lv2Plugin Steinberg::Vst::ParameterInfo info; tresult tErr = controller->getParameterInfo(param, info); - Lv2PluginUiControlPort port; + Lv2PluginUiPort port; port.index(param); // used for LV2 purposes. port.symbol(SS(info.id)); // Used for VST3 purposes. convert to int to specifiy a vst control id. diff --git a/src/VuUpdate.hpp b/src/VuUpdate.hpp index f56f1c8..277fa5b 100644 --- a/src/VuUpdate.hpp +++ b/src/VuUpdate.hpp @@ -26,7 +26,7 @@ namespace pipedal class VuUpdate { public: - uint64_t instanceId_ = 0; + int64_t instanceId_ = 0; long sampleTime_ = 0; bool isStereoInput_ = false; bool isStereoOutput_ = false; @@ -76,7 +76,6 @@ namespace pipedal { AccumulateVu(&outputMaxValueL_,outputL,samples); AccumulateVu(&outputMaxValueR_,outputR,samples); - } DECLARE_JSON_MAP(VuUpdate); diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 9add1cb..7e919f7 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -27,6 +27,8 @@ #include +#include "WebServerLog.hpp" + using namespace pipedal; using namespace std; @@ -37,6 +39,34 @@ using tcp = boost::asio::ip::tcp; // from const size_t MAX_READ_SIZE = 1 * 1024 * 204; using namespace boost; +class CustomPpConfig: public websocketpp::config::asio { +public: + typedef CustomPpConfig type; + typedef websocketpp::config::asio base; + + + static const size_t max_http_body_size = 100000000; //websocketpp::config::asio::max_http_body_size; + typedef pipedal_elog elog_type; + typedef pipedal_alog alog_type; + + + struct transport_config : public base::transport_config { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::basic_socket::endpoint + socket_type; + }; + + typedef websocketpp::transport::asio::endpoint + transport_type; + +}; + + + std::string pipedal::last_modified(const std::filesystem::path &path) { @@ -149,7 +179,7 @@ namespace pipedal boost::asio::io_context *pIoContext = nullptr; typedef websocketpp::connection_hdl connection_hdl; - typedef websocketpp::server server; + typedef websocketpp::server server; class HttpRequestImpl : public HttpRequest { @@ -208,11 +238,17 @@ namespace pipedal private: // IWriteCallback - virtual void close() { webSocket->close(websocketpp::close::status::normal, ""); } + virtual void close() { + webSocket->close(websocketpp::close::status::normal, ""); + webSocket = nullptr; + } virtual void writeCallback(const std::string &text) { - webSocket->send(text, websocketpp::frame::opcode::text); + if (webSocket) + { + webSocket->send(text, websocketpp::frame::opcode::text); + } } virtual std::string getFromAddress() const { @@ -222,12 +258,18 @@ namespace pipedal public: ~WebSocketSession() { + this->socketHandler = nullptr; + webSocket = nullptr; + pServer = nullptr; Lv2Log::info("WebSocketSession closed."); } using ptr = std::shared_ptr; WebSocketSession(WebServerImpl *pServer, server::connection_ptr &webSocket) : pServer(pServer), webSocket(webSocket) + { + } + void Open() { uri requestUri(webSocket->get_uri()->str().c_str()); fromAddress = SS(webSocket->get_socket().remote_endpoint()); @@ -276,48 +318,58 @@ namespace pipedal std::set> m_sessions; - void on_session_closed(WebSocketSession::ptr session, connection_hdl hConnection) + void on_session_closed(WebSocketSession::ptr &session, connection_hdl hConnection) { - m_sessions.erase(session); + m_sessions.erase(session); + session = nullptr; // probably delete here. m_connections.erase(hConnection); } void NotFound(server::connection_type &connection, const std::string &filename) { - // 404 error - std::stringstream ss; + try { + // 404 error + std::stringstream ss; - ss << "" - << "Error 404 (Resource not found)" - << "

Error 404

" - << "

The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.

" - << ""; + ss << "" + << "Error 404 (Resource not found)" + << "

Error 404

" + << "

The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.

" + << ""; - std::string body = ss.str(); - connection.set_body(body); - std::stringstream ssLen; - ssLen << body.length(); - connection.replace_header(HttpField::content_length, ssLen.str()); - connection.set_status(websocketpp::http::status_code::not_found); + std::string body = ss.str(); + connection.set_body(body); + std::stringstream ssLen; + ssLen << body.length(); + connection.replace_header(HttpField::content_length, ssLen.str()); + connection.set_status(websocketpp::http::status_code::not_found); + } catch (const std::exception&) + { + // ignored. Things weren't going well anyway. + } return; }; void ServerError(server::connection_type &connection, const std::string &error) { - // 404 error - std::stringstream ss; + try { + // 404 error + std::stringstream ss; - ss << "" - << "Error 500 (Server error)" - << "

Error 500

" - << "

" << HtmlHelper::HtmlEncode(error) << "

" - << ""; - std::string body = ss.str(); - connection.set_body(body); - std::stringstream ssLen; - ssLen << body.length(); - connection.replace_header(HttpField::content_length, ssLen.str()); + ss << "" + << "Error 500 (Server error)" + << "

Error 500

" + << "

" << HtmlHelper::HtmlEncode(error) << "

" + << ""; + std::string body = ss.str(); + connection.set_body(body); + std::stringstream ssLen; + ssLen << body.length(); + connection.replace_header(HttpField::content_length, ssLen.str()); - connection.set_status(websocketpp::http::status_code::internal_server_error); + connection.set_status(websocketpp::http::status_code::internal_server_error); + } catch (const std::exception&) + { + } return; }; @@ -564,10 +616,16 @@ namespace pipedal { m_connections.insert(hdl); - server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl); - WebSocketSession::ptr socketSession = std::make_shared(this, webSocket); - m_sessions.insert(socketSession); + try { + server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl); + WebSocketSession::ptr socketSession = std::make_shared(this, webSocket); + socketSession->Open(); + m_sessions.insert(socketSession); + } catch (const std::exception&e) + { + Lv2Log::error("Failed to open session: %s", e.what()); + } } void on_fail(connection_hdl hdl) @@ -591,7 +649,6 @@ namespace pipedal m_endpoint.set_reuse_addr(true); m_endpoint.clear_access_channels(websocketpp::log::alevel::all); - // m_endpoint.set_access_channels(websocketpp::log::alevel::access_core); m_endpoint.set_access_channels(websocketpp::log::alevel::fail); m_endpoint.init_asio(&ioc); diff --git a/src/WebServerLog.hpp b/src/WebServerLog.hpp new file mode 100644 index 0000000..26a3d65 --- /dev/null +++ b/src/WebServerLog.hpp @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2014, Peter Thorson. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the WebSocket++ Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + Based on websocketpp/logger/pipedal_logger.hpp, Copyright Peter Thorson. +*/ + + + +/// Logger that redirects webserverpp messages to Lv2Log. +#ifndef WEBSOCKETPP_LOGGER_PIPEDAL_HPP +#define WEBSOCKETPP_LOGGER_PIPEDAL_HPP + + +/* Need a way to print a message to the log + * + * - timestamps + * - channels + * - thread safe + * - output to stdout or file + * - selective output channels, both compile time and runtime + * - named channels + * - ability to test whether a log message will be printed at compile time + * + */ + +#include +#include +#include +#include +#include "Lv2Log.hpp" +#include "ss.hpp" +#include + + + + +namespace pipedal { + + + + +class pipedal_elog { +public: + using level = websocketpp::log::level; + using elevel = websocketpp::log::elevel; + using channel_type_hint = websocketpp::log::channel_type_hint; + pipedal_elog() + : m_channels(0xffffffff) + { + + } + + /// Destructor + ~pipedal_elog() {} + + /// Copy constructor + pipedal_elog(pipedal_elog const & other) + : m_channels(other.m_channels) + {} + + pipedal_elog(level level,channel_type_hint::value hint = channel_type_hint::error) + { + this->m_channels = level; + } + +#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + // no copy assignment operator because of const member variables + pipedal_elog & operator=(pipedal_elog const &) = delete; +#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + +#ifdef _WEBSOCKETPP_MOVE_SEMANTICS_ + /// Move constructor + pipedal_elog(pipedal_elog && other) + : m_channels(other.m_channels) + {} + +#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + // no move assignment operator because of const member variables + pipedal_elog & operator=(pipedal_elog &&) = delete; +#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + +#endif // _WEBSOCKETPP_MOVE_SEMANTICS_ + + void set_channels(level channels) { + m_channels |= channels; + } + + void clear_channels(level channels) { + m_channels &= ~m_channels; + + } + + /// Write a string message to the given channel + /** + * @param channel The channel to write to + * @param msg The message to write + */ + void write(level channel, std::string const & msg) { + if (!dynamic_test(channel)) return; + switch (channel) + { + case elevel::devel: + case elevel::library: + Lv2Log::debug(msg); + break; + case elevel::info: + Lv2Log::info(msg); + break; + case elevel::warn: + Lv2Log::warning(msg); + break; + case elevel::rerror: + Lv2Log::error(msg); + break; + case elevel::fatal: + Lv2Log::error("Fatal error: " + msg); + break; + default: + break; + } + } + + /// Write a cstring message to the given channel + /** + * @param channel The channel to write to + * @param msg The message to write + */ + void write(level channel, char const * msg) { + write(channel,std::string(msg)); + } + + constexpr bool static_test(level channel) const { + return m_static_channels; + } + + bool dynamic_test(level channel) { + return (m_channels) & channel != 0; + } + +protected: +private: + static constexpr level m_static_channels = elevel::warn | elevel::rerror | elevel::fatal; + level m_channels; +}; + +class pipedal_alog { +public: + using level = websocketpp::log::level; + using alevel = websocketpp::log::alevel; + using channel_type_hint = websocketpp::log::channel_type_hint; + + + pipedal_alog() + :m_channels(alevel::fail) + { + } + + pipedal_alog(level level, channel_type_hint::value hint = channel_type_hint::access) + : m_channels(level) + { + + } + /// Destructor + ~pipedal_alog() {} + + /// Copy constructor + pipedal_alog(pipedal_alog const & other) + :m_channels(other.m_channels) + {} + +#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + // no copy assignment operator because of const member variables + pipedal_alog & operator=(pipedal_elog const &) = delete; +#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + +#ifdef _WEBSOCKETPP_MOVE_SEMANTICS_ + /// Move constructor + pipedal_alog(pipedal_alog && other) + {} + +#ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + // no move assignment operator because of const member variables + pipedal_alog & operator=(pipedal_elog &&) = delete; +#endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ + +#endif // _WEBSOCKETPP_MOVE_SEMANTICS_ + + void set_channels(level channels) { + m_channels |= channels; + } + + void clear_channels(level channels) { + m_channels = m_channels & ~channels; + } + + /// Write a string message to the given channel + /** + * @param channel The channel to write to + * @param msg The message to write + */ + void write(level channel, std::string const & msg) { + if (dynamic_test(channel)) + { + Lv2Log::info(SS('[' << alevel::channel_name(channel) << "] " << msg)); + } + } + + /// Write a cstring message to the given channel + /** + * @param channel The channel to write to + * @param msg The message to write + */ + void write(level channel, char const * msg) { + write(channel,std::string(msg)); + } + + constexpr bool static_test(level channel) const { + return (this->m_static_channels & channel) != 0; + } + + bool dynamic_test(level channel) { + return false; + } + +protected: +private: + static constexpr level m_static_channels = alevel::access_core; + level m_channels; +}; + +} //namespace pipedal + +#endif // WEBSOCKETPP_LOGGER_PIPEDAL_HPP diff --git a/src/main.cpp b/src/main.cpp index 4ed5c8d..f019d0e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -139,6 +139,9 @@ public: return true; } else if (segment == "uploadBank") + { + return true; + } else if (segment == "uploadUserFile") { return true; } @@ -402,6 +405,37 @@ public: res.setContentLength(result.length()); res.setBody(result); + } else if (segment == "uploadUserFile") + { + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + + const std::string &fileBody = req.body(); + const std::string &directory = request_uri.query("directory"); + const std::string &filename = request_uri.query("filename"); + const std::string &patchProperty = request_uri.query("property"); + + + if (patchProperty.length() == 0 && directory.length() == 0) + { + throw PiPedalException("Malformed request."); + + } + + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + + std::string outputFileName = std::filesystem::path(directory) / filename; + + std::string path = this->model->UploadUserFile(directory,patchProperty,filename,fileBody); + + std::stringstream ss; + json_writer writer(ss); + writer.write(outputFileName); + std::string response = ss.str(); + + res.setContentLength(response.length()); + res.setBody(response); } else { @@ -698,6 +732,21 @@ int main(int argc, char *argv[]) model.GetAlsaDevices(); } + // only accept signals on the main thread. + int sig; + sigset_t sigSet; + int s; + sigemptyset(&sigSet); + sigaddset(&sigSet, SIGINT); + sigaddset(&sigSet, SIGTERM); + sigaddset(&sigSet, SIGUSR1); + + s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL); + if (s != 0) + { + throw std::logic_error("pthread_sigmask failed."); + } + #if JACK_HOST if (systemd) { @@ -767,28 +816,6 @@ int main(int argc, char *argv[]) { - int sig; - sigset_t sigSet; - int s; - sigemptyset(&sigSet); - sigaddset(&sigSet, SIGINT); - sigaddset(&sigSet, SIGTERM); - sigaddset(&sigSet, SIGUSR1); - - s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL); - if (s != 0) - { - throw std::logic_error("pthread_sigmask failed."); - } - if (s != 0) - { - throwSystemError(s); - } - - //signal(SIGINT, sig_handler); - //signal(SIGTERM, sig_handler); - //signal(SIGUSR1, sig_handler); - sigwait(&sigSet,&sig); if (systemd) diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template index 3fe571a..37ceaeb 100644 --- a/src/templates/pipedald.service.template +++ b/src/templates/pipedald.service.template @@ -3,6 +3,7 @@ Description=${DESCRIPTION} After=network.target After=sound.target After=local-fs.target +After=avahi-daemon