diff --git a/Builds/LinuxMakefile/Makefile b/Builds/LinuxMakefile/Makefile index 6b3385f8..6410f4ba 100644 --- a/Builds/LinuxMakefile/Makefile +++ b/Builds/LinuxMakefile/Makefile @@ -35,7 +35,7 @@ ifeq ($(CONFIG),Debug) TARGET_ARCH := endif - JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DDEBUG=1" "-D_DEBUG=1" "-DPIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==" "-DSAMPLER_SKIP_UI" "-DHAVE_LIBSAMPLERATE" "-DUSE_BUILTIN_FFT" "-DUSE_PTHREADS" "-DBUILD_DAWDREAMER_FAUST" "-DBUILD_DAWDREAMER_RUBBERBAND" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=0.6.1" "-DJUCE_APP_VERSION_HEX=0x601" $(shell pkg-config --cflags alsa freetype2 libcurl) -pthread -I../../JuceLibraryCode/modules/juce_audio_processors/format_types/VST3_SDK -I../../thirdparty/JUCE/modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../JuceLibraryCode/modules -I/usr/include/python3.9 -I../../thirdparty/pybind11/include -I../../thirdparty/faust/architecture -I../../thirdparty/faust/compiler -I../../thirdparty/faust/compiler/utils -I../../thirdparty/libsamplerate/src -I../../thirdparty/libsamplerate/include -I../../thirdparty/rubberband -I../../thirdparty/rubberband/rubberband -I../../thirdparty/rubberband/src/kissfft -I../../thirdparty/rubberband/src -I../../thirdparty/portable_endian/include $(CPPFLAGS) + JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DDEBUG=1" "-D_DEBUG=1" "-DPIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==" "-DSAMPLER_SKIP_UI" "-DJUCE_MODAL_LOOPS_PERMITTED" "-DHAVE_LIBSAMPLERATE" "-DUSE_BUILTIN_FFT" "-DUSE_PTHREADS" "-DBUILD_DAWDREAMER_FAUST" "-DBUILD_DAWDREAMER_RUBBERBAND" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=0.6.2" "-DJUCE_APP_VERSION_HEX=0x602" $(shell pkg-config --cflags alsa freetype2 libcurl) -pthread -I../../JuceLibraryCode/modules/juce_audio_processors/format_types/VST3_SDK -I../../thirdparty/JUCE/modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../JuceLibraryCode/modules -I/usr/include/python3.9 -I../../thirdparty/pybind11/include -I../../thirdparty/faust/architecture -I../../thirdparty/faust/compiler -I../../thirdparty/faust/compiler/utils -I../../thirdparty/libsamplerate/src -I../../thirdparty/libsamplerate/include -I../../thirdparty/rubberband -I../../thirdparty/rubberband/rubberband -I../../thirdparty/rubberband/src/kissfft -I../../thirdparty/rubberband/src -I../../thirdparty/portable_endian/include $(CPPFLAGS) JUCE_CPPFLAGS_DYNAMIC_LIBRARY := "-DJucePlugin_Build_VST=0" "-DJucePlugin_Build_VST3=0" "-DJucePlugin_Build_AU=0" "-DJucePlugin_Build_AUv3=0" "-DJucePlugin_Build_RTAS=0" "-DJucePlugin_Build_AAX=0" "-DJucePlugin_Build_Standalone=0" "-DJucePlugin_Build_Unity=0" JUCE_CFLAGS_DYNAMIC_LIBRARY := -fPIC -fvisibility=hidden JUCE_LDFLAGS_DYNAMIC_LIBRARY := -shared @@ -58,7 +58,7 @@ ifeq ($(CONFIG),Release) TARGET_ARCH := endif - JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DNDEBUG=1" "-DPIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==" "-DSAMPLER_SKIP_UI" "-DHAVE_LIBSAMPLERATE" "-DUSE_BUILTIN_FFT" "-DUSE_PTHREADS" "-DBUILD_DAWDREAMER_FAUST" "-DBUILD_DAWDREAMER_RUBBERBAND" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=0.6.1" "-DJUCE_APP_VERSION_HEX=0x601" $(shell pkg-config --cflags alsa freetype2 libcurl) -pthread -I../../JuceLibraryCode/modules/juce_audio_processors/format_types/VST3_SDK -I../../thirdparty/JUCE/modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../JuceLibraryCode/modules -I/usr/include/python3.9 -I../../thirdparty/pybind11/include -I../../thirdparty/faust/architecture -I../../thirdparty/faust/compiler -I../../thirdparty/faust/compiler/utils -I../../thirdparty/libsamplerate/src -I../../thirdparty/libsamplerate/include -I../../thirdparty/rubberband -I../../thirdparty/rubberband/rubberband -I../../thirdparty/rubberband/src/kissfft -I../../thirdparty/rubberband/src -I../../thirdparty/portable_endian/include $(CPPFLAGS) + JUCE_CPPFLAGS := $(DEPFLAGS) "-DLINUX=1" "-DNDEBUG=1" "-DPIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==" "-DSAMPLER_SKIP_UI" "-DJUCE_MODAL_LOOPS_PERMITTED" "-DHAVE_LIBSAMPLERATE" "-DUSE_BUILTIN_FFT" "-DUSE_PTHREADS" "-DBUILD_DAWDREAMER_FAUST" "-DBUILD_DAWDREAMER_RUBBERBAND" "-DJUCER_LINUX_MAKE_6D53C8B4=1" "-DJUCE_APP_VERSION=0.6.2" "-DJUCE_APP_VERSION_HEX=0x602" $(shell pkg-config --cflags alsa freetype2 libcurl) -pthread -I../../JuceLibraryCode/modules/juce_audio_processors/format_types/VST3_SDK -I../../thirdparty/JUCE/modules/juce_audio_processors/format_types/VST3_SDK -I../../JuceLibraryCode -I../../JuceLibraryCode/modules -I/usr/include/python3.9 -I../../thirdparty/pybind11/include -I../../thirdparty/faust/architecture -I../../thirdparty/faust/compiler -I../../thirdparty/faust/compiler/utils -I../../thirdparty/libsamplerate/src -I../../thirdparty/libsamplerate/include -I../../thirdparty/rubberband -I../../thirdparty/rubberband/rubberband -I../../thirdparty/rubberband/src/kissfft -I../../thirdparty/rubberband/src -I../../thirdparty/portable_endian/include $(CPPFLAGS) JUCE_CPPFLAGS_DYNAMIC_LIBRARY := "-DJucePlugin_Build_VST=0" "-DJucePlugin_Build_VST3=0" "-DJucePlugin_Build_AU=0" "-DJucePlugin_Build_AUv3=0" "-DJucePlugin_Build_RTAS=0" "-DJucePlugin_Build_AAX=0" "-DJucePlugin_Build_Standalone=0" "-DJucePlugin_Build_Unity=0" JUCE_CFLAGS_DYNAMIC_LIBRARY := -fPIC -fvisibility=hidden JUCE_LDFLAGS_DYNAMIC_LIBRARY := -shared diff --git a/Builds/MacOSX/DawDreamer.xcodeproj/project.pbxproj b/Builds/MacOSX/DawDreamer.xcodeproj/project.pbxproj index cef663ba..76f260b7 100644 --- a/Builds/MacOSX/DawDreamer.xcodeproj/project.pbxproj +++ b/Builds/MacOSX/DawDreamer.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ C98D426E06BA4F72F0E4C8DB /* Resampler.cpp */ = {isa = PBXBuildFile; fileRef = EF9722353A0A2BDA8376FCC6; }; CAE5EFB597BE247AE021B656 /* StretcherImpl.cpp */ = {isa = PBXBuildFile; fileRef = CF40530FC23B166C749C7CD4; }; D8E2F775028097109496AAA1 /* Cocoa.framework */ = {isa = PBXBuildFile; fileRef = 7EC708BB8670BC4B971F031E; }; + D915E61C72D98F0B97F31690 /* StandalonePluginWindow.h */ = {isa = PBXBuildFile; fileRef = 44A26898F037A3E6B43EB911; }; DFE780784ED99EACC1B654A6 /* include_juce_gui_extra.mm */ = {isa = PBXBuildFile; fileRef = 06AE4EC72C9D2D0775EF879E; }; E358C00D8D92D35AFBC9944C /* FaustProcessor.cpp */ = {isa = PBXBuildFile; fileRef = BFD142EBB8AFBADC8AB04A8A; }; E6C27C09FEC5B3BBEC75F414 /* CoreAudioKit.framework */ = {isa = PBXBuildFile; fileRef = E4E23DF360EE14C337676E2D; }; @@ -119,6 +120,7 @@ 3AAA87ED81FE59EA46C6FE62 /* juce_core */ /* juce_core */ = {isa = PBXFileReference; lastKnownFileType = folder; name = juce_core; path = ../../JuceLibraryCode/modules/juce_core; sourceTree = SOURCE_ROOT; }; 3C4A562AA5734126DF3D041A /* SpectralDifferenceAudioCurve.cpp */ /* SpectralDifferenceAudioCurve.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = SpectralDifferenceAudioCurve.cpp; path = ../../thirdparty/rubberband/src/audiocurves/SpectralDifferenceAudioCurve.cpp; sourceTree = SOURCE_ROOT; }; 41F9CCC5586A494C6A26B0B1 /* samplerate.h */ /* samplerate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = samplerate.h; path = ../../thirdparty/libsamplerate/include/samplerate.h; sourceTree = SOURCE_ROOT; }; + 44A26898F037A3E6B43EB911 /* StandalonePluginWindow.h */ /* StandalonePluginWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = StandalonePluginWindow.h; path = ../../Source/StandalonePluginWindow.h; sourceTree = SOURCE_ROOT; }; 45D38E4893A53D1263DF4E82 /* CompoundAudioCurve.cpp */ /* CompoundAudioCurve.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = CompoundAudioCurve.cpp; path = ../../thirdparty/rubberband/src/audiocurves/CompoundAudioCurve.cpp; sourceTree = SOURCE_ROOT; }; 4782971C3066F785B5354384 /* DataModel.h */ /* DataModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DataModel.h; path = ../../Source/Sampler/Source/DataModels/DataModel.h; sourceTree = SOURCE_ROOT; }; 4915A3C580F0DAA69FAAED98 /* CompressorProcessor.h */ /* CompressorProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CompressorProcessor.h; path = ../../Source/CompressorProcessor.h; sourceTree = SOURCE_ROOT; }; @@ -559,6 +561,7 @@ E553BE5C53ED6D87F71CC515 /* Processors */ = { isa = PBXGroup; children = ( + 44A26898F037A3E6B43EB911, D5EBA3BB8BAA3B120AB80BBF, C713F7F77E549E8C08771E49, 70B722073FC3FB8EC6C4E41D, @@ -726,6 +729,7 @@ 0C01D9069A27A2ECB0B24253, CAE5EFB597BE247AE021B656, 21FB7BCF683F51D403A7475D, + D915E61C72D98F0B97F31690, 911FCAF5D38B28CA617932B9, EC456483653E6271848DB065, 972909F8E3C96F724A1E69AE, @@ -777,14 +781,15 @@ "DEBUG=1", "PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==", "SAMPLER_SKIP_UI", + "JUCE_MODAL_LOOPS_PERMITTED", "HAVE_LIBSAMPLERATE", "HAVE_VDSP", "USE_PTHREADS", "BUILD_DAWDREAMER_FAUST", "BUILD_DAWDREAMER_RUBBERBAND", "JUCER_XCODE_MAC_F6D2F4CF=1", - "JUCE_APP_VERSION=0.6.1", - "JUCE_APP_VERSION_HEX=0x601", + "JUCE_APP_VERSION=0.6.2", + "JUCE_APP_VERSION_HEX=0x602", "JucePlugin_Build_VST=0", "JucePlugin_Build_VST3=0", "JucePlugin_Build_AU=0", @@ -902,14 +907,15 @@ "NDEBUG=1", "PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==", "SAMPLER_SKIP_UI", + "JUCE_MODAL_LOOPS_PERMITTED", "HAVE_LIBSAMPLERATE", "HAVE_VDSP", "USE_PTHREADS", "BUILD_DAWDREAMER_FAUST", "BUILD_DAWDREAMER_RUBBERBAND", "JUCER_XCODE_MAC_F6D2F4CF=1", - "JUCE_APP_VERSION=0.6.1", - "JUCE_APP_VERSION_HEX=0x601", + "JUCE_APP_VERSION=0.6.2", + "JUCE_APP_VERSION_HEX=0x602", "JucePlugin_Build_VST=0", "JucePlugin_Build_VST3=0", "JucePlugin_Build_AU=0", diff --git a/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj b/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj index 6bdf9c15..ed34c74b 100644 --- a/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj +++ b/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj @@ -66,7 +66,7 @@ Disabled ProgramDatabase ..\..\JuceLibraryCode\modules\juce_audio_processors\format_types\VST3_SDK;..\..\thirdparty\JUCE\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\JuceLibraryCode\modules;$(pythonLocation)\include;..\..\thirdparty\pybind11\include;..\..\thirdparty\faust\architecture;..\..\thirdparty\faust\compiler;..\..\thirdparty\faust\compiler\utils;..\..\thirdparty\libsamplerate\src;..\..\thirdparty\libsamplerate\include;..\..\thirdparty\rubberband;..\..\thirdparty\rubberband\rubberband;..\..\thirdparty\rubberband\src\kissfft;..\..\thirdparty\rubberband\src;..\..\thirdparty\portable_endian\include;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;DEBUG;_DEBUG;PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==;SAMPLER_SKIP_UI;_WIN32;__SSE__;__SSE2__;BUILD_DAWDREAMER_FAUST;BUILD_DAWDREAMER_RUBBERBAND;NOMINMAX;HAVE_LIBSAMPLERATE;HAVE_KISSFFT;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=0.6.1;JUCE_APP_VERSION_HEX=0x601;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_RTAS=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;_LIB;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;DEBUG;_DEBUG;PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==;SAMPLER_SKIP_UI;JUCE_MODAL_LOOPS_PERMITTED;_WIN32;__SSE__;__SSE2__;BUILD_DAWDREAMER_FAUST;BUILD_DAWDREAMER_RUBBERBAND;NOMINMAX;HAVE_LIBSAMPLERATE;HAVE_KISSFFT;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=0.6.2;JUCE_APP_VERSION_HEX=0x602;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_RTAS=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;_LIB;%(PreprocessorDefinitions) MultiThreadedDebugDLL true NotUsing @@ -116,7 +116,7 @@ copy "..\..\thirdparty\libfaust\win-x64\Debug\bin\faust.dll" "$(p Full ..\..\JuceLibraryCode\modules\juce_audio_processors\format_types\VST3_SDK;..\..\thirdparty\JUCE\modules\juce_audio_processors\format_types\VST3_SDK;..\..\JuceLibraryCode;..\..\JuceLibraryCode\modules;$(pythonLocation)\include;..\..\thirdparty\pybind11\include;..\..\thirdparty\faust\architecture;..\..\thirdparty\faust\compiler;..\..\thirdparty\faust\compiler\utils;..\..\thirdparty\libsamplerate\src;..\..\thirdparty\libsamplerate\include;..\..\thirdparty\rubberband;..\..\thirdparty\rubberband\rubberband;..\..\thirdparty\rubberband\src\kissfft;..\..\thirdparty\rubberband\src;..\..\thirdparty\portable_endian\include;%(AdditionalIncludeDirectories) - _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;NDEBUG;PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==;SAMPLER_SKIP_UI;_WIN32;__SSE__;__SSE2__;BUILD_DAWDREAMER_FAUST;BUILD_DAWDREAMER_RUBBERBAND;NOMINMAX;HAVE_LIBSAMPLERATE;HAVE_KISSFFT;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=0.6.1;JUCE_APP_VERSION_HEX=0x601;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_RTAS=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;_LIB;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;NDEBUG;PIP_JUCE_EXAMPLES_DIRECTORY=QzpcdG9vbHNcSlVDRVxleGFtcGxlcw==;SAMPLER_SKIP_UI;JUCE_MODAL_LOOPS_PERMITTED;_WIN32;__SSE__;__SSE2__;BUILD_DAWDREAMER_FAUST;BUILD_DAWDREAMER_RUBBERBAND;NOMINMAX;HAVE_LIBSAMPLERATE;HAVE_KISSFFT;JUCER_VS2019_78A5026=1;JUCE_APP_VERSION=0.6.2;JUCE_APP_VERSION_HEX=0x602;JucePlugin_Build_VST=0;JucePlugin_Build_VST3=0;JucePlugin_Build_AU=0;JucePlugin_Build_AUv3=0;JucePlugin_Build_RTAS=0;JucePlugin_Build_AAX=0;JucePlugin_Build_Standalone=0;JucePlugin_Build_Unity=0;_LIB;%(PreprocessorDefinitions) MultiThreadedDLL true NotUsing @@ -2316,6 +2316,7 @@ copy "..\..\thirdparty\libfaust\win-x64\Release\bin\faust.dll" "$ + diff --git a/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj.filters b/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj.filters index e52b812d..a986a562 100644 --- a/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj.filters +++ b/Builds/VisualStudio2019/DawDreamer_DynamicLibrary.vcxproj.filters @@ -2979,6 +2979,9 @@ DawDreamer\Rubberband + + DawDreamer\Processors + DawDreamer\Processors\Sampler\Components diff --git a/Builds/VisualStudio2019/resources.rc b/Builds/VisualStudio2019/resources.rc index 25359650..af74e07a 100644 --- a/Builds/VisualStudio2019/resources.rc +++ b/Builds/VisualStudio2019/resources.rc @@ -9,16 +9,16 @@ #include VS_VERSION_INFO VERSIONINFO -FILEVERSION 0,6,1,0 +FILEVERSION 0,6,2,0 BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" BEGIN VALUE "FileDescription", "DawDreamer\0" - VALUE "FileVersion", "0.6.1\0" + VALUE "FileVersion", "0.6.2\0" VALUE "ProductName", "DawDreamer\0" - VALUE "ProductVersion", "0.6.1\0" + VALUE "ProductVersion", "0.6.2\0" END END diff --git a/DawDreamer.jucer b/DawDreamer.jucer index 481acdd2..2c2a7a8a 100644 --- a/DawDreamer.jucer +++ b/DawDreamer.jucer @@ -1,6 +1,6 @@ - + myFilter, myFilter; +process = si.bus(4) :> sp.stereoize(myFilter); ``` #### **faust_test_stereo_mixdown.py:** @@ -220,11 +236,11 @@ engine.render(DURATION) ### Polyphony in Faust -Polyphony is supported too. You simply need to provide DSP code that refers to correctly named parameters such as `freq` or `note`, `gain`, and `gate`. For more information, see the FAUST [manual](https://faustdoc.grame.fr/manual/midi/#standard-polyphony-parameters). In DawDreamer, you must set the number of voices on the processor to 1 or higher. 0 disables polyphony. Refer to `tests/test_faust_poly.py`. +Polyphony is supported too. You simply need to provide DSP code that refers to correctly named parameters such as `freq` or `note`, `gain`, and `gate`. For more information, see the FAUST [manual](https://faustdoc.grame.fr/manual/midi/#standard-polyphony-parameters). In DawDreamer, you must set the number of voices on the processor to 1 or higher. The default (0) disables polyphony. Refer to [tests/test_faust_poly\*.py](https://github.com/DBraun/DawDreamer/tree/main/tests). ### Soundfiles in Faust -Faust code in DawDreamer can use the [soundfile](https://faustdoc.grame.fr/manual/syntax/#soundfile-primitive) primitive. Normally `soundfile` is meant to load `.wav` files, but DawDreamer uses it to receive data from numpy arrays. +Faust code in DawDreamer can use the [soundfile](https://faustdoc.grame.fr/manual/syntax/#soundfile-primitive) primitive. Normally `soundfile` is meant to load `.wav` files, but DawDreamer uses it to receive data from numpy arrays. Refer to [tests/test_faust_soundfile.py](https://github.com/DBraun/DawDreamer/blob/main/tests/test_faust_soundfile.py) **soundfile_test.py** ```python @@ -255,7 +271,7 @@ engine = daw.RenderEngine(SAMPLE_RATE, BUFFER_SIZE) playback_processor = engine.make_playbackwarp_processor("drums", load_audio_file("drums.wav")) playback_processor.time_ratio = 2. # Play back in twice the amount of time (i.e., slowed down). -playback_processor.transpose = 3. # Up 3 semitones. +playback_processor.transpose = -5. # Down 5 semitones. graph = [ (playback_processor, []), @@ -282,7 +298,8 @@ graph = [ engine.load_graph(graph) ``` -This will set several properties: +The `set_clip_file` method will set several properties: +* `.warp_markers` (np.array [N, 2]) : List of pairs of (time in samples, time in beats) * `.start_marker` (float) : Start marker position in beats relative to 1.1.1 * `.end_marker` (float) : End marker position in beats relative to 1.1.1 * `.loop_start` (float) : Loop start position in beats relative to 1.1.1 @@ -292,7 +309,7 @@ This will set several properties: Any of these properties can be changed after an `.asd` file is loaded. -If `.warp_on` is True, then any value set by `.time_ratio` will be ignored. If `.warp_on` is False, then the `start_marker` and `loop_start` are the first sample of the audio, and the `end_marker` and `loop_end` are the last sample. +If `.warp_on` is True, then any value set by `.time_ratio` will be ignored. With `set_clip_positions`, you can use the same audio clip at multiple places along the timeline. diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 2846999c..d4af45ba 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -4,6 +4,8 @@ #include +#include "StandalonePluginWindow.h" + using juce::ExtensionsVisitor; struct PresetVisitor : public ExtensionsVisitor { @@ -33,6 +35,26 @@ PluginProcessor::PluginProcessor(std::string newUniqueName, double sampleRate, i isLoaded = loadPlugin(sampleRate, samplesPerBlock); } +void +PluginProcessor::openEditor() { + if (!myPlugin) { + throw std::runtime_error( + "Editor cannot be shown because the plugin isn't loaded."); + } + + if (!juce::Desktop::getInstance().getDisplays().getPrimaryDisplay()) { + throw std::runtime_error( + "Editor cannot be shown because no visual display devices are available."); + } + + if (!juce::MessageManager::getInstance()->isThisTheMessageThread()) { + throw std::runtime_error( + "Plugin UI windows can only be shown from the main thread."); + } + + StandalonePluginWindow::openWindowAndWait(*this, *myPlugin); +} + bool PluginProcessor::loadPlugin(double sampleRate, int samplesPerBlock) { OwnedArray pluginDescriptions; @@ -83,11 +105,22 @@ PluginProcessor::loadPlugin(double sampleRate, int samplesPerBlock) { auto outputs = myPlugin->getTotalNumOutputChannels(); this->setPlayConfigDetails(inputs, outputs, sampleRate, samplesPerBlock); myPlugin->setPlayConfigDetails(inputs, outputs, sampleRate, samplesPerBlock); + myPlugin->prepareToPlay(sampleRate, samplesPerBlock); myPlugin->setNonRealtime(true); mySampleRate = sampleRate; createParameterLayout(); + { + // Process a block of silence a few times to "warm up" the processor. + juce::AudioSampleBuffer audioBuffer = AudioSampleBuffer(std::max(inputs, outputs), samplesPerBlock); + MidiBuffer emptyMidiBuffer; + for (int i = 0; i < 5; i++) { + audioBuffer.clear(); + myPlugin->processBlock(audioBuffer, emptyMidiBuffer); + } + } + return true; } @@ -118,6 +151,15 @@ PluginProcessor::canApplyBusesLayout(const juce::AudioProcessor::BusesLayout& la return myPlugin->checkBusesLayoutSupported(layout); } +bool +PluginProcessor::setBusesLayout(const BusesLayout& arr) { + if (myPlugin.get()) { + AudioProcessor::setBusesLayout(arr); + return myPlugin->setBusesLayout(arr); + } + return false; +} + void PluginProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { @@ -329,6 +371,8 @@ PluginProcessor::setParameter(const int paramIndex, const float value) return; } + myPlugin->setParameter(paramIndex, value); + std::string paramID = std::to_string(paramIndex); ProcessorBase::setAutomationVal(paramID, value); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 1b75f3bc..a02f8c13 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -13,12 +13,23 @@ class PluginProcessor : public ProcessorBase bool canApplyBusesLayout(const juce::AudioProcessor::BusesLayout& layout); - bool setBusesLayout(const BusesLayout& arr) { - if (myPlugin.get()) { - AudioProcessor::setBusesLayout(arr); - return myPlugin->setBusesLayout(arr); + bool canApplyBusInputsAndOutputs(int inputs, int outputs) { + BusesLayout busesLayout = this->makeBusesLayout(inputs, outputs); + return this->canApplyBusesLayout(busesLayout); + } + + bool setBusesLayout(const BusesLayout& arr); + + bool setMainBusInputsAndOutputs(int inputs, int outputs) { + BusesLayout busesLayout = this->makeBusesLayout(inputs, outputs); + + if (this->canApplyBusesLayout(busesLayout)) { + return this->setBusesLayout(busesLayout); + } + else { + throw std::invalid_argument(this->getUniqueName() + " CANNOT ApplyBusesLayout inputs: " + std::to_string(inputs) + " outputs: " + std::to_string(outputs)); + return false; } - return false; } void numChannelsChanged() { @@ -66,6 +77,35 @@ class PluginProcessor : public ProcessorBase void setPlayHead(AudioPlayHead* newPlayHead); + void openEditor(); + + void loadStateInformation(std::string filepath) { + + MemoryBlock state; + File file = File(filepath); + file.loadFileAsData(state); + + myPlugin->setStateInformation((const char*)state.getData(), (int)state.getSize()); + + for (int i = 0; i < myPlugin->AudioProcessor::getNumParameters(); i++) { + std::string paramID = std::to_string(i); + ProcessorBase::setAutomationVal(paramID, myPlugin->getParameter(i)); + } + } + + void saveStateInformation(std::string filepath) { + if (!myPlugin) { + throw std::runtime_error("Please load the plugin first"); + } + MemoryBlock state; + myPlugin->getStateInformation(state); + + juce::File file(filepath); + juce::FileOutputStream fos(file); + + fos.write(state.getData(), state.getSize()); + } + private: bool loadPlugin(double sampleRate, int samplesPerBlock); diff --git a/Source/ProcessorBase.h b/Source/ProcessorBase.h index 5fa6cd14..689386ad 100644 --- a/Source/ProcessorBase.h +++ b/Source/ProcessorBase.h @@ -135,24 +135,25 @@ class ProcessorBase : public juce::AudioProcessor bool isConnectedInGraph() { return m_isConnectedInGraph;} void setConnectedInGraph(bool isConnected) { m_isConnectedInGraph = isConnected; - } - void setMainBusInputsAndOutputs(int inputs, int outputs) { - BusesLayout busesLayout; - const AudioChannelSet inputChannelSet = AudioChannelSet::discreteChannels(inputs); - const AudioChannelSet outputChannelSet = AudioChannelSet::discreteChannels(outputs); - busesLayout.inputBuses.add(inputChannelSet); - busesLayout.outputBuses.add(outputChannelSet); + bool setMainBusInputsAndOutputs(int inputs, int outputs) { + BusesLayout busesLayout = makeBusesLayout(inputs, outputs); if (this->canApplyBusesLayout(busesLayout)) { - bool result = this->setBusesLayout(busesLayout); + return this->setBusesLayout(busesLayout); } else { - std::cerr << this->getUniqueName() << " CANNOT ApplyBusesLayout inputs: " << inputs << " outputs: " << outputs << std::endl; + throw std::invalid_argument(this->getUniqueName() + " CANNOT ApplyBusesLayout inputs: " + std::to_string(inputs) + " outputs: " + std::to_string(outputs)); + return false; } } + bool canApplyBusInputsAndOutputs(int inputs, int outputs) { + BusesLayout busesLayout = makeBusesLayout(inputs, outputs); + return this->canApplyBusesLayout(busesLayout); + } + private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ProcessorBase) @@ -161,7 +162,6 @@ class ProcessorBase : public juce::AudioProcessor bool m_isConnectedInGraph = false; protected: - AudioProcessorValueTreeState myParameters; @@ -171,4 +171,13 @@ class ProcessorBase : public juce::AudioProcessor return params; } bool m_recordEnable = false; + + BusesLayout makeBusesLayout(int inputs, int outputs) { + BusesLayout busesLayout; + const AudioChannelSet inputChannelSet = AudioChannelSet::discreteChannels(inputs); + const AudioChannelSet outputChannelSet = AudioChannelSet::discreteChannels(outputs); + busesLayout.inputBuses.add(inputChannelSet); + busesLayout.outputBuses.add(outputChannelSet); + return busesLayout; + } }; diff --git a/Source/StandalonePluginWindow.h b/Source/StandalonePluginWindow.h new file mode 100644 index 00000000..dec11fe4 --- /dev/null +++ b/Source/StandalonePluginWindow.h @@ -0,0 +1,120 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +// This file was slightly modified from +// https://github.com/spotify/pedalboard/blob/49cefb490191679034238a14db6ce3cc7e96b79d/pedalboard/ExternalPlugin.h#L167-L245 + +class StandalonePluginWindow : public juce::DocumentWindow { +public: + StandalonePluginWindow(PluginProcessor& dawDreamerPluginProcessor, juce::AudioProcessor& processor) + : DocumentWindow("DawDreamer: " + processor.getName(), + juce::LookAndFeel::getDefaultLookAndFeel().findColour( + juce::ResizableWindow::backgroundColourId), + //juce::DocumentWindow::minimiseButton | + juce::DocumentWindow::closeButton), + processor(processor), + dawDreamerPluginProcessor(dawDreamerPluginProcessor) { + setUsingNativeTitleBar(true); + + if (processor.hasEditor()) { + if (auto* editor = processor.createEditorIfNeeded()) { + setContentOwned(editor, true); + setResizable(editor->isResizable(), false); + } + else { + throw std::runtime_error("Failed to create plugin editor UI."); + } + } + else { + throw std::runtime_error("Plugin has no available editor UI."); + } + } + + /** + * Open a native window to show a given AudioProcessor's editor UI, + * pumping the juce::MessageManager run loop as necessary to service + * UI events. + */ + static void openWindowAndWait(PluginProcessor& dawDreamerPluginProcessor, juce::AudioProcessor& processor) { + bool shouldThrowErrorAlreadySet = false; + + JUCE_AUTORELEASEPOOL{ + StandalonePluginWindow window(dawDreamerPluginProcessor, processor); + window.show(); + + // Run in a tight loop so that we don't have to call ->stopDispatchLoop(), + // which causes the MessageManager to become unusable in the future. + // The window can be closed by sending a KeyboardInterrupt or closing + // the window in the UI. + while (window.isVisible()) { + if (PyErr_CheckSignals() != 0) { + window.closeButtonPressed(); + shouldThrowErrorAlreadySet = true; + break; + } + + { + // Release the GIL to allow other Python threads to run in the + // background while we the UI is running: + py::gil_scoped_release release; + juce::MessageManager::getInstance()->runDispatchLoopUntil(10); + } + } + } + + // Once the Autorelease pool has been drained, pump the dispatch loop one + // more time to process any window close events: + juce::MessageManager::getInstance()->runDispatchLoopUntil(10); + + if (shouldThrowErrorAlreadySet) { + throw py::error_already_set(); + } + } + + void closeButtonPressed() override { + setVisible(false); + + for (int j = 0; j < 2; j++) { + for (int i = 0; i < processor.getNumParameters(); ++i) + { + auto parameterName = processor.getParameterName(i); + std::string paramID = std::to_string(i); + // give it a valid single sample of automation. + dawDreamerPluginProcessor.setAutomationVal(paramID, processor.getParameter(i)); + } + } + } + + ~StandalonePluginWindow() override { clearContentComponent(); } + + void show() { + + centreWithSize(getWidth(), getHeight()); + + setVisible(true); + toFront(true); + juce::Process::makeForegroundProcess(); + } + +private: + juce::AudioProcessor& processor; + PluginProcessor& dawDreamerPluginProcessor; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StandalonePluginWindow) +}; \ No newline at end of file diff --git a/Source/source.cpp b/Source/source.cpp index 06ca9936..d7ff9979 100644 --- a/Source/source.cpp +++ b/Source/source.cpp @@ -90,7 +90,7 @@ play the audio in double the amount of time, so it will sound slowed down.") .def_property("loop_end", &PlaybackWarpProcessor::getLoopEnd, &PlaybackWarpProcessor::setLoopEnd, "The loop end position in beats (typically quarter notes) relative to 1.1.1") .def_property("start_marker", &PlaybackWarpProcessor::getStartMarker, &PlaybackWarpProcessor::setStartMarker, "The start position in beats (typically quarter notes) relative to 1.1.1") .def_property("end_marker", &PlaybackWarpProcessor::getEndMarker, &PlaybackWarpProcessor::setEndMarker, "The end position in beats (typically quarter notes) relative to 1.1.1") - .def_property("warp_markers", &PlaybackWarpProcessor::getWarpMarkers, &PlaybackWarpProcessor::setWarpMarkers, "Get/set the warp markers as a 2D array of time positions in seconds and positions in beats.") + .def_property("warp_markers", &PlaybackWarpProcessor::getWarpMarkers, &PlaybackWarpProcessor::setWarpMarkers, "Get/set the warp markers as an (N, 2) numpy array of time positions in samples and positions in beats.") .def("reset_warp_markers", &PlaybackWarpProcessor::resetWarpMarkers, arg("bpm"), "Reset the warp markers with a BPM.") .def("set_clip_file", &PlaybackWarpProcessor::loadAbletonClipInfo, arg("asd_file_path"), "Load an Ableton Live file with an \".asd\" extension") .def("set_data", &PlaybackWarpProcessor::setData, arg("data"), "Set the audio as a numpy array shaped (Channels, Samples).") @@ -157,6 +157,11 @@ but the filter mode cannot under automation."; .doc() = "An Add Processor adds one or more stereo inputs with corresponding gain parameters."; py::class_, ProcessorBase>(m, "PluginProcessor") + .def("can_set_bus", &PluginProcessorWrapper::canApplyBusInputsAndOutputs, arg("inputs"), arg("outputs"), "Return bool for whether this combination of input and output channels can be set.") + .def("set_bus", &PluginProcessorWrapper::setMainBusInputsAndOutputs, arg("inputs"), arg("outputs"), "Set the number of input and output channels. An error will be thrown for an unaccepted option.") + .def("save_state", &PluginProcessorWrapper::saveStateInformation, arg("filepath"), "Save the state to a file.") + .def("load_state", &PluginProcessorWrapper::loadStateInformation, arg("filepath"), "Load the state from a file.") + .def("open_editor", &PluginProcessorWrapper::openEditor, "Open the UI editor for the plugin.") .def("load_preset", &PluginProcessorWrapper::loadPreset, arg("filepath"), "Load an FXP preset with an absolute filepath and \".fxp\" extension.") .def("load_vst3_preset", &PluginProcessorWrapper::loadVST3Preset, arg("filepath"), "Load a VST3 preset with an absolute filepath and \".vstpreset\" extension.") .def("get_patch", &PluginProcessorWrapper::wrapperGetPatch) diff --git a/docs/conf.py b/docs/conf.py index 2782f2c3..8d9a13a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,7 @@ import sys import os +import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -53,17 +54,25 @@ # General information about the project. project = u'DawDreamer' -copyright = u'2021, David Braun' +copyright = datetime.datetime.now().strftime('%Y') + u', David Braun' author = u'David Braun' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + +def get_dawdreamer_version(): + import xml.etree.ElementTree as ET + from pathlib import Path + tree = ET.parse(Path(__file__).parent.parent / 'DawDreamer.jucer') + root = tree.getroot() + version = root.attrib['version'] + return version + # The short X.Y version. -version = u'0.0.1' +version = get_dawdreamer_version() # The full version, including alpha/beta/rc tags. -release = u'0.0.1' +release = get_dawdreamer_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -76,7 +85,7 @@ # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -119,7 +128,15 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + # 'github_banner': True, + 'github_button': True, + 'github_user': 'dbraun', + 'github_repo': 'DawDreamer', + 'sidebar_width': '320px', + 'page_width': '1200px', + # 'html_last_updated_fmt': '%b %d, %Y' +} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -143,7 +160,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or @@ -153,7 +170,7 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. diff --git a/tests/dawdreamer_utils.py b/tests/dawdreamer_utils.py index 4ec2d103..9ff462f5 100644 --- a/tests/dawdreamer_utils.py +++ b/tests/dawdreamer_utils.py @@ -1,5 +1,5 @@ from scipy.io import wavfile -from os.path import abspath, isfile +from os.path import abspath, isfile, basename from pathlib import Path import numpy as np import pathlib diff --git a/tests/output/.gitignore b/tests/output/.gitignore index 697e56f2..2d99b548 100644 --- a/tests/output/.gitignore +++ b/tests/output/.gitignore @@ -1 +1,2 @@ +state* *.wav \ No newline at end of file diff --git a/tests/test_faust_poly.py b/tests/test_faust_poly.py index 3228e467..0d7f7638 100644 --- a/tests/test_faust_poly.py +++ b/tests/test_faust_poly.py @@ -72,3 +72,55 @@ def test_faust_poly(): audio2 = _test_faust_poly(OUTPUT / 'test_faust_poly_automation_decay_ungrouped.wav', group_voices=False, decay=.5) assert(np.allclose(audio1[:,:-1], audio2[:,:-1])) # todo: don't drop last sample + + +def _test_faust_sine(midi_path: str, buffer_size=1): + + engine = daw.RenderEngine(SAMPLE_RATE, buffer_size) + + faust_processor = engine.make_faust_processor("faust") + faust_processor.num_voices = 16 + faust_processor.group_voices = True + faust_processor.release_length = .5 # note that this is the maximum of the "release" hslider below + + faust_processor.set_dsp_string( + f""" + declare name "MyInstrument"; + + declare options "[nvoices:8]"; // FaustProcessor has a property which will override this. + import("stdfaust.lib"); + + freq = hslider("freq",200,50,1000,0.01); // note pitch + gain = hslider("gain",0.1,0,1,0.01); // note velocity + gate = button("gate"); // note on/off + + attack = hslider("attack", .002, 0.001, 10., 0.); + decay = hslider("decay", .05, 0.001, 10., 0.); + sustain = hslider("sustain", 1.0, 0.0, 1., 0.); + release = hslider("release", .05, 0.001, {faust_processor.release_length}, 0.); + + envVol = 0.35*gain*en.adsr(attack, decay, sustain, release, gate); + + process = os.osc(freq)*envVol <: _, _; + effect = _, _; + """ + ) + faust_processor.compile() + # desc = faust_processor.get_parameters_description() + # for par in desc: + # print(par) + + faust_processor.load_midi(abspath(ASSETS / midi_path)) + + graph = [ + (faust_processor, []) + ] + + engine.load_graph(graph) + render(engine, file_path=OUTPUT / ('test_faust_sine_' + basename(midi_path) + '.wav'), duration=10.) + + audio = engine.get_audio() + assert(np.mean(np.abs(audio)) > .0001) + +def test_faust_sine1(): + _test_faust_sine(abspath(ASSETS / 'MIDI-Unprocessed_SMF_02_R1_2004_01-05_ORIG_MID--AUDIO_02_R1_2004_05_Track05_wav.midi')) \ No newline at end of file diff --git a/tests/test_playbackwarp_processor.py b/tests/test_playbackwarp_processor.py index 356f70ca..f26cdd7f 100644 --- a/tests/test_playbackwarp_processor.py +++ b/tests/test_playbackwarp_processor.py @@ -29,6 +29,11 @@ def test_playbackwarp_processor1(): warp_markers = drums.warp_markers + # re-assign and test that it stayed the same + drums.warp_markers = warp_markers + assert (drums.warp_markers == warp_markers).all() + + # add one interpolated warp marker in the middle warp1 = warp_markers[0] warp3 = warp_markers[-1] warp2 = 0.5*(warp1+warp3) @@ -40,8 +45,7 @@ def test_playbackwarp_processor1(): drums.warp_markers = warp_markers - assert drums.warp_markers.shape[0] == 3 - assert drums.warp_markers.shape[1] == 2 + assert (drums.warp_markers == warp_markers).all() graph = [ (drums, []), diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1164e937..aed03515 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -260,51 +260,55 @@ def test_plugin_goodhertz_sidechain(): _test_plugin_goodhertz_sidechain(do_sidechain=False) -# def test_plugin_effect_ambisonics(set_data=False): +def test_plugin_effect_ambisonics(): -# if MY_SYSTEM != "Windows": -# return + if MY_SYSTEM != "Windows": + return + + DURATION = 5. -# DURATION = 5. + engine = daw.RenderEngine(48000, 128) -# engine = daw.RenderEngine(SAMPLE_RATE, BUFFER_SIZE) + data = load_audio_file("assets/575854__yellowtree__d-b-funk-loop.wav", DURATION+.1) + + # convert to mono (1, N) + data = data.mean(axis=0, keepdims=True) + + playback_processor = engine.make_playback_processor("playback", data) -# data = load_audio_file("assets/575854__yellowtree__d-b-funk-loop.wav", DURATION+.1) -# playback_processor = engine.make_playback_processor("playback", data) + plugin_path = "C:/VSTPlugIns/sparta/sparta_ambiENC.dll" -# data = data.mean(axis=0, keepdims=True) + if not isfile(plugin_path): + return -# if set_data: -# playback_processor.set_data(data) + proc_encoder = engine.make_plugin_processor("effect", plugin_path) -# plugin_name = "sparta_ambiENC.vst" if MY_SYSTEM == "Darwin" else "sparta_ambiENC.dll" + # print('inputs: ', proc_encoder.get_num_input_channels(), ' outputs: ', proc_encoder.get_num_output_channels()) -# plugin_path = abspath("plugins/"+plugin_name) -# if not isfile(plugin_path): -# return + # proc_encoder.open_editor() -# effect = engine.make_plugin_processor("effect", plugin_path) + proc_encoder.set_bus(1, 4) -# effect.set_parameter(0, 1) + # print('inputs: ', proc_encoder.get_num_input_channels(), ' outputs: ', proc_encoder.get_num_output_channels()) -# assert(effect.get_num_input_channels() == 64) -# assert(effect.get_num_output_channels() == 64) + assert(proc_encoder.get_num_input_channels() == 1) + assert(proc_encoder.get_num_output_channels() == 4) -# # for par in effect.get_plugin_parameters_description(): -# # print(par) + # for par in proc_encoder.get_plugin_parameters_description(): + # print(par) -# graph = [ -# (playback_processor, []), -# (effect, ["playback"]) -# ] + graph = [ + (playback_processor, []), + (proc_encoder, ["playback"]) + ] -# assert(engine.load_graph(graph)) + assert(engine.load_graph(graph)) -# render(engine, file_path='output/test_plugin_effect_ambisonics.wav', duration=DURATION) + render(engine, file_path='output/test_plugin_effect_ambisonics.wav', duration=DURATION) -# audio = engine.get_audio() + audio = engine.get_audio() -# assert(effect.get_num_output_channels() == audio.shape[0]) + assert(proc_encoder.get_num_output_channels() == audio.shape[0]) def test_plugin_upright_piano(): @@ -345,5 +349,145 @@ def test_plugin_upright_piano(): audio = np.array(audio, np.float32).transpose() wavfile.write(OUTPUT / 'test_plugin_upright_piano.wav', SAMPLE_RATE, audio) -# if __name__ == '__main__': -# test_plugin_upright_piano() + +def test_plugin_editor(): + + if MY_SYSTEM not in ["Windows"]: + # We don't Serum on platforms other than Windows. + return + + # plugin_path = "C:/VSTPlugIns/Serum_x64.dll" + plugin_path = "C:/VSTPlugIns/TAL-NoiseMaker-64.vst3" + # plugin_path = "C:/VSTPlugIns/sparta/sparta_ambiBIN.dll" + + plugin_basename = os.path.splitext(basename(plugin_path))[0] + + if not isfile(plugin_path): + return + + DURATION = 5. + + engine = daw.RenderEngine(SAMPLE_RATE, 128) + + synth = engine.make_plugin_processor("synth", plugin_path) + + state_file_path = abspath(OUTPUT / (f'state_test_plugin_{plugin_basename}')) + + if isfile(state_file_path): + synth.load_state(state_file_path) + + # synth.open_editor() + + synth.save_state(state_file_path) + + # print(synth.get_plugin_parameters_description()) + + synth.get_parameter(0) + synth.set_parameter(0, synth.get_parameter(0)) + synth.set_automation(0, np.array([synth.get_parameter(0)])) + + print('inputs: ', synth.get_num_input_channels(), ' outputs: ', synth.get_num_output_channels()) + + # assert(synth.get_num_input_channels() == 0) + # assert(synth.get_num_output_channels() == 2) + + # (MIDI note, velocity, start sec, duration sec) + synth.add_midi_note(60, 60, 0.0, .25) + synth.add_midi_note(64, 80, 0.5, .5) + synth.add_midi_note(67, 127, 0.75, .5) + + assert(synth.n_midi_events == 3*2) # multiply by 2 because of the off-notes. + + graph = [ + (synth, []), + ] + + engine.load_graph(graph) + + render(engine, file_path=OUTPUT / (f'test_plugin_{plugin_basename}.wav'), duration=DURATION) + + audio = engine.get_audio() + assert(not np.allclose(audio*0., audio, atol=1e-07)) + + +def test_plugin_iem(plugin_path="C:/VSTPlugIns/IEMPluginSuite/VST2/IEM/MultiEncoder.dll", + plugin_path2="C:/VSTPlugIns/IEMPluginSuite/VST2/IEM/BinauralDecoder.dll" + ): + + if "PYTEST_CURRENT_TEST" in os.environ: + # we are not actually using pytest, so open the UI. + # todo: need a pytest way to test open_editor() + return + + + if not isfile(plugin_path): + return + + plugin_basename = os.path.splitext(basename(plugin_path))[0] + + DURATION = 5. + + engine = daw.RenderEngine(SAMPLE_RATE, 128) + + ambisonics_encoder = engine.make_plugin_processor("ambisonics_encoder", plugin_path) + ambisonics_decoder = engine.make_plugin_processor("ambisonics_decoder", plugin_path2) + ambisonics_encoder.record = True + ambisonics_decoder.record = True + + state_file_path = abspath(OUTPUT / (f'state_test_plugin_{plugin_basename}')) + + if isfile(state_file_path): + ambisonics_encoder.load_state(state_file_path) + + AMBISONICS_ORDER = 3 + num_inputs = 1 + num_outputs = (AMBISONICS_ORDER+1)**2 # this is a fixed equation + + ambisonics_encoder.set_bus(num_inputs, num_outputs) + + # The UI window will open. In the upper-left, select 1-channel input. + # In the upper-right select 3rd-order ambisonics (AMBISONICS_ORDER) + ambisonics_encoder.open_editor() + + ambisonics_encoder.save_state(state_file_path) + + assert ambisonics_encoder.get_num_input_channels() == num_inputs + assert ambisonics_encoder.get_num_output_channels() == num_outputs + + # print(ambisonics_encoder.get_plugin_parameters_description()) + # print('inputs: ', ambisonics_encoder.get_num_input_channels(), ' outputs: ', ambisonics_encoder.get_num_output_channels()) + + ambisonics_decoder.set_bus(num_outputs, 2) + + # Remember to select AMBISONICS_ORDER ambisonics. + ambisonics_decoder.open_editor() + + assert ambisonics_decoder.get_num_input_channels() == num_outputs + assert ambisonics_decoder.get_num_output_channels() == 2 + + data = load_audio_file("assets/575854__yellowtree__d-b-funk-loop.wav", DURATION+.1) + # convert to mono (1, N) + data = data.mean(axis=0, keepdims=True) + + graph = [ + (engine.make_playback_processor("playback", data), []), + (ambisonics_encoder, ["playback"]), + (ambisonics_decoder, [ambisonics_encoder.get_name()]) + ] + + engine.load_graph(graph) + engine.render(DURATION) + + audio = ambisonics_decoder.get_audio() + assert(not np.allclose(audio*0., audio, atol=1e-07)) + file_path = OUTPUT / f'test_plugin_{plugin_basename}_decoder.wav' + wavfile.write(file_path, SAMPLE_RATE, audio.transpose()) + + audio = ambisonics_encoder.get_audio() + assert(not np.allclose(audio*0., audio, atol=1e-07)) + file_path = OUTPUT / f'test_plugin_{plugin_basename}_encoder.wav' + wavfile.write(file_path, SAMPLE_RATE, audio.transpose()) + + +if __name__ == '__main__': + print('All done!') \ No newline at end of file