Alpha
This commit is contained in:
Vendored
-45
@@ -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",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-cab-sim>
|
||||
a lv2:Plugin ,
|
||||
@@ -49,7 +49,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 13 ;
|
||||
lv2:microVersion 14 ;
|
||||
rdfs:comment "TooB amplifier cabinet simulator." ;
|
||||
|
||||
mod:brand "TooB";
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
||||
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
|
||||
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
|
||||
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
|
||||
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix mod: <http://moddevices.com/ns/mod#> .
|
||||
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
|
||||
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
|
||||
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
|
||||
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
|
||||
@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
|
||||
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix state: <http://lv2plug.in/ns/ext/state#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
|
||||
|
||||
@prefix toobConvolutionReverb: <http://two-play.com/plugins/toob-convolution-reverb#> .
|
||||
@prefix toobImpulse: <http://two-play.com/impulseFile#> .
|
||||
|
||||
toobImpulse:#impulseFile
|
||||
a rdf:property;
|
||||
rdfs:label "File";
|
||||
rdfs:range atom:Path.
|
||||
|
||||
|
||||
<http://two-play.com/rerdavies#me>
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-convolution-reverb>
|
||||
a lv2:Plugin ,
|
||||
lv2:ReverbPlugin ;
|
||||
doap:name "TooB Convolution Reverb" ,
|
||||
"TooB Convolution Reverb"@en-gb-gb
|
||||
;
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
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" ;
|
||||
]
|
||||
.
|
||||
|
||||
|
||||
<http://two-play.com/plugins/toob-convolution-reverb> 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 <http://two-play.com/plugins/toob-convolution-reverb>;
|
||||
ui:protocol ui:floatProtocol;
|
||||
# pipedal_ui:style pipedal_ui:text ;
|
||||
# pipedal_ui:redLevel 0;
|
||||
# pipedal_ui:yellowLevel -12;
|
||||
# ui:portIndex 3;
|
||||
|
||||
]
|
||||
.
|
||||
@@ -41,7 +41,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
inputStage:filterGroup
|
||||
a param:ControlGroup ,
|
||||
@@ -59,7 +59,7 @@ inputStage:filterGroup
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 13 ;
|
||||
lv2:microVersion 14 ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "Input Stage";
|
||||
|
||||
@@ -1,624 +0,0 @@
|
||||
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
||||
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
|
||||
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
|
||||
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
|
||||
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix mod: <http://moddevices.com/ns/mod#> .
|
||||
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
|
||||
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
|
||||
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
|
||||
|
||||
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
|
||||
@prefix plug: <http://two-play.com/plugins/toob-power-stage#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix state: <http://lv2plug.in/ns/ext/state#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix modgui: <http://moddevices.com/ns/modgui#> .
|
||||
@prefix pstage: <http://two-play.com/plugins/toob-power-stage#> .
|
||||
|
||||
|
||||
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" .
|
||||
|
||||
|
||||
<http://two-play.com/plugins/toob-power-stage#uiState>
|
||||
a lv2:Parameter ;
|
||||
rdfs:label "uiState" ;
|
||||
rdfs:range atom:Vector .
|
||||
|
||||
|
||||
<http://two-play.com/rerdavies#me>
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
|
||||
<http://two-play.com/plugins/toob-power-stage>
|
||||
a lv2:Plugin ,
|
||||
lv2:SimulatorPlugin ;
|
||||
|
||||
doap:name "TooB Power Stage";
|
||||
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
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
|
||||
<http://two-play.com/plugins/toob-power-stage#uiState>;
|
||||
|
||||
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." ;
|
||||
]
|
||||
.
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ pstage:stage3
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-power-stage-2>
|
||||
a lv2:Plugin ,
|
||||
@@ -67,7 +67,7 @@ pstage:stage3
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 1 ;
|
||||
lv2:microVersion 14 ;
|
||||
|
||||
mod:brand "TooB";
|
||||
mod:label "Power Stage";
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-spectrum>
|
||||
a lv2:Plugin ,
|
||||
@@ -39,7 +39,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
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 ;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-tone-stack>
|
||||
a lv2:Plugin ,
|
||||
@@ -49,7 +49,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
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.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -28,7 +28,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-chorus>
|
||||
a lv2:Plugin ,
|
||||
@@ -39,7 +39,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 1 ;
|
||||
lv2:microVersion 14 ;
|
||||
rdfs:comment """
|
||||
Emulation of Boss CE-2 Chorus.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-delay>
|
||||
a lv2:Plugin ,
|
||||
@@ -39,7 +39,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 13 ;
|
||||
lv2:microVersion 14 ;
|
||||
rdfs:comment """
|
||||
A straighforward no-frills digital delay.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-freeverb>
|
||||
a lv2:Plugin ,
|
||||
@@ -39,7 +39,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 13 ;
|
||||
lv2:microVersion 14 ;
|
||||
rdfs:comment """
|
||||
ToobFreeverb is an Lv2 implementation of the famous Freeverb reverb effect.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ toobml:filterGroup
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-ml>
|
||||
a lv2:Plugin ,
|
||||
@@ -47,7 +47,7 @@ toobml:filterGroup
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
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.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
a foaf:Person ;
|
||||
foaf:name "Robin Davies" ;
|
||||
foaf:mbox <mailto:rerdavies@gmail.com> ;
|
||||
foaf:homepage <http://two-play.com/> .
|
||||
foaf:homepage <https://github.com/sponsors/rerdavies> .
|
||||
|
||||
<http://two-play.com/plugins/toob-tuner>
|
||||
a lv2:Plugin ,
|
||||
@@ -37,7 +37,7 @@
|
||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||
lv2:minorVersion 0 ;
|
||||
lv2:microVersion 1 ;
|
||||
lv2:microVersion 14 ;
|
||||
rdfs:comment """
|
||||
A chromatic guitar tuner.
|
||||
""" ;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -7,6 +7,11 @@
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ToneStack.ttl> .
|
||||
|
||||
<http://two-play.com/plugins/toob-convolution-reverb> a lv2:Plugin ;
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ConvolutionReverb.ttl> .
|
||||
|
||||
|
||||
<http://two-play.com/plugins/toob-ml> a lv2:Plugin ;
|
||||
lv2:binary <ToobAmp.so> ;
|
||||
rdfs:seeAlso <ToobML.ttl> .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,3 +12,6 @@
|
||||
padding-left: 36px;
|
||||
border-bottom: 1px #CCC solid
|
||||
}
|
||||
div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -665,7 +665,6 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
size="large">
|
||||
<MenuButton />
|
||||
</IconButton>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ flex: "0 1 400px", minWidth: 100 }}>
|
||||
<PresetSelector />
|
||||
</div>
|
||||
|
||||
@@ -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 (<div/>);
|
||||
return (<div />);
|
||||
}
|
||||
if (pedalboardItem.isSplit())
|
||||
{
|
||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
);
|
||||
}
|
||||
if (pedalboardItem.isSplit()) {
|
||||
return (
|
||||
<SplitControlView item={pedalboardItem as PedalboardSplitItem} instanceId={pedalboardItem!.instanceId}
|
||||
/>
|
||||
);
|
||||
} 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)
|
||||
{
|
||||
<div style={{paddingLeft: 40, paddingRight: 40}}>
|
||||
if (!uiPlugin) {
|
||||
<div style={{ paddingLeft: 40, paddingRight: 40 }}>
|
||||
<Typography color="error" variant="h6" >Missing plugin.</Typography>
|
||||
<Typography>The plugin '{pedalboardItem.pluginName}' ({pedalboardItem.uri}) is not currently installed.</Typography>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ transform: "" }}
|
||||
<div className={this.props.classes.frame} style={{ transform: "" }}
|
||||
onPointerDown={this.onPointerDown}
|
||||
onPointerMove={this.onPointerMove}
|
||||
onPointerCancel={this.onPointerCancel}
|
||||
|
||||
@@ -29,14 +29,16 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import OldDeleteIcon from './OldDeleteIcon';
|
||||
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalFileProperty } from './Lv2Plugin';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import UploadFileDialog from './UploadFileDialog';
|
||||
import OkCancelDialog from './OkCancelDialog';
|
||||
|
||||
export interface FilePropertyDialogProps {
|
||||
open: boolean,
|
||||
@@ -51,6 +53,8 @@ export interface FilePropertyDialogState {
|
||||
selectedFile: string;
|
||||
hasSelection: boolean;
|
||||
files: string[];
|
||||
openUploadFileDialog: boolean;
|
||||
openConfirmDeleteDialog: boolean;
|
||||
};
|
||||
|
||||
export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||
@@ -66,7 +70,9 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
fullScreen: false,
|
||||
selectedFile: props.selectedFile,
|
||||
hasSelection: false,
|
||||
files: []
|
||||
files: [],
|
||||
openUploadFileDialog: false,
|
||||
openConfirmDeleteDialog: false
|
||||
};
|
||||
|
||||
this.requestFiles();
|
||||
@@ -74,32 +80,22 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
private mounted: boolean = false;
|
||||
private model: PiPedalModel;
|
||||
|
||||
private lastFileProperty: PiPedalFileProperty = new PiPedalFileProperty();
|
||||
|
||||
private requestFiles() {
|
||||
if (!this.props.open)
|
||||
{
|
||||
if (!this.props.open) {
|
||||
return;
|
||||
}
|
||||
if (this.props.fileProperty.directory === "")
|
||||
{
|
||||
if (this.props.fileProperty.directory === "") {
|
||||
return;
|
||||
}
|
||||
if (this.lastFileProperty === this.props.fileProperty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.lastFileProperty = this.props.fileProperty;
|
||||
|
||||
this.model.requestFileList(this.props.fileProperty)
|
||||
.then((files) => {
|
||||
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<FilePr
|
||||
this.mounted = false;
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, 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<FilePr
|
||||
|
||||
}
|
||||
|
||||
private isFileInList(files: string[],file: string) {
|
||||
private isFileInList(files: string[], file: string) {
|
||||
|
||||
let hasSelection = false;
|
||||
for (var listFile of files) {
|
||||
@@ -152,7 +154,52 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
}
|
||||
|
||||
onSelect(selectedFile: string) {
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files,selectedFile) })
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files, selectedFile) })
|
||||
}
|
||||
|
||||
handleDelete() {
|
||||
this.setState({openConfirmDeleteDialog: true});
|
||||
}
|
||||
handleConfirmDelete()
|
||||
{
|
||||
this.setState({openConfirmDeleteDialog: false});
|
||||
if (this.state.hasSelection)
|
||||
{
|
||||
let selectedFile = this.state.selectedFile;
|
||||
let position = -1;
|
||||
for (let i = 0; i < this.state.files.length;++i)
|
||||
{
|
||||
let file = this.state.files[i];
|
||||
if (file === selectedFile)
|
||||
{
|
||||
position = i;
|
||||
}
|
||||
}
|
||||
let newSelection = "";
|
||||
if (position >= 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<FilePr
|
||||
this.mapKey = 0;
|
||||
return this.props.open &&
|
||||
(
|
||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={()=> {this.props.onCancel();}} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{opacity: "0.6"}} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >{this.props.fileProperty.name}</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row nowrap", overflowX: "auto", overflowY: "visible" }}>
|
||||
<div style={{ flex: "1 1 100%", display: "flex", flexFlow: "column wrap", justifyContent: "start", alignItems: "flex-start",paddingLeft: 16,paddingTop:16 }}>
|
||||
{
|
||||
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 (
|
||||
<ButtonBase key={ this.mapKey++ }
|
||||
style={{ width: "320px", flex: "0 0 48px", position: "relative"}}
|
||||
onClick={() => this.onSelect(value)}
|
||||
>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{this.fileNameOnly(value)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onCancel(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >{this.props.fileProperty.name}</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row nowrap", overflowX: "auto", overflowY: "visible" }}>
|
||||
<div style={{ flex: "1 1 100%", display: "flex", flexFlow: "column wrap", justifyContent: "start", alignItems: "flex-start", paddingLeft: 16, paddingTop: 16 }}>
|
||||
{
|
||||
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 (
|
||||
<ButtonBase key={this.mapKey++}
|
||||
style={{ width: "320px", flex: "0 0 48px", position: "relative" }}
|
||||
onClick={() => this.onSelect(value)}
|
||||
>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{this.fileNameOnly(value)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<div style={{ display: "flex", width: "100%", flexFlow: "row nowrap" }}>
|
||||
<Button style={{ flex: "0 0 auto" }} startIcon={<FileUploadIcon />}>
|
||||
<input hidden accept="audio/x-wav" name="Upload Impuse File" type="file" multiple />
|
||||
Upload
|
||||
</Button>
|
||||
<IconButton style={{visibility: (this.state.hasSelection? "visible": "hidden")}} aria-label="delete selected file" component="label" color="primary" >
|
||||
<DeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
<Button onClick={() => {this.props.onCancel();}} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{ flex: "0 0 auto" }} onClick={() => {this.props.onOk(this.props.fileProperty,this.state.selectedFile);}} color="secondary" disabled={!this.state.hasSelection} aria-label="select">
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
||||
onClick={()=> this.handleDelete()} >
|
||||
<OldDeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
|
||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||
onClick={()=> { this.setState({openUploadFileDialog: true})}}
|
||||
>
|
||||
<div>Upload</div>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
|
||||
<Button onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{ flex: "0 0 auto" }} onClick={() => { this.props.onOk(this.props.fileProperty, this.state.selectedFile); }} color="secondary" disabled={!this.state.hasSelection} aria-label="select">
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
<UploadFileDialog
|
||||
open={this.state.openUploadFileDialog}
|
||||
onClose=
|
||||
{
|
||||
() => {
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)}
|
||||
onUploaded={()=> { this.requestFiles();}}
|
||||
fileProperty={this.props.fileProperty}
|
||||
|
||||
|
||||
/>
|
||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
||||
text="Are you sure you want to delete the selected file?"
|
||||
okButtonText="Delete"
|
||||
onOk={()=>this.handleConfirmDelete()}
|
||||
onClose={()=>{
|
||||
this.setState({openConfirmDeleteDialog: false});
|
||||
}}
|
||||
|
||||
/>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
+191
-80
@@ -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<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
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 (<img alt="vst" src="/img/vst.svg" style={{marginLeft: 8,height: 22, opacity: 0.6,position: "relative",top: -1}} />)
|
||||
vst3_indicator(uiPlugin?: UiPlugin) {
|
||||
if (uiPlugin?.is_vst3) {
|
||||
return (<img alt="vst" src="/img/vst.svg" style={{ marginLeft: 8, height: 22, opacity: 0.6, position: "relative", top: -1 }} />)
|
||||
}
|
||||
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 (<Fragment />);
|
||||
} else {
|
||||
let stereoIndicator = this.stereo_indicator(uiPlugin)
|
||||
if (uiPlugin.author_name !== "") {
|
||||
if (uiPlugin.author_homepage !== "") {
|
||||
return (<Fragment>
|
||||
<div style={{flex: "0 1 auto"}}>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ",\u00A0"}</Typography>
|
||||
</div>
|
||||
<div style={{flex: "0 1 auto"}}>
|
||||
<a href={uiPlugin.author_homepage} target="_blank" >
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.author_name}</Typography>
|
||||
</a>
|
||||
</div>
|
||||
</Fragment>);
|
||||
|
||||
} else {
|
||||
return (<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ", " + uiPlugin.author_name}</Typography>
|
||||
</Fragment>);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin?.name ?? ""}</Typography>
|
||||
</Fragment>);
|
||||
|
||||
}
|
||||
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 (<div />);
|
||||
}
|
||||
let value = this.gridItems[item];
|
||||
let value = gridItems[item];
|
||||
|
||||
let classes = this.props.classes;
|
||||
let isFavorite: boolean = this.state.favoritesList[value.uri] ?? false;
|
||||
|
||||
return (
|
||||
<div key={value.uri}
|
||||
onDoubleClick={(e) => { this.onDoubleClick(e, value.uri) }}
|
||||
@@ -520,16 +602,19 @@ export const LoadPluginDialog =
|
||||
<PluginIcon pluginType={value.plugin_type} pluginUri={value.uri} size={24} />
|
||||
</div>
|
||||
<div className={classes.content2}>
|
||||
<div className={classes.label} >
|
||||
<Typography color="textPrimary" noWrap sx={{ display: "inline" }} >
|
||||
{value.name}
|
||||
{
|
||||
isFavorite && (
|
||||
<StarBorderIcon sx={{ color: "#C80", fontSize: 16 }} />
|
||||
<div className={classes.label} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }} >
|
||||
|
||||
)
|
||||
}
|
||||
<Typography color="textPrimary" noWrap sx={{ display: "block", flex: "0 1 auto", }} >
|
||||
{value.name}
|
||||
</Typography>
|
||||
{
|
||||
isFavorite && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<StarBorderIcon sx={{ color: "#C80", fontSize: 16, marginRight: "2px" }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
<Typography color="textSecondary" noWrap>
|
||||
{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 =
|
||||
<Dialog
|
||||
onKeyPress={(e) => { 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 =
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 160px",marginRight: 24 }} >
|
||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
||||
|
||||
<TextField select variant="standard"
|
||||
defaultValue={this.state.filterType}
|
||||
key={this.state.filterType}
|
||||
onChange={(e) => { this.onFilterChange(e); }}
|
||||
sx={{ minWidth: 160}}
|
||||
sx={{ minWidth: 160 }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</TextField>
|
||||
@@ -643,22 +747,33 @@ export const LoadPluginDialog =
|
||||
flex: "1 1 100px",
|
||||
}} >
|
||||
<AutoSizer>
|
||||
{(arg: { height: number, width: number }) => {
|
||||
{(arg) => {
|
||||
if ((!arg.width) || (!arg.height)) {
|
||||
return (<div></div>);
|
||||
}
|
||||
let width = arg.width ?? 1;
|
||||
let height = arg.height ?? 1;
|
||||
let gridItems = this.state.gridItems;
|
||||
|
||||
let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); }
|
||||
|
||||
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
width={arg.width}
|
||||
ref={scrollRef}
|
||||
width={width}
|
||||
columnCount={this.gridColumnCount}
|
||||
columnWidth={(arg.width - 40) / this.gridColumnCount}
|
||||
height={arg.height}
|
||||
columnWidth={(width - 40) / this.gridColumnCount}
|
||||
height={height}
|
||||
rowHeight={64}
|
||||
overscanRowCount={10}
|
||||
rowCount={Math.ceil(this.gridItems.length / this.gridColumnCount)}
|
||||
rowCount={Math.ceil(gridItems.length / this.gridColumnCount)}
|
||||
itemKey={(args: { columnIndex: number, data: any, rowIndex: number }) => {
|
||||
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) ? (
|
||||
<DialogActions style={{ flex: "0 0 auto" }} >
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<div style={{ display: "flex", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
{this.info_string(selectedPlugin)}
|
||||
{this.vst3_indicator(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri !== "" ? "block" : "none"),
|
||||
@@ -708,12 +821,10 @@ export const LoadPluginDialog =
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<div style={{ width: 1, height: 48 }} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
{this.info_string(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri ? "block" : "block"),
|
||||
position: "relative", top: -5
|
||||
position: "relative"
|
||||
}}>
|
||||
<IconButton color="inherit" aria-label="Set as favorite"
|
||||
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
|
||||
@@ -722,11 +833,11 @@ export const LoadPluginDialog =
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.bottom} style={{height: 64}}>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120, height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 120, height: 48 }} >SELECT</Button>
|
||||
<Button onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
+72
-22
@@ -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<UiControl> {
|
||||
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<UiControl> {
|
||||
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<UiControl> {
|
||||
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<UiControl> {
|
||||
}
|
||||
|
||||
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<UiControl> {
|
||||
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<UiControl> {
|
||||
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);
|
||||
|
||||
+90
-63
@@ -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 (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<span style={{ whiteSpace: "nowrap", color: "#800000" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8, minWidth: 0,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto",minWidth: 0 }}>
|
||||
<span style={{ color: "#800000" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 1 auto", minWidth: 0,overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto", minWidth: 0,overflow: "hidden",textOverflow: "ellipsis" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
<span className={classes.author}>{author}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
|
||||
<PluginInfoDialog plugin_uri={pluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
|
||||
<PluginInfoDialog plugin_uri={pluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
<div className={classes.separator} />
|
||||
<div className={classes.controlToolBar}>
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center",
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center", minWidth: 0,
|
||||
width: "100%", height: 48, paddingLeft: 16, paddingRight: 16
|
||||
}} >
|
||||
<div style={{ flex: "0 0 auto", width: 80 }} >
|
||||
@@ -432,7 +459,7 @@ export const MainPage =
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canAdd ? "block" : "none", paddingRight: 8 }}>
|
||||
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton onClick={(e) => { this.onAddClick(e) }} size="large">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
@@ -444,11 +471,11 @@ export const MainPage =
|
||||
onClose={() => this.handleAddClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>
|
||||
<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>)}
|
||||
<Divider />
|
||||
<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>
|
||||
<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>)}
|
||||
</Menu>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
||||
@@ -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={<InputIcon />}
|
||||
style={{ borderRadius: 24, paddingLeft: 18, paddingRight: 18, textTransform: "none"}}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
@@ -493,19 +520,19 @@ export const MainPage =
|
||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
{
|
||||
missing ? (
|
||||
<div style={{marginLeft: 100,marginTop: 20}}>
|
||||
<div style={{ marginLeft: 100, marginTop: 20 }}>
|
||||
<Typography variant="body1" paragraph={true}>Error: Plugin is not installed.</Typography>
|
||||
<Typography noWrap variant="body2" paragraph={true}>{pluginUri}</Typography>
|
||||
<Typography noWrap variant="body2" paragraph={true}>{pluginUri}</Typography>
|
||||
</div>
|
||||
|
||||
):
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
)
|
||||
) :
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<MidiBindingsDialog open={this.state.showMidiBindingsDialog}
|
||||
onClose={()=> this.setState({showMidiBindingsDialog: false} ) }
|
||||
onClose={() => this.setState({ showMidiBindingsDialog: false })}
|
||||
/>
|
||||
{
|
||||
(this.state.loadDialogOpen) && (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { createSvgIcon } from '@mui/material/utils';
|
||||
|
||||
const OldDeleteIcon = createSvgIcon(
|
||||
<path d="M20,4h-4.5l-1-1h-5l-1,1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4z M17,19H7V6h10V19z M9 8h2v8h-2z M13,8h2v8h-2z"/>,
|
||||
"Delete",
|
||||
);
|
||||
|
||||
export default OldDeleteIcon;
|
||||
@@ -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<T> {
|
||||
deserialize(input: any): T;
|
||||
}
|
||||
@@ -92,6 +91,10 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
|
||||
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<PedalboardItem> {
|
||||
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<PedalboardItem> {
|
||||
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<Pedalboard> {
|
||||
|
||||
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<Pedalboard> {
|
||||
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<Pedalboard> {
|
||||
}
|
||||
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<Pedalboard> {
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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((
|
||||
<path d={svgPath} stroke={color} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke={color} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={svgPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (numberOfOutputs === 1) {
|
||||
output.push((
|
||||
<path d={svgPath} stroke={color} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke={color} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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((<path d={topStartPath} stroke={topColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path d={topStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke={topColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
} else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) {
|
||||
output.push((<path d={topStartPath} stroke={topColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke={topColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
}
|
||||
|
||||
if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) {
|
||||
output.push((<path d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path d={bottomStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
|
||||
} else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) {
|
||||
output.push((<path d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
}
|
||||
|
||||
let lastTop = item.topChildren[item.topChildren.length - 1];
|
||||
@@ -813,26 +817,26 @@ const PedalboardView =
|
||||
// display stereo strokes with cutoff line.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!firstPathAbsent) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!secondPathAbsent) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
|
||||
@@ -840,32 +844,32 @@ const PedalboardView =
|
||||
// stereo strokes merge.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!firstPathAbsent) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!secondPathAbsent) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
|
||||
// draw stereo inner lines.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
|
||||
}
|
||||
@@ -874,10 +878,10 @@ const PedalboardView =
|
||||
{
|
||||
// stereo output of L/R splitter
|
||||
output.push((
|
||||
<path d={thirdPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={thirdPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={thirdPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={thirdPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={classes.iconFrame} onContextMenu={(e) => { e.preventDefault(); }}>
|
||||
<div className={hasBorder? classes.iconFrame : classes.borderlessIconFrame} onContextMenu={(e) => { e.preventDefault(); }}>
|
||||
|
||||
<ButtonBase style={{ width: "100%", height: "100%" }}
|
||||
onClick={(e) => { this.onItemClick(e, instanceId); }}
|
||||
onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }}
|
||||
onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }}
|
||||
>
|
||||
<SelectHoverBackground selected={instanceId === this.props.selectedId} showHover={true} />
|
||||
<Draggable draggable={draggable} getScrollContainer={() => this.getScrollContainer()}
|
||||
onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<SelectHoverBackground selected={instanceId === this.props.selectedId} showHover={true} borderRadius={6} >
|
||||
<Draggable draggable={draggable} getScrollContainer={() => this.getScrollContainer()}
|
||||
onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }}
|
||||
>
|
||||
<img src={iconUrl} className={classes.pedalIcon} alt="Pedal" draggable={false}
|
||||
style={{opacity: enabled? 0.99: 0.6}} />
|
||||
</div>
|
||||
</Draggable>
|
||||
style={{opacity: enabled? 0.99: 0.6}} />
|
||||
</Draggable>
|
||||
</SelectHoverBackground>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
);
|
||||
@@ -945,6 +948,7 @@ const PedalboardView =
|
||||
let outputs: ReactNode[] = [];
|
||||
this.renderConnectors(outputs, layoutChain, true, false);
|
||||
return (
|
||||
<div style={{width: layoutSize.width, height: layoutSize.height, overflow: "hidden"}}>
|
||||
<svg width={layoutSize.width} height={layoutSize.height}
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox={"0 0 " + layoutSize.width + " " + layoutSize.height}>
|
||||
<g fill="none">
|
||||
@@ -954,6 +958,7 @@ const PedalboardView =
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -973,19 +978,25 @@ const PedalboardView =
|
||||
let item = v.value;
|
||||
switch (item.uri) {
|
||||
case START_PEDALBOARD_ITEM_URI:
|
||||
result.push(<div className={classes.startItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
<img src={item.iconUrl} className={classes.pedalIcon} alt="" draggable={false} />
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
|
||||
{this.pedalButton(START_CONTROL, item.iconUrl, false,true,false)}
|
||||
</div>
|
||||
</div>);
|
||||
break;
|
||||
case END_PEDALBOARD_ITEM_URI:
|
||||
result.push(<div className={classes.endItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
<img src={item.iconUrl} className={classes.pedalIcon} alt="" draggable={false} />
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
|
||||
{this.pedalButton(END_CONTROL, item.iconUrl, false,true,false)}
|
||||
</div>
|
||||
</div>);
|
||||
break;
|
||||
default:
|
||||
if (item.isSplitter()) {
|
||||
|
||||
result.push(<div className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
{this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false,true)}
|
||||
</div>
|
||||
@@ -993,7 +1004,7 @@ const PedalboardView =
|
||||
|
||||
} else {
|
||||
result.push(
|
||||
<div style={{
|
||||
<div key={this.renderKey++} style={{
|
||||
display: "flex", justifyContent: "flex-start", alignItems: "flex-start",
|
||||
position: "absolute", left: item.bounds.x, width: CELL_WIDTH, top: item.bounds.bottom - 12, paddingLeft: 2, paddingRight: 2
|
||||
}}>
|
||||
@@ -1002,7 +1013,7 @@ const PedalboardView =
|
||||
>{item.pedalItem?.pluginName}</Typography>
|
||||
</div>
|
||||
)
|
||||
result.push(<div className={classes.pedalItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
result.push(<div key={this.renderKey++} className={classes.pedalItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
{this.pedalButton(item.pedalItem?.instanceId ?? -1, item.iconUrl, !item.isEmpty(), item.pedalItem?.isEnabled ?? false)}
|
||||
|
||||
</div>);
|
||||
@@ -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();
|
||||
|
||||
+182
-151
@@ -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<string>;
|
||||
// alertMessage: ObservableProperty<string>;
|
||||
// showStatusMonitor: ObservableProperty<boolean>;
|
||||
// setShowStatusMonitor(show: boolean): void;
|
||||
// state: ObservableProperty<State>;
|
||||
// visibilityState: ObservableProperty<VisibilityState>;
|
||||
|
||||
// ui_plugins: ObservableProperty<UiPlugin[]>;
|
||||
// plugin_classes: ObservableProperty<PluginClass>;
|
||||
// jackConfiguration: ObservableProperty<JackConfiguration>;
|
||||
// jackSettings: ObservableProperty<JackChannelSelection>;
|
||||
// banks: ObservableProperty<BankIndex>;
|
||||
// jackServerSettings: ObservableProperty<JackServerSettings>;
|
||||
// wifiConfigSettings: ObservableProperty<WifiConfigSettings>;
|
||||
// wifiDirectConfigSettings: ObservableProperty<WifiDirectConfigSettings>;
|
||||
// governorSettings: ObservableProperty<GovernorSettings>;
|
||||
// favorites: ObservableProperty<FavoritesList>;
|
||||
|
||||
// presets: ObservableProperty<PresetIndex>;
|
||||
// systemMidiBindings: ObservableProperty<MidiBinding[]>;
|
||||
|
||||
|
||||
// zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined>;
|
||||
|
||||
// setJackServerSettings(jackServerSettings: JackServerSettings): void;
|
||||
// setMidiBinding(instanceId: number, midiBinding: MidiBinding): void;
|
||||
|
||||
|
||||
// getUiPlugin(uri: string): UiPlugin | null;
|
||||
|
||||
// pedalboard: ObservableProperty<Pedalboard>;
|
||||
// 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<void>;
|
||||
// deletePresetItem(instanceId: number): Promise<number>;
|
||||
// renamePresetItem(instanceId: number, name: string): Promise<void>;
|
||||
// copyPreset(fromId: number, toId: number): Promise<number>;
|
||||
// duplicatePreset(instanceId: number): Promise<number>;
|
||||
|
||||
// moveBank(from: number, to: number): Promise<void>;
|
||||
// deleteBankItem(instanceId: number): Promise<number>;
|
||||
|
||||
|
||||
// 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<Type = any>(instanceId: number, uri: string): Promise<Type>;
|
||||
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>;
|
||||
|
||||
// renameBank(instanceId: number, newName: string): Promise<void>;
|
||||
// saveBankAs(instanceId: number, newName: string): Promise<number>;
|
||||
// openBank(bankId: number): Promise<void>;
|
||||
|
||||
// getJackStatus(): Promise<JackHostStatus>;
|
||||
|
||||
// getPluginPresets(uri: string): Promise<PluginUiPresets>;
|
||||
// loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void;
|
||||
// updatePluginPresets(uri: string, presets: PluginUiPresets): Promise<void>;
|
||||
// duplicatePluginPreset(uri: string, instanceId: number): Promise<number>;
|
||||
|
||||
// setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void;
|
||||
|
||||
// shutdown(): Promise<void>;
|
||||
// restart(): Promise<void>;
|
||||
|
||||
// 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<number>;
|
||||
// uploadBank(file: File, uploadAfter: number): Promise<number>;
|
||||
|
||||
// setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void>;
|
||||
// setWifiDirectConfigSettings(wifiDirectConfigSettings: WifiDirectConfigSettings): Promise<void>;
|
||||
// setGovernorSettings(governor: string): Promise<void>;
|
||||
|
||||
// getWifiChannels(countryIso3661: string): Promise<WifiChannel[]>;
|
||||
|
||||
// getAlsaDevices(): Promise<AlsaDeviceInfo[]>;
|
||||
|
||||
// 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<string[]>;
|
||||
// };
|
||||
|
||||
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<boolean> {
|
||||
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<string[]>('requestFileList', piPedalFileProperty);
|
||||
}
|
||||
|
||||
deleteUserFile(fileName: string) : Promise<boolean>
|
||||
{
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName)
|
||||
}
|
||||
|
||||
|
||||
saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): Promise<number> {
|
||||
// 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<string> {
|
||||
let result = new Promise<string>((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<number> {
|
||||
let result = new Promise<number>((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<number>((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<number>
|
||||
{
|
||||
return nullCast(this.webSocket).request<number>("newPreset");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<PluginOutputControl instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
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 (<div className={classes.frame} ></div>);
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof styles> {
|
||||
uiControl: UiControl;
|
||||
instanceId: number;
|
||||
}
|
||||
type PluginOutputControlState = {
|
||||
hasValue: boolean;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const PluginOutputControl =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<PluginOutputControlProps, PluginOutputControlState>
|
||||
{
|
||||
|
||||
private model: PiPedalModel;
|
||||
private vuRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
|
||||
constructor(props: PluginOutputControlProps) {
|
||||
super(props);
|
||||
this.vuRef = React.createRef<HTMLDivElement>();
|
||||
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 (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: item_width, margin: 8, height: 98 }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8, marginLeft: 0, marginRight: 0 }}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> {control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div style={{ flex: "1 1 auto", display: "flex", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<div style={{ width: 6, height: 48, background: "#000", }}>
|
||||
<div style={{ height: 46, overflow: "hidden", position: "absolute", margin: 1 }}>
|
||||
<div ref={this.vuRef} style={{ width: 4, height: 44, background: "#0C0", }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div style={{ flex: "0 0 auto", position: "relative", width: 30, height: 27 }}>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: item_width, margin: 8, height: 98 }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8, marginLeft: 0, marginRight: 0 }}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "left"
|
||||
}}> {control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div style={{ flex: "1 1 auto", display: "flex", justifyContent: "start", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<Typography variant="caption" display="block" noWrap style={{ width: "100%" }}>
|
||||
{text}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div style={{ flex: "0 0 auto", position: "relative", width: 40, height: 27 }}>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default PluginOutputControl;
|
||||
@@ -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 })(
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title="Upload Plugin Presets"
|
||||
extension='.piPluginPresets'
|
||||
uploadPage='uploadPluginPresets'
|
||||
onUploaded={(instanceId) => {} }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadDialog}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
open={this.state.openUploadPresetDialog}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
|
||||
@@ -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 })(
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => 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 }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
|
||||
@@ -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<typeof styles> {
|
||||
@@ -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 (
|
||||
<div style={{ marginLeft: 12, display: "flex", flexDirection: "row",
|
||||
justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative" }}>
|
||||
<div style={{
|
||||
marginLeft: 12, display: "flex", flexDirection: "row",
|
||||
justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative"
|
||||
}}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0 }}
|
||||
onClick={(e) => { this.handleSave(); }}
|
||||
size="large">
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: "1 1 auto", minWidth: 60, maxWidth: 300, position: "relative", paddingRight: 12 }} >
|
||||
{true ? (
|
||||
<Select variant="standard"
|
||||
className={classes.select}
|
||||
style={{ width: "100%", position: "relative", top: 0}} disabled={!this.state.enabled}
|
||||
style={{ width: "100%", position: "relative", top: 0 }} disabled={!this.state.enabled}
|
||||
onChange={(e, extra) => this.handleChange(e, extra)}
|
||||
onClose={(e) => this.handleSelectClose(e)}
|
||||
displayEmpty
|
||||
@@ -331,14 +358,6 @@ const PresetSelector =
|
||||
</ButtonBase>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0 }}
|
||||
onClick={(e) => { this.handleSave(); }}
|
||||
size="large">
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto" }}
|
||||
@@ -356,6 +375,7 @@ const PresetSelector =
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
|
||||
<MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem>
|
||||
@@ -369,14 +389,14 @@ const PresetSelector =
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
|
||||
open={this.state.openUploadDialog}
|
||||
open={this.state.openUploadPresetDialog}
|
||||
uploadAfter={-1}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 { Component, SyntheticEvent } from 'react';
|
||||
import React,{ Component, SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
|
||||
@@ -54,11 +54,12 @@ const styles = ({ palette }: Theme) => createStyles({
|
||||
interface HoverProps extends WithStyles<typeof styles> {
|
||||
selected: boolean;
|
||||
showHover?: boolean;
|
||||
borderRadius?: number;
|
||||
theme: Theme;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface HoverState {
|
||||
hovering: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,44 +70,53 @@ export const SelectHoverBackground =
|
||||
constructor(props: HoverProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hovering: false
|
||||
};
|
||||
this.selfRef = React.createRef<HTMLDivElement>();
|
||||
this.hoverElementRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
}
|
||||
|
||||
handleMouseEnter(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(true);
|
||||
handleMouseOver(e: SyntheticEvent): void {
|
||||
console.log("onMouseOver")
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "block";
|
||||
} else {
|
||||
console.log("No hoverElementRef")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseLeave(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
setHovering(value: boolean): void {
|
||||
if (this.state.hovering !== value) {
|
||||
this.setState({ hovering: value });
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "none";
|
||||
} else {
|
||||
console.log("No hoverElementRef")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private selfRef: React.RefObject<HTMLDivElement>;
|
||||
private hoverElementRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let isSelected = this.props.selected;
|
||||
let hovering = this.state.hovering && (this.props.showHover??true);
|
||||
return (<div className={classes.frame}
|
||||
onMouseEnter={(e) => { this.handleMouseEnter(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
return (<div className={classes.frame} ref={this.selfRef}
|
||||
onMouseOver={(e) => { this.handleMouseOver(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
>
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none") }} />
|
||||
<div className={classes.hover} style={{ display: (hovering ? "block" : "none") }} />
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none"), borderRadius: this.props.borderRadius }} />
|
||||
<div ref={this.hoverElementRef} className={classes.hover} style={{ display: "none", borderRadius: this.props.borderRadius }} />
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
// 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
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalFileProperty } from './Lv2Plugin';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
|
||||
// const PRESET_EXTENSION = ".piPreset";
|
||||
// const BANK_EXTENSION = ".piBank";
|
||||
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
|
||||
|
||||
export interface UploadFileDialogProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onUploaded: (fileName: string) => void,
|
||||
uploadPage: string,
|
||||
fileProperty: PiPedalFileProperty
|
||||
|
||||
};
|
||||
|
||||
enum FileUploadStatus {
|
||||
Ready,
|
||||
Uploading,
|
||||
Uploaded,
|
||||
Error,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
class FileUploadEntry {
|
||||
constructor(file: File) {
|
||||
this.id = nextId++;
|
||||
this.file = file;
|
||||
this.abortController = undefined;
|
||||
this.status = FileUploadStatus.Ready;
|
||||
this.statusMessage = "";
|
||||
}
|
||||
status: FileUploadStatus
|
||||
statusMessage: string;
|
||||
file: File;
|
||||
abortController?: AbortController;
|
||||
id: number;
|
||||
}
|
||||
|
||||
class FileUploadDisplayEntry {
|
||||
constructor(fileUploadEntry: FileUploadEntry) {
|
||||
this.name = fileUploadEntry.file.name;
|
||||
this.status = fileUploadEntry.status;
|
||||
this.statusMessage = fileUploadEntry.statusMessage;
|
||||
this.id = fileUploadEntry.id;
|
||||
}
|
||||
name: string;
|
||||
status: FileUploadStatus;
|
||||
statusMessage: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export interface UploadFileDialogState {
|
||||
fullScreen: boolean;
|
||||
okEnabled: boolean;
|
||||
files: FileUploadDisplayEntry[];
|
||||
};
|
||||
|
||||
export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFileDialogProps, UploadFileDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: UploadFileDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false,
|
||||
okEnabled: true,
|
||||
files: []
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<UploadFileDialogProps>, prevState: Readonly<UploadFileDialogState>, snapshot?: any): void {
|
||||
if (prevProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
this.uploadList = [];
|
||||
this.setState({ files: [], okEnabled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClose(): void {
|
||||
this.uploadList = [];
|
||||
this.setState({ files: [] });
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
for (let upload of this.uploadList) {
|
||||
if (upload.status === FileUploadStatus.Ready || upload.status === FileUploadStatus.Uploading) {
|
||||
upload.status = FileUploadStatus.Cancelled;
|
||||
upload.statusMessage = "Cancelled.";
|
||||
}
|
||||
if (upload.abortController) {
|
||||
upload.abortController.abort();
|
||||
upload.abortController = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
this.handleClose();
|
||||
}
|
||||
handleOk(): void {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
updateUploadDisplayList() {
|
||||
let result: FileUploadDisplayEntry[] = [];
|
||||
let uploadsPending = false;
|
||||
for (let upload of this.uploadList) {
|
||||
result.push(new FileUploadDisplayEntry(upload));
|
||||
if (upload.status === FileUploadStatus.Ready || upload.status === FileUploadStatus.Uploading) {
|
||||
uploadsPending = true;
|
||||
}
|
||||
|
||||
}
|
||||
this.setState({ files: result, okEnabled: !uploadsPending });
|
||||
}
|
||||
|
||||
private uploading = false;
|
||||
private wantsScrollTo = false;
|
||||
|
||||
private uploadList: FileUploadEntry[] = [];
|
||||
|
||||
async uploadFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
let file = files.item(i);
|
||||
if (file) {
|
||||
this.uploadList.push(new FileUploadEntry(file));
|
||||
}
|
||||
}
|
||||
this.wantsScrollTo = true;
|
||||
if (this.uploading) {
|
||||
this.updateUploadDisplayList();
|
||||
return;
|
||||
}
|
||||
this.uploading = true;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < this.uploadList.length; ++i) {
|
||||
let upload = this.uploadList[i];
|
||||
if (upload.status !== FileUploadStatus.Ready) {
|
||||
continue;
|
||||
}
|
||||
upload.status = FileUploadStatus.Uploading;
|
||||
upload.statusMessage = "Uploading...";
|
||||
this.wantsScrollTo = true;
|
||||
this.updateUploadDisplayList();
|
||||
try {
|
||||
upload.abortController = new AbortController();
|
||||
let filename = await this.model.uploadFile(this.props.uploadPage, this.uploadList[i].file, "application/octet-stream", upload.abortController);
|
||||
this.props.onUploaded(filename);
|
||||
upload.status = FileUploadStatus.Uploaded;
|
||||
upload.statusMessage = "Uploaded.";
|
||||
upload.abortController = undefined;
|
||||
} catch (error: any) {
|
||||
// @ts-ignore: TS2367 // No overlap between FileUploadStatus.Updating and FileUploadStatus.Cancelled
|
||||
if (upload.status !== FileUploadStatus.Cancelled) {
|
||||
upload.status = FileUploadStatus.Error;
|
||||
if (error instanceof Error) {
|
||||
upload.statusMessage = (error as Error).message;
|
||||
} else if (typeof error === "string") {
|
||||
upload.statusMessage = error as string;
|
||||
} else {
|
||||
throw new Error("Unrecgnized exception type: " + error.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.model.showAlert(error + "");
|
||||
};
|
||||
this.updateUploadDisplayList();
|
||||
this.uploading = false;
|
||||
}
|
||||
handleDrop(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
e.currentTarget.style.background = "";
|
||||
|
||||
if (this.wantsTransfer(e.dataTransfer)) {
|
||||
this.uploadFiles(e.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
|
||||
private wantsFile(file: File | null) {
|
||||
if (file === null) return false;
|
||||
return this.props.fileProperty.wantsFile(file.name);
|
||||
|
||||
}
|
||||
private wantsTransfer(fileList: DataTransfer): boolean {
|
||||
if (fileList.files.length === 0) return false;
|
||||
|
||||
console.log("File count: " + fileList.files.length);
|
||||
for (let i = 0; i < fileList.files.length; ++i) {
|
||||
let file = fileList.files[i];
|
||||
if (this.wantsFile(file)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
handleDragOver(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
e.currentTarget.style.background = "#D8D8D8";
|
||||
}
|
||||
|
||||
handleButtonSelect(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.currentTarget.files) {
|
||||
this.uploadFiles(e.currentTarget.files);
|
||||
}
|
||||
}
|
||||
hasFileList() {
|
||||
return this.state.files.length !== 0;
|
||||
}
|
||||
|
||||
getIcon(status: FileUploadStatus) {
|
||||
let size = 14;
|
||||
|
||||
let style = { width: size, height: size, opacity: 0.75 };
|
||||
let top = 0;
|
||||
|
||||
switch (status) {
|
||||
case FileUploadStatus.Error:
|
||||
return (<div style={{ position: "relative", top: top }}><ErrorIcon color="error" style={style} /></div>);
|
||||
case FileUploadStatus.Uploaded:
|
||||
return (<div style={{ position: "relative", top: top }}><CheckCircleOutlineIcon color="success" style={style}/></div >);
|
||||
case FileUploadStatus.Uploading:
|
||||
return (<CircularProgress size={size } style={{ margin: 2 }} />);
|
||||
|
||||
default:
|
||||
return (<SvgIcon style={{ width: size, height: size, }} />);
|
||||
}
|
||||
}
|
||||
makeBrowserAcceptList() {
|
||||
let fileTypes = this.props.fileProperty.fileTypes;
|
||||
let result = "";
|
||||
for (let i = 0; i < fileTypes.length; ++i) {
|
||||
if (result.length !== 0) {
|
||||
result += ',';
|
||||
}
|
||||
result += fileTypes[i].fileExtension;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="UploadFileDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.handleCancel(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >Upload</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ paddingBottom: 0 }}>
|
||||
<div style={{
|
||||
width: "100%", height: 140, marginBottom: 0,
|
||||
border: "2px dashed #CCC",
|
||||
borderRadius: "10px",
|
||||
fontFamily: "Roboto",
|
||||
padding: 0
|
||||
}}
|
||||
onDragEnter={(e) => { this.handleDragOver(e); }}
|
||||
onDragLeave={(e) => { e.currentTarget.style.background = ""; e.preventDefault(); }}
|
||||
onDragOver={(e) => { this.handleDragOver(e); }}
|
||||
onDrop={(e) => {
|
||||
this.handleDrop(e);
|
||||
}}
|
||||
>
|
||||
{!this.hasFileList() && (
|
||||
<Typography noWrap color="textSecondary" align="center" display="block" variant="caption" style={{ width: "100%", marginTop: 20, verticalAlign: "middle" }} >Drop files here</Typography>
|
||||
)}
|
||||
{this.hasFileList() && (
|
||||
<div style={{ width: "100%", height: "100%", overflow: "auto" }}>
|
||||
<table style={{ margin: 8, }}>
|
||||
{
|
||||
this.state.files.map(
|
||||
(upload) => {
|
||||
let myRef: ((arg: HTMLDivElement) => void) | undefined = undefined;
|
||||
if (this.wantsScrollTo && upload.status === FileUploadStatus.Uploading) {
|
||||
myRef = (refElement: HTMLDivElement | null) => {
|
||||
if (refElement && this.wantsScrollTo) {
|
||||
let options: ScrollIntoViewOptions = {
|
||||
block: "center"
|
||||
};
|
||||
refElement.scrollIntoView(options);
|
||||
this.wantsScrollTo = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<div ref={myRef} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
|
||||
{this.getIcon(upload.status)}
|
||||
<Typography variant="caption" noWrap display="block" style={{ marginLeft: 4 }}> {upload.name} </Typography>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<Typography variant="caption" noWrap display="block" style={{ marginLeft: 8 }}> {upload.statusMessage} </Typography>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</DialogContent >
|
||||
|
||||
<DialogActions >
|
||||
<Button
|
||||
component="label" color="primary" style={{ marginLeft: 16, width: 120, flex: "0 0 auto" }}
|
||||
>
|
||||
Select Files
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept={this.makeBrowserAcceptList()}
|
||||
multiple
|
||||
onChange={(e) => this.handleButtonSelect(e)}
|
||||
/>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 20px" }} />
|
||||
{
|
||||
this.state.okEnabled ?
|
||||
(
|
||||
<Button onClick={() => this.handleClose()} color="secondary">
|
||||
OK
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Button onClick={() => this.handleCancel()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
)
|
||||
}
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<UploadDialogProps, UploadDialogState> {
|
||||
export default class UploadPresetDialog extends ResizeResponsiveComponent<UploadPresetDialogProps, UploadPresetDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: UploadDialogProps) {
|
||||
constructor(props: UploadPresetDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
@@ -157,7 +157,7 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="UploadDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
<DialogEx tag="UploadPresetDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{userSelect: "none"}}
|
||||
>
|
||||
+1
-1
@@ -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
|
||||
|
||||
+62
-15
@@ -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<int64_t> &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<Lv2PluginState*>(&pedalboardItem.lv2State()));
|
||||
try {
|
||||
effect->GetLv2State(const_cast<Lv2PluginState*>(&pedalboardItem.lv2State()));
|
||||
} catch(const std::exception &e)
|
||||
{
|
||||
Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -207,7 +207,9 @@ public:
|
||||
virtual void SetPedalboard(const std::shared_ptr<Lv2Pedalboard> &pedalboard) = 0;
|
||||
|
||||
virtual void SetControlValue(uint64_t instanceId,const std::string&symbol, float value) = 0;
|
||||
virtual void SetPluginPreset(uint64_t instanceId, const std::vector<ControlValue> & values) = 0;
|
||||
virtual void SetInputVolume(float value) = 0;
|
||||
virtual void SetOutputVolume(float value) = 0;
|
||||
virtual void SetPluginPreset(uint64_t instanceId, const std::vector<ControlValue> &values) = 0;
|
||||
virtual void SetBypass(uint64_t instanceId, bool enabled) = 0;
|
||||
|
||||
virtual bool IsOpen() const = 0;
|
||||
|
||||
@@ -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)
|
||||
|
||||
+76
-7
@@ -37,9 +37,9 @@ namespace pipedal
|
||||
{
|
||||
}
|
||||
AutoLilvNode(const LilvNode *node)
|
||||
: node(const_cast<LilvNode*>(node))
|
||||
: node(const_cast<LilvNode *>(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;
|
||||
};
|
||||
};
|
||||
|
||||
class AutoLilvNodes
|
||||
{
|
||||
|
||||
public:
|
||||
AutoLilvNodes()
|
||||
{
|
||||
}
|
||||
AutoLilvNodes(const LilvNodes *nodes)
|
||||
: nodes(const_cast<LilvNodes *>(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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
DbDezipper.hpp DbDezipper.cpp
|
||||
WebServerLog.hpp
|
||||
|
||||
util.hpp util.cpp
|
||||
PiPedalUI.hpp
|
||||
PiPedalUI.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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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_; }
|
||||
|
||||
+26
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace pipedal
|
||||
class Lv2Effect : public IEffect
|
||||
{
|
||||
private:
|
||||
|
||||
std::unique_ptr<StateInterface> stateInterface;
|
||||
void RestoreState(const PedalboardItem&pedalboardItem);
|
||||
|
||||
|
||||
+73
-29
@@ -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<float>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <functional>
|
||||
#include <lv2/urid.lv2/urid.h>
|
||||
#include <functional>
|
||||
#include "DbDezipper.hpp"
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
@@ -37,6 +38,9 @@ class RealtimeRingBufferWriter;
|
||||
class Lv2Pedalboard {
|
||||
IHost *pHost = nullptr;
|
||||
|
||||
DbDezipper inputVolume;
|
||||
DbDezipper outputVolume;
|
||||
|
||||
BufferPool bufferPool;
|
||||
std::vector<float*> pedalboardInputBuffers;
|
||||
std::vector<float*> 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);
|
||||
|
||||
|
||||
+2
-13
@@ -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()
|
||||
|
||||
+8
-1
@@ -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<PedalboardItem> 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);
|
||||
|
||||
+137
-25
@@ -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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::string, float> 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<ControlValue> 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<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
|
||||
return std::vector<std::string>(); // don't disclose to users what the problem is.
|
||||
}
|
||||
}
|
||||
|
||||
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
|
||||
|
||||
return storage.CreateNewPreset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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<std::string, bool> &favorites);
|
||||
|
||||
std::vector<std::string> 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.
|
||||
+152
-39
@@ -31,6 +31,7 @@
|
||||
#include <atomic>
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include "Promise.hpp"
|
||||
#include <mutex>
|
||||
|
||||
#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<float>::max();
|
||||
float lastValue = INVALID_VALUE;
|
||||
float currentValue = INVALID_VALUE;
|
||||
|
||||
bool pendingValue = false;
|
||||
bool waitingForAck = false;
|
||||
};
|
||||
|
||||
std::vector<PortMonitorSubscription> activePortMonitors;
|
||||
std::mutex activePortMonitorsMutex;
|
||||
std::vector<std::shared_ptr<PortMonitorSubscription>> 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<PortMonitorSubscription> getPortMonitorSubscription(uint64_t subscriptionId)
|
||||
{
|
||||
for (int i = 0; i < this->activePortMonitors.size(); ++i)
|
||||
std::shared_ptr<PortMonitorSubscription> 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<PortMonitorSubscription> &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<PortMonitorSubscription> &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<PortMonitorSubscription> 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<PortMonitorSubscription> 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<PortMonitorSubscription>(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<std::recursive_mutex> 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<std::string> 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
|
||||
{
|
||||
|
||||
+70
-18
@@ -39,7 +39,7 @@ PiPedalUI::PiPedalUI(PluginHost *pHost, const LilvNode *uiNode, const std::files
|
||||
try
|
||||
{
|
||||
UiFileProperty::ptr fileUI = std::make_shared<UiFileProperty>(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<UiFileProperty::ptr> &&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)
|
||||
|
||||
+10
-4
@@ -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<PiPedalFileType> 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<PiPedalFileType> fileTypes_;
|
||||
std::string patchProperty_;
|
||||
@@ -101,9 +102,11 @@ namespace pipedal {
|
||||
using ptr = std::shared_ptr<UiFileProperty>;
|
||||
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<PiPedalFileType> &fileTypes() const { return fileTypes_; }
|
||||
@@ -120,9 +123,12 @@ namespace pipedal {
|
||||
public:
|
||||
using ptr = std::shared_ptr<PiPedalUI>;
|
||||
PiPedalUI(PluginHost*pHost, const LilvNode*uiNode, const std::filesystem::path&resourcePath);
|
||||
|
||||
PiPedalUI(std::vector<UiFileProperty::ptr> &&fileProperites);
|
||||
|
||||
const std::vector<UiFileProperty::ptr>& fileProperties() const
|
||||
{
|
||||
return fileProperites_;
|
||||
return fileProperties_;
|
||||
}
|
||||
|
||||
const std::vector<UiPortNotification::ptr> &portNotifications() const { return portNotifications_; }
|
||||
@@ -140,7 +146,7 @@ namespace pipedal {
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<UiFileProperty::ptr> fileProperites_;
|
||||
std::vector<UiFileProperty::ptr> fileProperties_;
|
||||
std::vector<UiPortNotification::ptr> portNotifications_;
|
||||
};
|
||||
|
||||
|
||||
+245
-109
@@ -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<std::string> 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<Lv2PortInfo> &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<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host,const LilvPlugin*pPlugin)
|
||||
{
|
||||
// example:
|
||||
|
||||
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
||||
// a lv2:Parameter;
|
||||
// rdfs:label "Model";
|
||||
// rdfs:range atom:Path.
|
||||
// ...
|
||||
// patch:writable <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>;
|
||||
|
||||
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<UiFileProperty::ptr> 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<UiFileProperty>(
|
||||
strLabel,propertyUri.AsUri(),lv2DirectoryName)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (fileProperties.size() != 0)
|
||||
{
|
||||
return std::make_shared<PiPedalUI>(std::move(fileProperties));
|
||||
}
|
||||
|
||||
return std::shared_ptr<PiPedalUI>();
|
||||
|
||||
}
|
||||
|
||||
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<PiPedalUI>(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<std::string> 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<Lv2PluginClass> PluginHost::GetLv2PluginClass() const
|
||||
{
|
||||
return this->GetPluginClass(LV2_PLUGIN);
|
||||
return this->GetPluginClass(LV2_CORE__Plugin);
|
||||
}
|
||||
|
||||
std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri) const
|
||||
@@ -1078,7 +1211,7 @@ std::vector<ControlValue> 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> 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> Lv2PluginUiPortGroup::jmap{{
|
||||
MAP_REF(Lv2PluginUiPortGroup, program_list_id),
|
||||
}};
|
||||
|
||||
json_map::storage_type<Lv2PluginUiControlPort> 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> 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),
|
||||
|
||||
}};
|
||||
|
||||
|
||||
+57
-26
@@ -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<PiPedalUI> 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<Lv2PluginUiPortGroup> 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<Lv2ScalePoint> 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<Lv2PluginUiControlPort> jmap;
|
||||
static json_map::storage_type<Lv2PluginUiPort> jmap;
|
||||
};
|
||||
|
||||
class Lv2PluginUiInfo
|
||||
@@ -557,7 +577,7 @@ namespace pipedal
|
||||
std::string description_;
|
||||
bool is_vst3_ = false;
|
||||
|
||||
std::vector<Lv2PluginUiControlPort> controls_;
|
||||
std::vector<Lv2PluginUiPort> controls_;
|
||||
std::vector<Lv2PluginUiPortGroup> port_groups_;
|
||||
std::vector<UiFileProperty::ptr> fileProperties_;
|
||||
std::vector<UiPortNotification::ptr> 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;
|
||||
|
||||
|
||||
+19
-8
@@ -25,6 +25,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#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<std::string,float> && controlValues)
|
||||
: instanceId_(instanceId),
|
||||
label_(std::forward<std::string>(label)),
|
||||
controlValues_(std::forward<std::map<std::string,float>>(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<std::string,float> & controlValues)
|
||||
PluginPreset(
|
||||
uint64_t instanceId,
|
||||
const std::string&label,
|
||||
const std::map<std::string,
|
||||
float> & controlValues,
|
||||
const Lv2PluginState &state)
|
||||
: instanceId_(instanceId),
|
||||
label_(label),
|
||||
controlValues_(controlValues)
|
||||
controlValues_((controlValues)),
|
||||
state_(state)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+43
-22
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(']');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ namespace pipedal
|
||||
class Lv2PluginState: public JsonSerializable
|
||||
{
|
||||
public:
|
||||
bool isValid_ = false;
|
||||
std::map<std::string,Lv2PluginStateEntry> values_;
|
||||
void Erase() {
|
||||
isValid_ = false;
|
||||
values_.clear();
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
private:
|
||||
|
||||
+164
-8
@@ -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<ControlValue> 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<ControlValue> 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<std::string, float> &values)
|
||||
float inputVolume,
|
||||
float outputVolume,
|
||||
const std::map<std::string, float> &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<std::string> 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<std::string> 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)
|
||||
|
||||
+29
-4
@@ -53,6 +53,12 @@ public:
|
||||
};
|
||||
|
||||
|
||||
struct PluginPresetValues {
|
||||
|
||||
std::vector<ControlValue> 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<std::string> 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<ControlValue> GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId);
|
||||
uint64_t SavePluginPreset(const std::string&pluginUri, const std::string&name, const std::map<std::string,float> & 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<std::string, float> &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<MidiBinding>&bindings);
|
||||
std::vector<MidiBinding> 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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
+45
-10
@@ -36,32 +36,46 @@ Units UriToUnits(const std::string &uri);
|
||||
|
||||
std::map<Units,std::string> 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<std::string,Units> 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<std::string,Units> unitMap = {
|
||||
|
||||
|
||||
std::map<std::string,Units> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+8
-1
@@ -44,7 +44,14 @@ enum class Units {
|
||||
pc,
|
||||
s,
|
||||
semitone12TET,
|
||||
custom
|
||||
custom,
|
||||
degree,
|
||||
coef,
|
||||
frame,
|
||||
inch,
|
||||
mile,
|
||||
mm,
|
||||
oct,
|
||||
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+92
-35
@@ -27,6 +27,8 @@
|
||||
|
||||
#include <websocketpp/server.hpp>
|
||||
|
||||
#include "WebServerLog.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace std;
|
||||
|
||||
@@ -37,6 +39,34 @@ using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
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_config>
|
||||
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<websocketpp::config::asio> server;
|
||||
typedef websocketpp::server<CustomPpConfig> 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>;
|
||||
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<WebSocketSession::ptr, std::owner_less<WebSocketSession::ptr>> 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 << "<!doctype html><html><head>"
|
||||
<< "<title>Error 404 (Resource not found)</title><body>"
|
||||
<< "<h1>Error 404</h1>"
|
||||
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
||||
<< "</body></head></html>";
|
||||
ss << "<!doctype html><html><head>"
|
||||
<< "<title>Error 404 (Resource not found)</title><body>"
|
||||
<< "<h1>Error 404</h1>"
|
||||
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
||||
<< "</body></head></html>";
|
||||
|
||||
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 << "<!doctype html><html><head>"
|
||||
<< "<title>Error 500 (Server error)</title><body>"
|
||||
<< "<h1>Error 500</h1>"
|
||||
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
||||
<< "</body></head></html>";
|
||||
std::string body = ss.str();
|
||||
connection.set_body(body);
|
||||
std::stringstream ssLen;
|
||||
ssLen << body.length();
|
||||
connection.replace_header(HttpField::content_length, ssLen.str());
|
||||
ss << "<!doctype html><html><head>"
|
||||
<< "<title>Error 500 (Server error)</title><body>"
|
||||
<< "<h1>Error 500</h1>"
|
||||
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
||||
<< "</body></head></html>";
|
||||
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<WebSocketSession>(this, webSocket);
|
||||
m_sessions.insert(socketSession);
|
||||
try {
|
||||
server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl);
|
||||
WebSocketSession::ptr socketSession = std::make_shared<WebSocketSession>(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);
|
||||
|
||||
@@ -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 <ctime>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ss.hpp"
|
||||
#include <websocketpp/logger/levels.hpp>
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
+49
-22
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@ Description=${DESCRIPTION}
|
||||
After=network.target
|
||||
After=sound.target
|
||||
After=local-fs.target
|
||||
After=avahi-daemon
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user