diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c73e91..b58096d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -128,6 +128,29 @@ "windows": { "MIMode": "gdb", } + }, + { + "name": "Standalone Plugin", + "type": "cppdbg", + "request": "launch", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/plugin/build", + "environment": [], + "externalConsole": false, + "logging": { + "engineLogging": false + }, + "preLaunchTask": "build plugin", + + "osx": { + "MIMode": "lldb", + "program": "${workspaceFolder}/plugin/build/TapeLooperPlugin_artefacts/Debug/Standalone/Tape Looper Plugin.app/Contents/MacOS/Tape Looper Plugin", + }, + "windows": { + "MIMode": "gdb", + "program": "${workspaceFolder}/plugin/build/TapeLooperPlugin_artefacts/Debug/Standalone/Tape Looper Plugin.exe", + } } ] } \ No newline at end of file diff --git a/dsp/CMakeLists.txt b/dsp/CMakeLists.txt index 2350948..46c1aa0 100644 --- a/dsp/CMakeLists.txt +++ b/dsp/CMakeLists.txt @@ -11,6 +11,7 @@ set(HEADER_FILES src/dsp/Recorder.h src/dsp/TapeLooper.h src/dsp/TapeProcessor.h + src/util/Memory.h ) add_subdirectory(lib/sprout) @@ -19,4 +20,5 @@ add_library(${TARGET} INTERFACE) target_include_directories(${TARGET} INTERFACE src/ ${CMAKE_CURRENT_SOURCE_DIR}/lib/sprout/ -) \ No newline at end of file +) +set_property(TARGET tapeLooperDsp PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/dsp/src/dsp/Player.h b/dsp/src/dsp/Player.h index 5688242..2f90716 100644 --- a/dsp/src/dsp/Player.h +++ b/dsp/src/dsp/Player.h @@ -64,6 +64,8 @@ class Player return isPlaying_; } + size_t getLoopLengthInSamples() const { return playbackLength_; } + void process(float paramSpeed, float speedModulationAmt, Direction direction, diff --git a/dsp/src/dsp/Recorder.h b/dsp/src/dsp/Recorder.h index de3de41..45c0067 100644 --- a/dsp/src/dsp/Recorder.h +++ b/dsp/src/dsp/Recorder.h @@ -2,6 +2,7 @@ #include "DspHelpers.h" #include "AudioBuffer.h" +#include "../util/Memory.h" template class Recorder @@ -89,6 +90,30 @@ class Recorder } } + size_t getSaveAndRecallStorageSize() const + { + return sizeof(uint32_t); // recordingLengthInSamples + } + + template + bool save(WritableMemory& mem) const + { + const auto recordingLengthInSamples = uint32_t(currentLength_); + return mem.writeItems(recordingLengthInSamples); + } + + template + bool restore(ReadableMemory& mem) + { + reset(); + uint32_t recordingLengthInSamples = 0; + if (!mem.readItems(recordingLengthInSamples)) + return false; + + currentLength_ = std::min(size_t(recordingLengthInSamples), buffer_.size_); + return true; + } + size_t getCurrentRecordingLength() const { return currentLength_; } bool isRecording() const { return isRecording_ || isFadingOut_; } diff --git a/dsp/src/dsp/TapeLooper.h b/dsp/src/dsp/TapeLooper.h index 299a5db..c8bef04 100644 --- a/dsp/src/dsp/TapeLooper.h +++ b/dsp/src/dsp/TapeLooper.h @@ -6,11 +6,19 @@ #include "TapeProcessor.h" #include "WowAndFlutter.h" -template +#include "../util/Memory.h" + +template struct LooperStoragePtr { - float* data[numChannels]; + float* data[numChannels_]; size_t numSamples; + static constexpr size_t numChannels = numChannels_; + + size_t getTotalSizeInBytes() const + { + return numChannels * numSamples * sizeof(float); + } }; template @@ -44,6 +52,8 @@ class TapeLooper public: using ProcessorType = TapeProcessor; using SpeedModulatorType = WowAndFlutterOscillator; + using PlayerType = Player; + using RecorderType = Recorder; TapeLooper(LooperStoragePtr storageToUse) : storage_(storageToUse), @@ -110,10 +120,66 @@ class TapeLooper switchState(LooperState::playing); } + size_t getSaveAndRecallStorageSize() const + { + return recorder_.getSaveAndRecallStorageSize() + + sizeof(uint8_t) // isPlaying? + + storage_.getTotalSizeInBytes(); + } + + /** + * Saves the state of the looper + */ + template + bool save(WritableMemory& mem) const + { + if (!recorder_.save(mem)) + return false; + + // write sample data + for (size_t ch = 0; ch < numChannels; ch++) + { + if (!mem.writeRaw(storage_.data[ch], sizeof(float) * storage_.numSamples)) + return false; + } + + const uint8_t isPlaying = state_ == LooperState::playing ? 1 : 0; + + return mem.writeItems(isPlaying); + } + + /** + * Stopps playback or recording and recalls the state of the looper, including + * - the length of the current recording + * - if the looper is currently playing + * - the sample data stored + */ + template + bool restore(ReadableMemory& mem) + { + if (!recorder_.restore(mem)) + return false; + + // read sample data + for (size_t ch = 0; ch < numChannels; ch++) + { + if (!mem.readRaw(storage_.data[ch], sizeof(float) * storage_.numSamples)) + return false; + } + + uint8_t isPlaying = 0; + mem.readItems(isPlaying); + + switchState(isPlaying == 1 ? LooperState::playing : LooperState::stopped); + return true; + } + + const LooperStoragePtr getSampleStoragePtr() const { return storage_; } + private: static constexpr float maxWowAndFlutterAmt_ = 0.0125f; const LooperStoragePtr storage_; LooperState state_; - Player player_; - Recorder recorder_; + PlayerType player_; + RecorderType recorder_; }; diff --git a/dsp/src/util/Memory.h b/dsp/src/util/Memory.h new file mode 100644 index 0000000..2a6f3e5 --- /dev/null +++ b/dsp/src/util/Memory.h @@ -0,0 +1,192 @@ +#pragma once + +#include + +/** + * Represents a fixed size chunk of memory to which data can be written. + * Access to the memory is provided by the MemoryProviderType template argument, + * allowing to implement various storage backends, e.g. raw memory, file access, etc. + * @tparam MemoryProviderType A storage provider backend. Must implement the following + * public member functions: + * // returns the maximum number of bytes that can be written + * size_t getAvailableSize() const; + * // writes some data to the memory and advances the write + * // position so that multiple consecutive calls append + * // the data continuously. + * void write(const void* srcData, size_t sizeToWrite); + */ +template +class WritableMemory +{ +public: + /** + * @param rawMem Instance of the storage provider backend, + * which is captured by reference (so it must stay alive + * until this WritableMemory is deconstructed). + */ + WritableMemory(MemoryProviderType& rawMem) : + rawMem_(rawMem), + size_(rawMem.getAvailableSize()) + { + } + + ~WritableMemory() {} + + /** + * Returns the number of bytes that can still be written + */ + size_t getRemainingSize() const { return size_; } + + /** + * Attempts to write to the memory. + * If enough bytes are available, the data is written and true is returned. + * Otherwise, false is returned and the data is not written. + */ + bool writeRaw(const void* src, size_t sizeToWrite) + { + if (size_ < sizeToWrite) + return false; + + rawMem_.write(src, sizeToWrite); + size_ -= sizeToWrite; + return true; + } + + /** + * Attempts to write a value to the memory. + * If enough bytes are available, the data is written and true is returned. + * Otherwise, false is returned and the data is not written. + */ + template + bool writeItem(const T& value) + { + return writeRaw((const void*) &value, sizeof(T)); + } + + /** + * Attempts to write multiple values to the memory. + * If enough bytes are available to store all values, they're written and true is returned. + * Otherwise, false is returned and none of the values is written. + */ + template + bool writeItems(T const&... values) + { + const auto totalSize = (size_t(0) + ... + sizeof(T)); + if (size_ < totalSize) + return false; + + return (writeItem(values) && ...); + } + +private: + WritableMemory(const WritableMemory&) = delete; + WritableMemory& operator=(const WritableMemory&) = delete; + MemoryProviderType& rawMem_; + size_t size_; +}; + +/** + * Represents a fixed size chunk of memory from which data can be read. + * Access to the memory is provided by the MemoryProviderType template argument, + * allowing to implement various storage backends, e.g. raw memory, file access, etc. + * @tparam MemoryProviderType A storage provider backend. Must implement the following + * public member functions: + * // returns the maximum number of bytes that can be read + * size_t getAvailableSize() const; + * // reads some data from the memory and advances the read + * // position + * void read(void* dest, size_t sizeToRead); + */ +template +class ReadableMemory +{ +public: + /** + * @param rawMem Instance of the storage provider backend, + * which is captured by reference (so it must stay alive + * until this ReadableMemory is deconstructed). + */ + ReadableMemory(MemoryProviderType& rawMem) : + rawMem_(rawMem), + size_(rawMem.getAvailableSize()) + { + } + ~ReadableMemory() {} + + /** + * Returns the number of bytes that can still be read + */ + size_t getRemainingSize() const { return size_; } + + /** + * Attempts to read from the memory. + * If enough bytes are available, the data is read and true is returned. + * Otherwise, false is returned and `dest` remains unchanged. + */ + bool readRaw(void* dest, size_t sizeToRead) + { + if (size_ < sizeToRead) + return false; + + rawMem_.read(dest, sizeToRead); + size_ -= sizeToRead; + return true; + } + + /** + * Attempts to read a value from the memory. + * If enough bytes are available, the data is read and true is returned. + * Otherwise, false is returned and the value is unchanged. + */ + template + bool readItem(T& value) + { + return readRaw((void*) &value, sizeof(T)); + } + + /** + * Attempts to read multiple values from the memory. + * If enough bytes are available to read all values, they're read and true is returned. + * Otherwise, false is returned and all values are unchanged. + */ + template + bool readItems(T&... values) + { + const auto totalSize = (size_t(0) + ... + sizeof(T)); + if (size_ < totalSize) + return false; + + return (readItem(values) && ...); + } + +private: + ReadableMemory(const ReadableMemory&) = delete; + ReadableMemory& operator=(const ReadableMemory&) = delete; + MemoryProviderType& rawMem_; + size_t size_; +}; + +/** + * A memory provider that allocates a fixed size buffer + * on the Stack. For use with ReadableMemory and WritableMemory + */ +template +struct StackMemoryProvider +{ + std::array arr; + uint8_t* ptr = arr.data(); + + constexpr size_t getAvailableSize() const { return size; } + + void write(const void* srcData, size_t sizeToWrite) + { + memcpy(ptr, srcData, sizeToWrite); + ptr += sizeToWrite; + } + + void read(void* destData, size_t sizeToRead) + { + memcpy(destData, ptr, sizeToRead); + ptr += sizeToRead; + } +}; \ No newline at end of file diff --git a/dsp/tests/Memory_gtest.cpp b/dsp/tests/Memory_gtest.cpp new file mode 100644 index 0000000..6e048e1 --- /dev/null +++ b/dsp/tests/Memory_gtest.cpp @@ -0,0 +1,248 @@ +#include +#include +#include + +#include +#include + +#include + +using ::testing::_; + +TEST(StackMemoryProvider, a_write) +{ + StackMemoryProvider<4> mem; + const uint32_t src = 123456789u; + mem.write(&src, sizeof(uint32_t)); + EXPECT_EQ(*(uint32_t*) (mem.arr.data()), 123456789u); + EXPECT_EQ(mem.ptr, mem.arr.data() + sizeof(uint32_t)); +} + +TEST(StackMemoryProvider, b_read) +{ + StackMemoryProvider<4> mem; + *(uint32_t*) (mem.arr.data()) = 123456789u; + uint32_t dest; + mem.read(&dest, sizeof(uint32_t)); + EXPECT_EQ(dest, 123456789u); +} + +///////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////// + +class MemoryProviderMock +{ +public: + MOCK_METHOD(size_t, getAvailableSize, (), (const)); + MOCK_METHOD(void, write, (const void* src, size_t sizeToWrite), ()); + MOCK_METHOD(void, read, (void* dest, size_t sizeToRead), ()); +}; + +TEST(WritableMemory, a_dontAlterContentsOnConstruction) +{ + ::testing::StrictMock memProvider; + + // call getAvailableSize() just once + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(2ul)); + // No other calls expected. + EXPECT_CALL(memProvider, read(_, _)).Times(0); + EXPECT_CALL(memProvider, write(_, _)).Times(0); + + WritableMemory mem(memProvider); +} + +TEST(WritableMemory, b_writeSingleItem) +{ + ::testing::StrictMock memProvider; + + const uint8_t value1 = 12; + const uint16_t value2 = 345; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read(_, _)).Times(0); + EXPECT_CALL(memProvider, write((const void*) &value1, sizeof(value1))).Times(1); + EXPECT_CALL(memProvider, write((const void*) &value2, sizeof(value2))).Times(1); + + WritableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.writeItem(value1)); + EXPECT_EQ(mem.getRemainingSize(), 2ul); + EXPECT_TRUE(mem.writeItem(value2)); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + EXPECT_FALSE(mem.writeItem(value1)); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} + +TEST(WritableMemory, c_writeMultipleItems) +{ + ::testing::StrictMock memProvider; + + const uint8_t value1 = 12; + const uint16_t value2 = 345; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read(_, _)).Times(0); + EXPECT_CALL(memProvider, write((const void*) &value1, sizeof(value1))).Times(1); + EXPECT_CALL(memProvider, write((const void*) &value2, sizeof(value2))).Times(1); + + WritableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.writeItems(value1, value2)); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + EXPECT_FALSE(mem.writeItems(value1, value2)); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} + +TEST(WritableMemory, d_writeRaw) +{ + ::testing::StrictMock memProvider; + + const uint8_t value1 = 12; + const uint16_t value2 = 234; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read(_, _)).Times(0); + EXPECT_CALL(memProvider, write((const void*) &value1, sizeof(value1))).Times(1); + EXPECT_CALL(memProvider, write((const void*) &value2, sizeof(value2))).Times(1); + + WritableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.writeRaw((void*) &value1, sizeof(value1))); + EXPECT_EQ(mem.getRemainingSize(), 2ul); + EXPECT_TRUE(mem.writeRaw((void*) &value2, sizeof(value2))); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + EXPECT_FALSE(mem.writeRaw((void*) &value1, sizeof(value1))); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} + +///////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////// + +TEST(ReadableMemory, a_dontReadOnConstruction) +{ + ::testing::StrictMock memProvider; + + // call getAvailableSize() just once + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(2ul)); + // No other calls expected. + EXPECT_CALL(memProvider, read(_, _)).Times(0); + EXPECT_CALL(memProvider, write(_, _)).Times(0); + + ReadableMemory mem(memProvider); +} + +TEST(ReadableMemory, b_readSingleitem) +{ + ::testing::StrictMock memProvider; + + uint8_t value1 = 0; + uint16_t value2 = 0; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read((void*) &value1, sizeof(value1))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint8_t*) dest) = 12u; + })); + EXPECT_CALL(memProvider, read((void*) &value2, sizeof(value2))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint16_t*) dest) = 234u; + })); + EXPECT_CALL(memProvider, write(_, _)).Times(0); + + ReadableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.readItem(value1)); + EXPECT_EQ(value1, 12); + EXPECT_EQ(mem.getRemainingSize(), 2ul); + EXPECT_TRUE(mem.readItem(value2)); + EXPECT_EQ(value2, 234); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + + uint8_t value3 = 123; + EXPECT_FALSE(mem.readItem(value3)); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} + +TEST(ReadableMemory, c_readMultipleItems) +{ + ::testing::StrictMock memProvider; + + uint8_t value1 = 0; + uint16_t value2 = 0; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read((void*) &value1, sizeof(value1))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint8_t*) dest) = 12u; + })); + EXPECT_CALL(memProvider, read((void*) &value2, sizeof(value2))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint16_t*) dest) = 234u; + })); + EXPECT_CALL(memProvider, write(_, _)).Times(0); + + ReadableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.readItems(value1, value2)); + EXPECT_EQ(value1, 12); + EXPECT_EQ(value2, 234); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + + uint8_t value3 = 123; + EXPECT_FALSE(mem.readItem(value3)); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} + +TEST(ReadableMemory, d_readRaw) +{ + ::testing::StrictMock memProvider; + + uint8_t value1 = 0; + uint16_t value2 = 0; + + ::testing::Sequence expectCallToHappenInSequence; + + EXPECT_CALL(memProvider, getAvailableSize()) + .WillOnce(::testing::Return(3ul)); + EXPECT_CALL(memProvider, read((void*) &value1, sizeof(value1))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint8_t*) dest) = 12u; + })); + EXPECT_CALL(memProvider, read((void*) &value2, sizeof(value2))) + .WillOnce(testing::Invoke([](void* dest, size_t) { + *((uint16_t*) dest) = 234u; + })); + EXPECT_CALL(memProvider, write(_, _)).Times(0); + + ReadableMemory mem(memProvider); + EXPECT_EQ(mem.getRemainingSize(), 3ul); + EXPECT_TRUE(mem.readRaw((void*) &value1, sizeof(value1))); + EXPECT_EQ(value1, 12); + EXPECT_EQ(mem.getRemainingSize(), 2ul); + EXPECT_TRUE(mem.readRaw((void*) &value2, sizeof(value2))); + EXPECT_EQ(value2, 234); + EXPECT_EQ(mem.getRemainingSize(), 0ul); + + uint8_t value3 = 123; + EXPECT_FALSE(mem.readRaw((void*) &value3, sizeof(value3))); // out of memory now! + EXPECT_EQ(mem.getRemainingSize(), 0ul); +} diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index da85cf7..2f438a9 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -47,6 +47,7 @@ juce_add_plugin(TapeLooperPlugin # GarageBand 10.3 requires the first letter to be upper-case, and the remaining letters to be lower-case FORMATS AU VST3 Standalone # The formats to build. Other valid formats are: AAX Unity VST AU AUv3 PRODUCT_NAME "Tape Looper Plugin") # The name of the final executable, which can differ from the target name +set_property(TARGET TapeLooperPlugin PROPERTY CXX_STANDARD 17) # `juce_generate_juce_header` will create a JuceHeader.h for a given target, which will be generated # into your build tree. This should be included with `#include `. The include path for @@ -151,7 +152,7 @@ set( # This target adds a googletest executable add_executable(TapeLooperPlugin_Gtest) -set_target_properties (TapeLooperPlugin PROPERTIES +set_target_properties (TapeLooperPlugin_Gtest PROPERTIES FOLDER TapeLooperPlugin ) @@ -178,4 +179,10 @@ target_link_libraries(TapeLooperPlugin_Gtest juce::juce_recommended_config_flags juce::juce_recommended_lto_flags juce::juce_recommended_warning_flags - ) \ No newline at end of file + ) + + +set_property(TARGET TapeLooperPlugin_Gtest + PROPERTY + CXX_STANDARD 17 +) \ No newline at end of file diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 1e254df..25a95f9 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -134,17 +134,36 @@ juce::AudioProcessorEditor* TapeLooperPluginAudioProcessor::createEditor() //============================================================================== void TapeLooperPluginAudioProcessor::getStateInformation(juce::MemoryBlock& destData) { - // You should use this method to store your parameters in the memory block. - // You could do that either as raw data, or use the XML or ValueTree classes - // as intermediaries to make it easy to save and load complex data. - juce::ignoreUnused(destData); + try + { + loopers_.saveState(destData); + } + catch (const std::exception& e) + { + juce::NativeMessageBox::showAsync( + juce::MessageBoxOptions() + .withTitle(juce::String("Exception saving state")) + .withMessage(juce::String("Exception saving state: ") + e.what()) + .withIconType(juce::MessageBoxIconType::WarningIcon), + [](int) {}); + } } void TapeLooperPluginAudioProcessor::setStateInformation(const void* data, int sizeInBytes) { - // You should use this method to restore your parameters from this memory block, - // whose contents will have been created by the getStateInformation() call. - juce::ignoreUnused(data, sizeInBytes); + try + { + loopers_.recallState(data, size_t(sizeInBytes)); + } + catch (const std::exception& e) + { + juce::NativeMessageBox::showAsync( + juce::MessageBoxOptions() + .withTitle(juce::String("Exception recalling state")) + .withMessage(juce::String("Exception recalling state: ") + e.what()) + .withIconType(juce::MessageBoxIconType::WarningIcon), + [](int) {}); + } } juce::AudioProcessorValueTreeState::ParameterLayout TapeLooperPluginAudioProcessor::getParameterLayout() diff --git a/plugin/src/TapeLooperProcessor.h b/plugin/src/TapeLooperProcessor.h index dc21e89..8af395e 100644 --- a/plugin/src/TapeLooperProcessor.h +++ b/plugin/src/TapeLooperProcessor.h @@ -1,10 +1,62 @@ #pragma once #include "DspDefinitions.h" +#include #include #include #include +#include + +class MemoryBlockStorageProvider +{ +public: + MemoryBlockStorageProvider(juce::MemoryBlock& mem) : + size_(mem.getSize()), + ptr_((uint8_t*) mem.getData()) + { + } + + constexpr size_t getAvailableSize() const { return size_; } + + void write(const void* srcData, size_t sizeToWrite) + { + if (size_ >= sizeToWrite) + { + memcpy(ptr_, srcData, sizeToWrite); + ptr_ += sizeToWrite; + size_ -= sizeToWrite; + } + } + + size_t size_; + uint8_t* ptr_; +}; + +class RawMemoryStorageProvider +{ +public: + RawMemoryStorageProvider(const void* data, size_t size) : + size_(size), + ptr_((uint8_t*) data) + { + } + + constexpr size_t getAvailableSize() const { return size_; } + + void read(void* destData, size_t sizeToRead) + { + if (size_ >= sizeToRead) + { + memcpy(destData, ptr_, sizeToRead); + ptr_ += sizeToRead; + size_ -= sizeToRead; + } + } + + size_t size_; + uint8_t* ptr_; +}; class ITapeLooperProcessor { @@ -14,6 +66,9 @@ class ITapeLooperProcessor virtual void prepareToPlay() = 0; virtual void processBlock(juce::dsp::AudioBlock input, juce::dsp::AudioBlock outputToAddTo) = 0; + virtual size_t getStateSizeInBytes() const = 0; + virtual void saveState(WritableMemory& mem) const = 0; + virtual void recallState(ReadableMemory& mem) = 0; }; template @@ -92,6 +147,62 @@ class TapeLooperProcessor : public ITapeLooperProcessor outChPtrs, outputToAddTo.getNumSamples())); } + size_t getStateSizeInBytes() const override + { + return looper_.getSaveAndRecallStorageSize() + + sizeof(ParameterValues); + } + + void saveState(WritableMemory& mem) const override + { + if (mem.getRemainingSize() < getStateSizeInBytes()) + throw std::runtime_error("Not enough memory to store looper state"); + + bool result = true; + + result &= looper_.save(mem); + + ParameterValues params; + params.state = uint8_t(stateParameter_.getIndex()); + params.speed = speedParameter_.get(); + params.drive = driveParameter_.get(); + params.grainAmt = grainAmtParameter_.get(); + params.wowAndFlutter = wowAndFlutterAmtParameter_.get(); + params.postGain = postGainParameter_.get(); + result &= mem.writeItem(params); + + if (!result) + throw std::runtime_error("Error saving looper state"); + } + + void recallState(ReadableMemory& mem) override + { + if (mem.getRemainingSize() < getStateSizeInBytes()) + throw std::runtime_error("Not enough memory to restore looper state"); + + bool result = true; + + result &= looper_.restore(mem); + + ParameterValues params; + result &= mem.readItem(params); + + if (!result) + throw std::runtime_error("Error loading looper state"); + + const auto setParam = [](juce::RangedAudioParameter& param, float value) { + param.beginChangeGesture(); + param.setValueNotifyingHost(param.convertTo0to1(value)); + param.endChangeGesture(); + }; + setParam(stateParameter_, float(params.state)); + setParam(speedParameter_, params.speed); + setParam(driveParameter_, params.drive); + setParam(grainAmtParameter_, params.grainAmt); + setParam(wowAndFlutterAmtParameter_, params.wowAndFlutter); + setParam(postGainParameter_, params.postGain); + } + private: juce::AudioParameterChoice& stateParameter_; juce::AudioParameterFloat& speedParameter_; @@ -100,7 +211,18 @@ class TapeLooperProcessor : public ITapeLooperProcessor juce::AudioParameterFloat& wowAndFlutterAmtParameter_; juce::AudioParameterFloat& postGainParameter_; - TapeLooper looper_; + struct ParameterValues + { + uint8_t state; + float speed; + float drive; + float grainAmt; + float wowAndFlutter; + float postGain; + }; + + using TapeLooperType = TapeLooper; + TapeLooperType looper_; }; class TapeLooperBank @@ -114,7 +236,7 @@ class TapeLooperBank void prepareToPlay(const juce::dsp::ProcessSpec& specs) { - inputTmpBuffer_.setSize(2, specs.maximumBlockSize); + inputTmpBuffer_.setSize(2, int(specs.maximumBlockSize)); inputTmpBuffer_.clear(); createInstancesFor(specs.sampleRate); @@ -138,6 +260,29 @@ class TapeLooperBank processor->processBlock(inputBuffer, buffer); } + void saveState(juce::MemoryBlock& memory) + { + size_t sizeNeeded = 0; + for (auto processor : processors_) + sizeNeeded += processor->getStateSizeInBytes(); + + memory.setSize(sizeNeeded, true); + MemoryBlockStorageProvider storageProvider(memory); + WritableMemory writableMem(storageProvider); + + for (auto processor : processors_) + processor->saveState(writableMem); + } + + void recallState(const void* data, size_t sizeInBytes) + { + RawMemoryStorageProvider storageProvider(data, sizeInBytes); + ReadableMemory readableMem(storageProvider); + + for (auto processor : processors_) + processor->recallState(readableMem); + } + private: void createInstancesFor(double sampleRate) {