diff --git a/.github/workflows/firmware.yaml b/.github/workflows/firmware.yaml index 3512b08..2d3a661 100644 --- a/.github/workflows/firmware.yaml +++ b/.github/workflows/firmware.yaml @@ -22,9 +22,9 @@ jobs: steps: - name: Install arm-none-eabi-gcc - uses: fiam/arm-none-eabi-gcc@v1 + uses: carlosperate/arm-none-eabi-gcc-action@v1 with: - release: '9-2019-q4' # The arm-none-eabi-gcc release to use. + release: '10-2020-q4' - name: Setup cmake uses: jwlawson/actions-setup-cmake@v1.9 @@ -37,19 +37,19 @@ jobs: submodules: recursive - name: Configure - run: cmake -S firmware -B firmware/build -G "Unix Makefiles" -D CMAKE_TOOLCHAIN_FILE="$(pwd)/lib/libDaisy/cmake/toolchains/stm32h750xx.cmake" -D CMAKE_BUILD_TYPE=RelWithDebInfo -D TOOLCHAIN_PREFIX=$(which arm-none-eabi-gcc | sed -e "s/bin\/arm-none-eabi-gcc//") + run: cmake -S firmware -B firmware/build -G "Unix Makefiles" -D CMAKE_BUILD_TYPE=RelWithDebInfo -D TOOLCHAIN_PREFIX=$(which arm-none-eabi-gcc | sed -e "s/bin\/arm-none-eabi-gcc//") - name: Build run: cmake --build firmware/build - name: Upload Firmware hex - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: TapeLooper.hex path: firmware/build/TapeLooper.hex - name: Upload Firmware bin - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: TapeLooper.bin path: firmware/build/TapeLooper.bin @@ -72,14 +72,14 @@ jobs: - name: Run Tests run: | - cd firmware/tests/build/bin - ./TapeLooper_gtest --gtest_output=xml:gtestresults.xml + cd firmware/tests + ./build/bin/TapeLooper_gtest --gtest_output=xml:gtestresults.xml - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v1 if: always() with: - files: firmware/tests/build/bin/**/*.xml + files: firmware/tests/**/*.xml github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/plugin.yaml b/.github/workflows/plugin.yaml deleted file mode 100644 index d5ae327..0000000 --- a/.github/workflows/plugin.yaml +++ /dev/null @@ -1,180 +0,0 @@ -name: Plugin Checks And Builds -on: - # run for all pull requests - # Run for all commits (e.g. on feature branches) - push: - paths: - - 'dsp/**' - - 'plugin/**' - - 'lib/**' - - '.github/workflows/**' - pull_request: - paths: - - 'dsp/**' - - 'plugin/**' - - 'lib/**' - - '.github/workflows/**' -jobs: - ############################################################################### - # builds the plugin on windows - testBuildWindows: - runs-on: windows-2019 - - steps: - - name: Setup cmake - uses: jwlawson/actions-setup-cmake@v1.8 - with: - cmake-version: '3.30.x' - - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Configure - run: | - cmake.exe -G "Visual Studio 16 2019" -A x64 -B plugin/build -S plugin - - - name: Build - run: | - cmake --build plugin/build --config Release - - - name: Run Tests - run: | - plugin/build/Release/TapeLooperPlugin_Gtest.exe --gtest_output=xml:TestResults\gtestResults_windows.xml - - - name: Upload Test Results - uses: actions/upload-artifact@v2 - with: - name: TestResults - path: TestResults/** - - - name: Upload executable - uses: actions/upload-artifact@v2 - with: - name: TapeLooperPlugin_Windows - path: | - plugin\build\TapeLooperPlugin_artefacts\Release\VST3\Tape Looper Plugin.vst3\Contents\x86_64-win\Tape Looper Plugin.vst3 - plugin\build\TapeLooperPlugin_artefacts\Release\Standalone\Tape Looper Plugin.exe - plugin/build/Release/TapeLooperPlugin_Gtest.exe - TestResults\gtestResults_windows.xml - ############################################################################### - # builds the plugin on macos - testBuildMacOS: - runs-on: macos-latest - - steps: - - name: Setup cmake - uses: jwlawson/actions-setup-cmake@v1.8 - with: - cmake-version: '3.30.x' - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Select Xcode - run: | - sudo xcode-select --reset - - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Configure - run: | - cmake -G "Xcode" -B plugin/build -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -S plugin - - - name: Build - run: | - cmake --build plugin/build --config Release - - - name: Run Tests - run: | - plugin/build/Release/TapeLooperPlugin_Gtest --gtest_output=xml:TestResults/gtestResults_macos.xml - - - name: Upload Test Results - uses: actions/upload-artifact@v2 - with: - name: TestResults - path: TestResults/** - - - name: Upload executable - uses: actions/upload-artifact@v2 - with: - name: TapeLooperPlugin_MacOS - path: | - plugin/build/TapeLooperPlugin_artefacts/Release/VST3/Tape Looper Plugin.vst3/** - plugin/build/TapeLooperPlugin_artefacts/Release/Standalone/Tape Looper Plugin.app/** - plugin/build/Release/TapeLooperPlugin_Gtest - TestResults/gtestResults_macos.xml - ############################################################################### - # builds the plugin on ubuntu - testBuildUbuntu: - runs-on: ubuntu-latest - - steps: - - name: Setup cmake - uses: jwlawson/actions-setup-cmake@v1.8 - with: - cmake-version: '3.30.x' - - - name: Setup dependencies - run: | - sudo apt-get update - sudo apt-get -y install g++ libfreetype6-dev libx11-dev libxinerama-dev libxrandr-dev libxcursor-dev mesa-common-dev libasound2-dev freeglut3-dev libxcomposite-dev - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Configure - run: | - cmake -G "Unix Makefiles" -B plugin/build -S plugin - - - name: Build - run: | - cmake --build plugin/build --config Release - - - name: Run Tests - run: | - plugin/build/TapeLooperPlugin_Gtest --gtest_output=xml:TestResults/gtestResults_ubuntu.xml - - - name: Upload Test Results - uses: actions/upload-artifact@v2 - with: - name: TestResults - path: TestResults/** - - - name: Upload executable - uses: actions/upload-artifact@v2 - with: - name: TapeLooperPlugin_Linux - path: | - plugin/build/TapeLooperPlugin_artefacts/VST3/Tape Looper Plugin.vst3 - plugin/build/TapeLooperPlugin_artefacts/Standalone/Tape Looper Plugin - plugin/build/TapeLooperPlugin_Gtest - TestResults/gtestResults_ubuntu.xml - ############################################################################### - # uploads test results from the previous jobs - publishTestResults: - name: "Publish Unit Tests Results" - needs: [testBuildUbuntu, testBuildMacOS, testBuildWindows] - runs-on: ubuntu-latest - # the dependency jobs might be skipped, we don't need to run this job then - if: success() || failure() - - steps: - - name: Download Artifacts - uses: actions/download-artifact@v3 - with: - name: TestResults - path: TestResults - - - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 - if: always() - with: - files: TestResults/**/*.xml - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index dd4a973..1713b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ plugin/build # macOS crap .DS_Store +firmware/.cache diff --git a/.vscode/launch.json b/.vscode/launch.json index b1df9b1..34d3657 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,12 +34,12 @@ }, "linux": { "MIMode": "gdb", - "MIDebuggerPath": "~/dev/gcc-arm-none-eabi-9-2020-q2-update/bin/arm-none-eabi-gdb", + "MIDebuggerPath": "~/dev/gcc-arm-none-eabi-10-2020-q4-major/bin/arm-none-eabi-gdb", "debugServerPath": "openocd" }, "osx": { "MIMode": "gdb", - "MIDebuggerPath": "/Users/johanneselliesen/dev/gcc-arm-none-eabi-9-2020-q2-update/bin/arm-none-eabi-gdb", + "MIDebuggerPath": "/Users/johanneselliesen/dev/gcc-arm-none-eabi-10-2020-q4-major/bin/arm-none-eabi-gdb", "debugServerPath": "openocd" }, "windows": { @@ -91,7 +91,7 @@ "program": "${workspaceFolder}/firmware/tests/build/bin/TapeLooper_gtest", "args": [], "stopAtEntry": false, - "cwd": "${workspaceFolder}/firmware/tests/build/bin", + "cwd": "${workspaceFolder}/firmware/tests/", "environment": [], "externalConsole": false, "logging": { @@ -106,53 +106,6 @@ "windows": { "MIMode": "gdb", } - }, - { - "name": "Plugin Tests", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/plugin/build/TapeLooperPlugin_Gtest", - "args": [], - "stopAtEntry": false, - "cwd": "${workspaceFolder}/plugin/build", - "environment": [], - "externalConsole": false, - "logging": { - "engineLogging": false - }, - "preLaunchTask": "build plugin", - - "osx": { - "type": "lldb", - "MIMode": "lldb", - }, - "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": { - "type": "lldb", - "MIMode": "lldb", - "program": "${workspaceFolder}/plugin/build/TapeLooperPlugin_artefacts/Standalone/Tape Looper Plugin.app/Contents/MacOS/Tape Looper Plugin", - }, - "windows": { - "MIMode": "gdb", - "program": "${workspaceFolder}/plugin/build/TapeLooperPlugin_artefacts/Standalone/Tape Looper Plugin.exe", - } } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8b75eb1..6f6c9d5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,9 +17,7 @@ "command": "cmake", "args": [ "-D", - "TOOLCHAIN_PREFIX=\"${env:HOME}/dev/gcc-arm-none-eabi-9-2020-q2-update\"", - "-D", - "CMAKE_TOOLCHAIN_FILE=\"${workspaceRoot}/lib/libDaisy/cmake/toolchains/stm32h750xx.cmake\"", + "TOOLCHAIN_PREFIX=\"${env:HOME}/dev/gcc-arm-none-eabi-10-2020-q4-major\"", "-D", "CMAKE_BUILD_TYPE=RelWithDebInfo", "-S", @@ -38,9 +36,7 @@ "command": "cmake.exe", "args": [ "-D", - "TOOLCHAIN_PREFIX=\\\"C:/Program Files (x86)/GNU Arm Embedded Toolchain/9 2020-q2-update\\\"", - "-D", - "CMAKE_TOOLCHAIN_FILE=\\\"${workspaceRoot}/lib/libDaisy/cmake/toolchains/stm32h750xx.cmake\\\"", + "TOOLCHAIN_PREFIX=\\\"C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2020-q4-major\\\"", "-D", "CMAKE_BUILD_TYPE=RelWithDebInfo", "-S", @@ -64,9 +60,7 @@ "command": "cmake", "args": [ "-D", - "TOOLCHAIN_PREFIX=\"${env:HOME}/dev/gcc-arm-none-eabi-9-2020-q2-update\"", - "-D", - "CMAKE_TOOLCHAIN_FILE=\"${workspaceRoot}/lib/libDaisy/cmake/toolchains/stm32h750xx.cmake\"", + "TOOLCHAIN_PREFIX=\"${env:HOME}/dev/gcc-arm-none-eabi-10-2020-q4-major\"", "-D", "CMAKE_BUILD_TYPE=Debug", "-S", @@ -85,9 +79,7 @@ "command": "cmake.exe", "args": [ "-D", - "TOOLCHAIN_PREFIX=\\\"C:/Program Files (x86)/GNU Arm Embedded Toolchain/9 2020-q2-update\\\"", - "-D", - "CMAKE_TOOLCHAIN_FILE=\\\"${workspaceRoot}/lib/libDaisy/cmake/toolchains/stm32h750xx.cmake\\\"", + "TOOLCHAIN_PREFIX=\\\"C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2020-q4-major\\\"", "-D", "CMAKE_BUILD_TYPE=Debug", "-S", @@ -149,123 +141,6 @@ }, "problemMatcher": [] }, - { - "label": "configure plugin", - "type": "shell", - "command": "cmake", - "args": [ - "-S", - "plugin", - "-B", - "plugin/build", - "-D", - "CMAKE_EXPORT_COMPILE_COMMANDS=1", - "-G", - "\"Unix Makefiles\"" - ], - "options": { - "cwd": "${workspaceRoot}" - }, - "windows": { - "command": "cmake.exe", - "args": [ - "-S", - "plugin", - "-B", - "plugin/build", - "-D", - "CMAKE_EXPORT_COMPILE_COMMANDS=1", - "-G", - "\\\"Visual Studio 16 2019\\\"" - ] - }, - "group": { - "kind": "build" - }, - "problemMatcher": [] - }, - { - "label": "configure plugin (debug)", - "type": "shell", - "command": "cmake", - "args": [ - "-D", - "CMAKE_BUILD_TYPE=Debug", - "-S", - "plugin", - "-B", - "plugin/build", - "-D", - "CMAKE_EXPORT_COMPILE_COMMANDS=1", - "-G", - "\"Unix Makefiles\"" - ], - "options": { - "cwd": "${workspaceRoot}" - }, - "windows": { - "command": "cmake.exe", - "args": [ - "-D", - "CMAKE_BUILD_TYPE=Debug", - "-S", - "plugin", - "-B", - "plugin/build", - "-D", - "CMAKE_EXPORT_COMPILE_COMMANDS=1", - "-G", - "\\\"Visual Studio 16 2019\\\"" - ] - }, - "group": { - "kind": "build" - }, - "problemMatcher": [] - }, - { - "label": "build plugin", - "type": "shell", - "command": "cmake", - "args": [ - "--build", - "plugin/build" - ], - "options": { - "cwd": "${workspaceRoot}" - }, - "group": { - "kind": "build" - }, - "problemMatcher": [ - "$gcc" - ] - }, - { - "label": "clean-plugin", - "osx": { - "command": "make", - "args": [ - "clean" - ] - }, - "linux": { - "command": "make", - "args": [ - "clean" - ] - }, - "windows": { - "command": "make.exe", - "args": [ - "clean" - ] - }, - "options": { - "cwd": "${workspaceRoot}/plugin/build" - }, - "problemMatcher": [] - }, { "label": "buildTests-TapeLooperDSP", "command": "make", diff --git a/README.md b/README.md index 7205bec..0c44599 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ [![DSP Library Checks](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/dsp.yaml/badge.svg)](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/dsp.yaml) [![Firmware Checks And Builds](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/firmware.yaml/badge.svg)](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/firmware.yaml) -[![Plugin Checks And Builds](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/plugin.yaml/badge.svg)](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/plugin.yaml) [![Formatting](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/formatting.yaml/badge.svg)](https://github.com/TheSlowGrowth/TapeLooper/actions/workflows/formatting.yaml) -Hey explorer! This repo is just a construction site. We're trying to build a digital tape looper, that will hopefully become a desktop device and / or a Eurorack synth module. +Hey explorer! This repo is just a construction site. We're trying to build a digital tape looper, that will hopefully become a desktop device and / or a Eurorack synth module. diff --git a/dsp/src/dsp/Recorder.h b/dsp/src/dsp/Recorder.h index 6645163..21310e8 100644 --- a/dsp/src/dsp/Recorder.h +++ b/dsp/src/dsp/Recorder.h @@ -62,6 +62,15 @@ class Recorder recHeadIdx_ = 0; } + void stopRecordingImmediately() + { + isRecording_ = false; + isFadingOut_ = false; + xfadePhase_ = 0.0f; + currentLength_ = recHeadIdx_; + recHeadIdx_ = 0; + } + void setCrossfadeLength(size_t crossfadeLengthInSamples) { xFadeLengthInSamples_ = std::min(crossfadeLengthInSamples, buffer_.size_); @@ -131,9 +140,12 @@ class Recorder return true; } + void setCurrentPlaybackLength(size_t length) { currentLength_ = length; } size_t getCurrentRecordingLength() const { return currentLength_; } bool isRecording() const { return isRecording_ || isFadingOut_; } + void setRecordingProgressForTesting(size_t numSamplesDone) { recHeadIdx_ = numSamplesDone; } + private: static constexpr float defaultXFadeLengthInS_ = 0.1f; diff --git a/dsp/src/dsp/TapeLooper.h b/dsp/src/dsp/TapeLooper.h index 788ec42..75f53a7 100644 --- a/dsp/src/dsp/TapeLooper.h +++ b/dsp/src/dsp/TapeLooper.h @@ -107,9 +107,9 @@ class TapeLooper { } - void switchState(LooperState state) + void switchState(LooperState newState) { - switch (state) + switch (newState) { default: case LooperState::stopped: @@ -119,19 +119,25 @@ class TapeLooper player_.stopPlaying(); break; case LooperState::playing: + if (preventPlayback_) + return; if (state_ == LooperState::recording) recorder_.stopRecording(); player_.startPlaying(recorder_.getCurrentRecordingLength()); break; case LooperState::recording: + if (preventRecording_) + return; if (state_ == LooperState::playing) player_.stopPlaying(); recorder_.startRecording(); break; } - state_ = state; + state_ = newState; } + bool isRecording() const { return recorder_.isRecording(); } + LooperState getState() const { return state_; } void reset() @@ -205,6 +211,8 @@ class TapeLooper template bool restore(ReadableMemory& mem) { + if (preventRecording_) + return false; if (!recorder_.restore(mem)) return false; @@ -222,11 +230,51 @@ class TapeLooper return true; } - const LooperStoragePtr getSampleStoragePtr() const { return storage_; } + void preventPlaybackAndRecording(bool stopRecordingImmediately = false) + { + preventPlayback_ = true; + preventRecording_ = true; + const auto previousState = getState(); + switchState(LooperState::stopped); + if (previousState == LooperState::recording) + { + if (stopRecordingImmediately) + recorder_.stopRecordingImmediately(); + } + } + + void preventRecording(bool stopRecordingImmediately = false) + { + preventPlayback_ = false; + preventRecording_ = true; + + if (getState() == LooperState::recording) + { + switchState(LooperState::stopped); + if (stopRecordingImmediately) + recorder_.stopRecordingImmediately(); + } + } + + void liftRestrictions() + { + preventPlayback_ = false; + preventRecording_ = false; + } + + LooperStoragePtr getSampleStoragePtr() { return storage_; } + void setPlaybackLength(size_t length) { recorder_.setCurrentPlaybackLength(length); } + size_t getPlaybackLength() const { return recorder_.getCurrentRecordingLength(); } + static constexpr size_t getNumChannels() { return numChannels; } + static constexpr size_t getSampleRate() { return sampleRate; } + + RecorderType& recorderForTesting() { return recorder_; } private: const LooperStoragePtr storage_; LooperState state_; PlayerType player_; RecorderType recorder_; + bool preventPlayback_ = false; + bool preventRecording_ = false; }; diff --git a/dsp/tests/Recorder_gtest.cpp b/dsp/tests/Recorder_gtest.cpp index 068822e..c93b42c 100644 --- a/dsp/tests/Recorder_gtest.cpp +++ b/dsp/tests/Recorder_gtest.cpp @@ -97,12 +97,15 @@ TEST_F(DSP_Recorder, b_recordAndStop) { xfade += xFadeIncrement_; const float expectedSample = xfade * inputSequence_[ch][i] - + (1.0f - xfade) * inputSequence_[ch][i + expectedTotalNumSamples]; - EXPECT_NEAR(expectedSample, storage_[ch][i], absError_) << "at i=" << i; + + (1.0f - xfade) + * inputSequence_[ch][i + expectedTotalNumSamples]; + EXPECT_NEAR(expectedSample, storage_[ch][i], absError_) + << "at ch=" << ch << ", at i=" << i; } // compare the samples in the non-crossfaded portion for (size_t i = xFadeLengthInSamples_; i < expectedTotalNumSamples; i++) - EXPECT_NEAR(inputSequence_[ch][i], storage_[ch][i], absError_) << "at i=" << i; + EXPECT_NEAR(inputSequence_[ch][i], storage_[ch][i], absError_) + << "at ch=" << ch << ", at i=" << i; } if (HasFailure()) @@ -127,4 +130,30 @@ TEST_F(DSP_Recorder, b_recordAndStop) std::cout << "\n"; } } +} + +TEST_F(DSP_Recorder, c_stopRecordingImmediately) +{ + fillInputSequence(); + + recorder_.startRecording(); + // record some samples, but don't fill the recording buffer yet + constexpr auto kNumSamples = blockSize_ / 2; + recorder_.process(inputSequence_.subBlock(0, kNumSamples)); + + recorder_.stopRecordingImmediately(); + + // No longer recording - no crossfading + EXPECT_FALSE(recorder_.isRecording()); + + // The recorded data is preserved + EXPECT_EQ(recorder_.getCurrentRecordingLength(), kNumSamples); + for (size_t ch = 0; ch < numChannels_; ch++) + { + for (size_t i = 0; i < kNumSamples; i++) + { + EXPECT_NEAR(inputSequence_[ch][i], storage_[ch][i], absError_) + << "at ch=" << ch << ", at i=" << i; + } + } } \ No newline at end of file diff --git a/dsp/tests/TapeLooper_gtest.cpp b/dsp/tests/TapeLooper_gtest.cpp new file mode 100644 index 0000000..c0aa3a6 --- /dev/null +++ b/dsp/tests/TapeLooper_gtest.cpp @@ -0,0 +1,177 @@ +/** + * Copyright (C) Johannes Elliesen, 2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +class TapeLooperFixture : public ::testing::Test +{ +public: + TapeLooperFixture() : + looper_(storage_) {} + + void processOneBuffer() + { + TapeProcessorParameters processorParameters; + processorParameters.driveGain = 1.0f; + processorParameters.grainAmt = 0.0f; + AudioBuffer<1, kNumSamples> inBuffer; + AudioBuffer<1, kNumSamples> outBuffer; + looper_.process(1.0f, 0.0f, Direction::forwards, processorParameters, 1.0f, inBuffer, outBuffer); + } + + void recordSomeAudio() + { + looper_.switchState(LooperState::recording); + processOneBuffer(); + looper_.switchState(LooperState::stopped); + processOneBuffer(); + EXPECT_FALSE(looper_.isRecording()); + } + + static constexpr auto kSampleRate = 100; + static constexpr auto kNumSamples = 100; + + LooperStorage storage_; + TapeLooper looper_; +}; + +TEST_F(TapeLooperFixture, a_preventRecordingStopsCurrentRecording) +{ + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); + + looper_.preventRecording(); + // looper aborts the recording + EXPECT_EQ(looper_.getState(), LooperState::stopped); + // recorder is still crossfading out + EXPECT_TRUE(looper_.isRecording()); + + // after processing for a while + processOneBuffer(); + // recorder has stopped + EXPECT_FALSE(looper_.isRecording()); +} + +TEST_F(TapeLooperFixture, b_preventRecordingImmediatelyStopsCurrentRecordingWithoutCrossfade) +{ + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); + + looper_.preventRecording(true); + // looper aborts the recording + EXPECT_EQ(looper_.getState(), LooperState::stopped); + // recorder has stopped immediately + EXPECT_FALSE(looper_.isRecording()); +} + +TEST_F(TapeLooperFixture, c_preventRecordingPreventsStartingARecording) +{ + looper_.switchState(LooperState::stopped); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.preventRecording(); + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.liftRestrictions(); + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); +} + +TEST_F(TapeLooperFixture, d_preventRecordingStillAllowsPlayback) +{ + recordSomeAudio(); + + looper_.switchState(LooperState::stopped); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.preventRecording(); + looper_.switchState(LooperState::playing); + EXPECT_EQ(looper_.getState(), LooperState::playing); +} + +TEST_F(TapeLooperFixture, e_preventPlaybackAndRecordingStopsPlayback) +{ + recordSomeAudio(); + + looper_.switchState(LooperState::playing); + EXPECT_EQ(looper_.getState(), LooperState::playing); + + looper_.preventPlaybackAndRecording(); + // looper aborts the playback + EXPECT_EQ(looper_.getState(), LooperState::stopped); +} + +TEST_F(TapeLooperFixture, f_preventPlaybackAndRecordingStopsCurrentRecording) +{ + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); + + looper_.preventPlaybackAndRecording(); + // looper aborts the recording + EXPECT_EQ(looper_.getState(), LooperState::stopped); + // recorder is still crossfading out + EXPECT_TRUE(looper_.isRecording()); + + // after processing for a while + processOneBuffer(); + // recorder has stopped + EXPECT_FALSE(looper_.isRecording()); +} + +TEST_F(TapeLooperFixture, g_preventPlaybackAndRecordingImmediatelyStopsCurrentRecordingWithoutCrossfade) +{ + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); + + looper_.preventPlaybackAndRecording(true); + // looper aborts the recording + EXPECT_EQ(looper_.getState(), LooperState::stopped); + // recorder has stopped + EXPECT_FALSE(looper_.isRecording()); +} + +TEST_F(TapeLooperFixture, h_preventPlaybackAndRecordingPreventsStartingARecording) +{ + looper_.switchState(LooperState::stopped); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.preventPlaybackAndRecording(); + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.liftRestrictions(); + looper_.switchState(LooperState::recording); + EXPECT_EQ(looper_.getState(), LooperState::recording); +} + +TEST_F(TapeLooperFixture, i_preventPlaybackAndRecordingPreventsStartingPlayback) +{ + recordSomeAudio(); + + looper_.switchState(LooperState::stopped); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.preventPlaybackAndRecording(); + looper_.switchState(LooperState::playing); + EXPECT_EQ(looper_.getState(), LooperState::stopped); + + looper_.liftRestrictions(); + looper_.switchState(LooperState::playing); + EXPECT_EQ(looper_.getState(), LooperState::playing); +} \ No newline at end of file diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 255a826..37da238 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -1,27 +1,44 @@ -cmake_policy(SET CMP0048 NEW) -project(TapeLooper) -cmake_minimum_required(VERSION 3.19) -set(FIRMWARE_NAME TapeLooper) +cmake_minimum_required(VERSION 3.30) + +if(WIN32) + set(TOOLCHAIN_EXT ".exe") +else() + set(TOOLCHAIN_EXT "") +endif() + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}/bin/arm-none-eabi-gcc${TOOLCHAIN_EXT}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}/bin/arm-none-eabi-g++${TOOLCHAIN_EXT}) + +if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) + include(${CMAKE_CURRENT_LIST_DIR}/../lib/libDaisy/cmake/toolchains/ArmGNUToolchain.cmake) + #include(${CMAKE_CURRENT_LIST_DIR}/../lib/libDaisy/cmake/toolchains/autodetect.cmake) +endif() + +project(TapeLoope) + +if(COMMAND cmake_policy) + cmake_policy(SET CMP0003 NEW) +endif(COMMAND cmake_policy) +set(FIRMWARE_NAME TapeLooper) set(FIRMWARE_SOURCES + src/AudioSaveAndRecall.cpp src/main.cpp + src/util/WavFileFormat.cpp ) # include dsp library add_subdirectory(../dsp libTapeLooperDsp) # include libDaisy -set(LIBDAISY_DIR ../lib/libDaisy) -add_subdirectory(${LIBDAISY_DIR} libDaisy) +add_subdirectory(../lib/libDaisy daisy) -# linker script -get_filename_component(LIBDAISY_DIR_ABS "${LIBDAISY_DIR}" - REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") -set(LINKER_SCRIPT ${LIBDAISY_DIR_ABS}/core/STM32H750IB_flash.lds) +cmake_path(SET LOCAL_CMAKE_PATH "../lib/libDaisy/cmake") +cmake_path(ABSOLUTE_PATH LOCAL_CMAKE_PATH) -# firmware binary image -add_executable(${FIRMWARE_NAME} ${FIRMWARE_SOURCES}) +list(APPEND CMAKE_MODULE_PATH ${LOCAL_CMAKE_PATH}) +include(DaisyProject) target_link_libraries(${FIRMWARE_NAME} PRIVATE @@ -32,36 +49,4 @@ target_link_libraries(${FIRMWARE_NAME} set_target_properties(${FIRMWARE_NAME} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES - SUFFIX ".elf" - LINK_DEPENDS ${LINKER_SCRIPT} -) - -target_link_options(${FIRMWARE_NAME} PUBLIC - -T ${LINKER_SCRIPT} - -Wl,-Map=${FIRMWARE_NAME}.map,--cref - -Wl,--check-sections - -Wl,--unresolved-symbols=report-all - -Wl,--warn-common - -Wl,--warn-section-align - -Wl,--print-memory-usage -) - -add_custom_command(TARGET ${FIRMWARE_NAME} POST_BUILD - COMMAND ${CMAKE_OBJCOPY} - ARGS -O ihex - -S ${FIRMWARE_NAME}.elf - ${FIRMWARE_NAME}.hex - BYPRODUCTS - ${FIRMWARE_NAME}.hex - COMMENT "Generating HEX image" -VERBATIM) - -add_custom_command(TARGET ${FIRMWARE_NAME} POST_BUILD - COMMAND ${CMAKE_OBJCOPY} - ARGS -O binary - -S ${FIRMWARE_NAME}.elf - ${FIRMWARE_NAME}.bin - BYPRODUCTS - ${FIRMWARE_NAME}.bin - COMMENT "Generating binary image" -VERBATIM) +) \ No newline at end of file diff --git a/firmware/src/AudioSaveAndRecall.cpp b/firmware/src/AudioSaveAndRecall.cpp new file mode 100644 index 0000000..fcee53e --- /dev/null +++ b/firmware/src/AudioSaveAndRecall.cpp @@ -0,0 +1,84 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "AudioSaveAndRecall.h" + +namespace loop_library +{ + AudioFileName makeFileNameWithoutExtensionFor(const StorageBank bank, + const int slot) + { + auto filename = AudioFileName(kLibraryBaseFolder); + switch (bank) + { + case StorageBank::green: + filename.Append("green-"); + break; + case StorageBank::yellow: + filename.Append("yellow-"); + break; + case StorageBank::red: + filename.Append("red-"); + break; + default: + return ""; + } + switch (slot / 4) + { + case 0: + filename.Append("A"); + break; + case 1: + filename.Append("B"); + break; + case 2: + filename.Append("C"); + break; + case 3: + filename.Append("D"); + break; + default: + return ""; + } + switch (slot % 4) + { + case 0: + filename.Append("0"); + break; + case 1: + filename.Append("1"); + break; + case 2: + filename.Append("2"); + break; + case 3: + filename.Append("3"); + break; + default: + return ""; + } + return filename; + } + + AudioFileName makeWavFileNameFor(const StorageBank bank, + const int slot) + { + auto name = makeFileNameWithoutExtensionFor(bank, slot); + name.Append(".wav"); + return name; + } +} // namespace loop_library diff --git a/firmware/src/AudioSaveAndRecall.h b/firmware/src/AudioSaveAndRecall.h new file mode 100644 index 0000000..0514534 --- /dev/null +++ b/firmware/src/AudioSaveAndRecall.h @@ -0,0 +1,646 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "util/WavFileFormat.h" + +extern int32_t f2s32(float x); +extern float s322f(int32_t x); +extern float s242f(int32_t x); +extern float s162f(int16_t x); + +using AudioFileName = daisy::FixedCapStr<32>; + +enum class AudioSaveAndRecallResult +{ + ok, + error +}; + +enum class StorageBank +{ + green, + yellow, + red, + + count +}; + +typedef void (*AudioSaveAndRecallDoneCallbackPtr)(void* context, AudioSaveAndRecallResult result); + +namespace loop_library +{ + static auto kLibraryBaseFolder = "loops/"; + + AudioFileName makeFileNameWithoutExtensionFor(const StorageBank bank, + const int slot); + AudioFileName makeWavFileNameFor(const StorageBank bank, + const int slot); + + template + class Library + { + public: + Library(FileIoProvider& fileIo) : + fileIo_(fileIo) + { + for (int i = 0; i < int(StorageBank::count); i++) + { + isStorageSlotUsedBitfield_[i] = 0; + } + } + + void scanOrInitLibrary() + { + fileIo_.ensureVolumeMounted(); + + fileIo_.makeFolderIfNotExistent(loop_library::kLibraryBaseFolder); + + for (size_t bank = 0; bank < kNumBanks; bank++) + { + for (size_t slot = 0; slot < kNumSlots; slot++) + { + const auto filename = loop_library::makeWavFileNameFor(StorageBank(bank), slot); + + if (const auto isInUse = fileIo_.hasFile(filename)) + isStorageSlotUsedBitfield_[bank] |= (1 << slot); + else + isStorageSlotUsedBitfield_[bank] &= ~(1 << slot); + } + } + } + + bool hasLoop(StorageBank bank, int slot) + { + if (slot < 0 || slot >= kNumSlots) + return false; + + return isStorageSlotUsedBitfield_[int(bank)] & (1 << slot); + } + + private: + FileIoProvider& fileIo_; + + static constexpr auto kNumSlots = 16; + static constexpr auto kNumBanks = int(StorageBank::count); + int16_t isStorageSlotUsedBitfield_[kNumBanks]; + }; +}; // namespace loop_library + +template +class AudioSaveAndRecall +{ +public: + AudioSaveAndRecall(FileIoProvider& fileIo) : + fileIo_(fileIo) + { + } + + template + void startSavingToFile(StorageBank bank, + int slot, + LooperType& looper, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) + { + startSavingToFile(loop_library::makeWavFileNameFor(bank, slot), + looper, + doneCallback, + doneCallbackContext); + } + + template + void startSavingToFile(const AudioFileName& filename, + LooperType& looper, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) + { + static_assert(LooperType::getNumChannels() <= kMaxNumChannels_); + + fileIo_.ensureVolumeMounted(); + + looperPtr_ = &looper; + doneCallback_ = doneCallback; + doneCallbackContext_ = doneCallbackContext; + + looper.preventRecording(); + liftRestrictionsFunc_ = [](void* looperPtr) + { + LooperType* looper = reinterpret_cast(looperPtr); + looper->liftRestrictions(); + }; + + totalNumFrames_ = looper.getPlaybackLength(); + + if (totalNumFrames_ == 0) + { + abort(false); + return; + } + + if ( + !fileIo_.openForWriting(filename) + || !writeWavHeader(totalNumFrames_, looper.getNumChannels(), looper.getSampleRate())) + { + abort(true); + return; + } + + writeOrReadFunc_ = [](AudioSaveAndRecall* storage, void* looperPtr) + { + LooperType* looper = reinterpret_cast(looperPtr); + + // wait while the current recording is still crossfading out + if (looper->isRecording()) + return ReadOrWriteResult::inProgress; + + const auto numSamplesLeft = storage->totalNumFrames_ - storage->numFramesDone_; + + if (numSamplesLeft <= 0) + { + return ReadOrWriteResult::error; + } + + const auto numFramesInThisChunk = std::min(size_t(numSamplesLeft), kNumFramesPerChunk_); + const auto writeSuccessful = storage->writeBuffer(looper->getSampleStoragePtr(), + storage->numFramesDone_, + numFramesInThisChunk); + if (!writeSuccessful) + { + return ReadOrWriteResult::error; + } + + storage->numFramesDone_ += numFramesInThisChunk; + + const auto isNowDone = storage->numFramesDone_ == storage->totalNumFrames_; + if (isNowDone) + { + storage->fileIo_.closeFile(); + return ReadOrWriteResult::completed; + } + return ReadOrWriteResult::inProgress; + }; + } + + size_t getNumChannelsInFile(StorageBank bank, + int slot) + { + return getNumChannelsInFile(loop_library::makeWavFileNameFor(bank, slot)); + } + + size_t getNumChannelsInFile(const AudioFileName& filename) + { + if (isSavingOrRecalling()) + { + return 0; + } + + if (!fileIo_.openForReading(filename)) + { + return 0; + } + + if (!readWavHeader() || !isCompatibleFormat()) + { + fileIo_.closeFile(); + return 0; + } + + fileIo_.closeFile(); + return size_t(wavHeader_.numAudioChannels); + } + + template + void startReadingFromFile(StorageBank bank, + int slot, + LooperType& looper, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) + { + startReadingFromFile(loop_library::makeWavFileNameFor(bank, slot), + looper, + doneCallback, + doneCallbackContext); + } + + template + void startReadingFromFile(const AudioFileName& filename, + LooperType& looper, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) + { + static_assert(LooperType::getNumChannels() <= kMaxNumChannels_); + + fileIo_.ensureVolumeMounted(); + + looperPtr_ = &looper; + doneCallback_ = doneCallback; + doneCallbackContext_ = doneCallbackContext; + + looper.preventPlaybackAndRecording(true); + liftRestrictionsFunc_ = [](void* looperPtr) + { + LooperType* looper = reinterpret_cast(looperPtr); + looper->liftRestrictions(); + }; + + if (!fileIo_.openForReading(filename)) + { + abort(false); + return; + } + + if (!readWavHeader() + || !isCompatibleFormat() + || LooperType::getNumChannels() != wavHeader_.numAudioChannels) + { + abort(true); + return; + } + + totalNumFrames_ = std::min(looper.getSampleStoragePtr().numSamples, + size_t(getNumSamples(wavHeader_))); + + writeOrReadFunc_ = [](AudioSaveAndRecall* storage, void* looperPtr) + { + LooperType* looper = reinterpret_cast(looperPtr); + + const auto numSamplesLeft = storage->totalNumFrames_ - storage->numFramesDone_; + const auto numFramesInThisChunk = std::min(numSamplesLeft, kNumFramesPerChunk_); + + const auto numSamplesRead = storage->readBuffer(looper->getSampleStoragePtr(), + storage->numFramesDone_, + numFramesInThisChunk); + if (numSamplesRead < 0) + { + return ReadOrWriteResult::error; + } + + storage->numFramesDone_ += numSamplesRead; + + const auto isNowDone = storage->totalNumFrames_ == storage->numFramesDone_; + if (isNowDone) + { + looper->setPlaybackLength(storage->totalNumFrames_); + storage->fileIo_.closeFile(); + return ReadOrWriteResult::completed; + } + return ReadOrWriteResult::inProgress; + }; + } + + float getCurrentProgress() const + { + if (totalNumFrames_ <= 0) + return -1.0f; + return float(numFramesDone_) / float(totalNumFrames_); + } + + bool isSavingOrRecalling() const + { + return totalNumFrames_ > 0; + } + + void readOrWriteNextChunk() + { + if (writeOrReadFunc_) + { + const auto result = writeOrReadFunc_(this, looperPtr_); + + if (result == ReadOrWriteResult::error) + { + abort(true); + return; + } + else if (result == ReadOrWriteResult::completed) + { + liftRestrictionsFunc_(looperPtr_); + if (doneCallback_) + { + doneCallback_(doneCallbackContext_, AudioSaveAndRecallResult::ok); + } + + doneCallback_ = nullptr; + doneCallbackContext_ = nullptr; + liftRestrictionsFunc_ = nullptr; + looperPtr_ = nullptr; + } + } + } + + FileIoProvider& getFileIoProviderForTesting() { return fileIo_; } + + void abort() + { + if (isSavingOrRecalling()) + { + abort(true); + } + } + +private: + void abort(bool shouldCloseFile) + { + if (shouldCloseFile) + fileIo_.closeFile(); + + if (liftRestrictionsFunc_) + { + liftRestrictionsFunc_(looperPtr_); + } + + if (doneCallback_) + { + doneCallback_(doneCallbackContext_, AudioSaveAndRecallResult::error); + } + + doneCallback_ = nullptr; + doneCallbackContext_ = nullptr; + liftRestrictionsFunc_ = nullptr; + looperPtr_ = nullptr; + totalNumFrames_ = 0; + numFramesDone_ = 0; + } + + bool writeWavHeader(size_t totalNumFrames, size_t numChannelsPerFrame, int sampleRate) + { + wavHeader_ = makeWavFileHeader32bitInt(sampleRate, numChannelsPerFrame, totalNumFrames); + + return fileIo_.write(&wavHeader_, sizeof(wavHeader_)) == int32_t(sizeof(wavHeader_)); + } + + bool readWavHeader() + { + const auto skipForwardUntil = [&](int32_t value) + { + int32_t last4Bytes = 0; + while (last4Bytes != value) + { + uint8_t nextByte; + if (fileIo_.read(&nextByte, 1) != 1) + { + return false; + } + + last4Bytes = ((last4Bytes >> 8) & 0x00FFFFFF) + | (nextByte << 24); + } + return true; + }; + + // read Master RIFF chunk + { + const auto kMasterRiffChunkSize = 3 * sizeof(int32_t); + const auto numBytesRead = fileIo_.read(&wavHeader_.fileTypeBlocId, kMasterRiffChunkSize); + + // invalid Master RIFF chunk + if (numBytesRead != kMasterRiffChunkSize + || wavHeader_.fileTypeBlocId != 0x46464952 // "RIFF" + || wavHeader_.fileFormatId != 0x45564157 // "WAVE" + ) + { + return false; + } + } + + // read fmt chunk + { + if (!skipForwardUntil(0x20746d66)) // "fmt" + { + return false; + } + wavHeader_.formatBlocId = 0x20746d66; + + const auto kFmtChunkContentSize = 5 * sizeof(int32_t); + if (fileIo_.read(&wavHeader_.formatBlocSize, kFmtChunkContentSize) != kFmtChunkContentSize) + { + return false; + } + + if (wavHeader_.formatBlocSize < 0x10) // fmt chunk size can never be <16bytes + { + return false; + } + } + + // there can be additional sections in the file. Skip forward if our + // data section doesn't point to the right data yet. + { + if (!skipForwardUntil(0x61746164)) // "data" + { + return false; + } + + wavHeader_.dataBlocId = 0x61746164; + if (fileIo_.read(&wavHeader_.dataBlocSize, sizeof(wavHeader_.dataBlocSize)) != sizeof(wavHeader_.dataBlocSize)) + { + return false; + } + } + + return true; + } + + bool isCompatibleFormat() const + { + const auto isCompatibleSampleFormat = + // 16bit PCM or 24bit PCM or 32bit PCM + (wavHeader_.audioFormat == WavFileFormatCode::pcm + && (wavHeader_.numBitsPerSample == 16 + || wavHeader_.numBitsPerSample == 24 + || wavHeader_.numBitsPerSample == 32)) + // 32bit float + || (wavHeader_.audioFormat == WavFileFormatCode::ieeeFloat + && wavHeader_.numBitsPerSample == 32); + + const auto isCompatibleNumChannels = + wavHeader_.numAudioChannels == 1 + || wavHeader_.numAudioChannels == 2; + + return isCompatibleSampleFormat && isCompatibleNumChannels; + } + + template + bool writeBuffer(LooperStoragePtr storage, const size_t startFrame, const size_t numFrames) + { + float* inPtr[numChannels]; + + for (size_t ch = 0; ch < numChannels; ch++) + inPtr[ch] = &storage.data[ch][startFrame]; + + int32_t* outPtr = &writeBuffer_[0]; + + size_t numFramesLeft = numFrames; + while (numFramesLeft > 0) + { + for (size_t ch = 0; ch < numChannels; ch++) + { + *outPtr = f2s32(*inPtr[ch]); + outPtr++; + inPtr[ch]++; + } + numFramesLeft--; + } + + const auto numBytes = std::distance(&writeBuffer_[0], outPtr) * sizeof(int32_t); + return fileIo_.write(writeBuffer_, numBytes) == int32_t(numBytes); + } + + template + int32_t readBuffer(LooperStoragePtr storage, const size_t startFrame, const size_t maxNumFrames) + { + float* outPtr[numChannels]; + + for (size_t ch = 0; ch < numChannels; ch++) + outPtr[ch] = &storage.data[ch][startFrame]; + + uint8_t* inPtr = reinterpret_cast(&writeBuffer_[0]); + int32_t numFramesRead = 0; + + if (wavHeader_.audioFormat == WavFileFormatCode::ieeeFloat) + { + constexpr auto kFrameSize = sizeof(float); + const auto maxNumBytes = maxNumFrames * numChannels * kFrameSize; + const auto bytesRead = fileIo_.read(writeBuffer_, maxNumBytes); + + if (bytesRead < 0) + { + return -1; + } + + numFramesRead = bytesRead / kFrameSize; + size_t numFramesLeft = numFramesRead; + while (numFramesLeft > 0) + { + for (size_t ch = 0; ch < numChannels; ch++) + { + *outPtr[ch] = *reinterpret_cast(inPtr); + inPtr += kFrameSize; + outPtr[ch]++; + } + numFramesLeft--; + } + } + else if (wavHeader_.audioFormat == WavFileFormatCode::pcm && wavHeader_.numBitsPerSample == 32) + { + constexpr auto kFrameSize = sizeof(int32_t); + const auto maxNumBytes = maxNumFrames * numChannels * kFrameSize; + const auto bytesRead = fileIo_.read(writeBuffer_, maxNumBytes); + + if (bytesRead < 0) + { + return -1; + } + + numFramesRead = bytesRead / kFrameSize; + size_t numFramesLeft = numFramesRead; + while (numFramesLeft > 0) + { + for (size_t ch = 0; ch < numChannels; ch++) + { + *outPtr[ch] = s322f(*reinterpret_cast(inPtr)); + inPtr += kFrameSize; + outPtr[ch]++; + } + numFramesLeft--; + } + } + else if (wavHeader_.audioFormat == WavFileFormatCode::pcm && wavHeader_.numBitsPerSample == 24) + { + constexpr auto kFrameSize = sizeof(int8_t) * 3; + const auto maxNumBytes = maxNumFrames * numChannels * kFrameSize; + const auto bytesRead = fileIo_.read(writeBuffer_, maxNumBytes); + + if (bytesRead < 0) + { + return -1; + } + + numFramesRead = bytesRead / kFrameSize; + size_t numFramesLeft = numFramesRead; + while (numFramesLeft > 0) + { + for (size_t ch = 0; ch < numChannels; ch++) + { + *outPtr[ch] = s242f(*reinterpret_cast(inPtr) & 0xFFFFFF); + inPtr += kFrameSize; + outPtr[ch]++; + } + numFramesLeft--; + } + } + else if (wavHeader_.audioFormat == WavFileFormatCode::pcm && wavHeader_.numBitsPerSample == 16) + { + constexpr auto kFrameSize = sizeof(int16_t); + const auto maxNumBytes = maxNumFrames * numChannels * kFrameSize; + const auto bytesRead = fileIo_.read(writeBuffer_, maxNumBytes); + + if (bytesRead < 0) + { + return -1; + } + + numFramesRead = bytesRead / kFrameSize; + size_t numFramesLeft = numFramesRead; + while (numFramesLeft > 0) + { + for (size_t ch = 0; ch < numChannels; ch++) + { + *outPtr[ch] = s162f(*reinterpret_cast(inPtr)); + inPtr += kFrameSize; + outPtr[ch]++; + } + numFramesLeft--; + } + } + + return numFramesRead / numChannels; + } + + static constexpr size_t kNumFramesPerChunk_ = 1000; + static constexpr size_t kBitsPerSample_ = 32u; + static constexpr size_t kMaxNumChannels_ = 2u; + static constexpr size_t kWriteBufferSizeBytes_ = kNumFramesPerChunk_ + * kMaxNumChannels_ + * kBitsPerSample_ / 8u; + + WavFileHeader wavHeader_; + + enum class ReadOrWriteResult + { + inProgress, + completed, + error + }; + typedef ReadOrWriteResult (*WriteOrReadFuncPtr)(AudioSaveAndRecall* storage, void* looper); + typedef void (*LiftRestrictionsFuncPtr)(void* looper); + + FileIoProvider& fileIo_; + + size_t totalNumFrames_ = 0; + size_t numFramesDone_ = 0; + WriteOrReadFuncPtr writeOrReadFunc_ = nullptr; + LiftRestrictionsFuncPtr liftRestrictionsFunc_ = nullptr; + void* looperPtr_ = nullptr; + AudioSaveAndRecallDoneCallbackPtr doneCallback_ = nullptr; + void* doneCallbackContext_ = nullptr; + + int32_t writeBuffer_[kWriteBufferSizeBytes_ / sizeof(int32_t)]; +}; diff --git a/firmware/src/LooperController.h b/firmware/src/LooperController.h index 8b83679..7b6031d 100644 --- a/firmware/src/LooperController.h +++ b/firmware/src/LooperController.h @@ -24,11 +24,13 @@ #include "constants.h" #include "util/LateInitializedObject.h" +#include "AudioSaveAndRecall.h" enum class ChannelLayout { mono, - stereo + stereo, + invalid }; enum class MotorAcceleration @@ -48,15 +50,18 @@ class LooperController using MonoLooperType = typename LooperTypes::MonoLooperType; using StereoLooperType = typename LooperTypes::StereoLooperType; using ParameterProviderType = typename LooperTypes::ParameterProvider; + using AudioSaveAndRecallType = typename LooperTypes::AudioSaveAndRecallType; LooperController(std::array looperStorage, AudioBufferPtr<1> monoDownmixBuffer, AudioBufferPtr<1> temporaryBuffer, - const ParameterProviderType& paramProvider) : + const ParameterProviderType& paramProvider, + AudioSaveAndRecallType& audioSaveAndRecall) : looperStorage_(looperStorage), monoDownmixBuffer_(monoDownmixBuffer), temporaryBuffer_(temporaryBuffer), - paramProvider_(paramProvider) + paramProvider_(paramProvider), + audioSaveAndRecall_(audioSaveAndRecall) { for (size_t i = 0; i < numLoopers; i++) loopers_[i].initializeToLayout(ChannelLayout::stereo, looperStorage_[i]); @@ -78,18 +83,86 @@ class LooperController return loopers_[looperIdx].looper.template as().getState(); } - void saveTo(size_t looperIdx, size_t slot) + void saveTo(StorageBank bank, + size_t looperIdx, + size_t slot, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) { - (void) (looperIdx); - (void) (slot); - // TODO + if (loopers_[looperIdx].layout == ChannelLayout::mono) + audioSaveAndRecall_.startSavingToFile(bank, + slot, + loopers_[looperIdx].looper.template as(), + doneCallback, + doneCallbackContext); + else + audioSaveAndRecall_.startSavingToFile(bank, + slot, + loopers_[looperIdx].looper.template as(), + doneCallback, + doneCallbackContext); + } + + void loadFrom(StorageBank bank, + size_t looperIdx, + size_t slot, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) + { + const auto numChannels = audioSaveAndRecall_.getNumChannelsInFile(bank, slot); + if (numChannels == 0) + { + if (doneCallback) + { + doneCallback(doneCallbackContext, AudioSaveAndRecallResult::error); + } + return; + } + + if (numChannels == 1 && getChannelLayout(looperIdx) != ChannelLayout::mono) + { + setChannelLayout(looperIdx, ChannelLayout::mono); + } + else if (numChannels == 2 && getChannelLayout(looperIdx) != ChannelLayout::stereo) + { + setChannelLayout(looperIdx, ChannelLayout::stereo); + } + + if (loopers_[looperIdx].layout == ChannelLayout::mono) + audioSaveAndRecall_.startReadingFromFile(bank, + slot, + loopers_[looperIdx].looper.template as(), + doneCallback, + doneCallbackContext); + else + audioSaveAndRecall_.startReadingFromFile(bank, + slot, + loopers_[looperIdx].looper.template as(), + doneCallback, + doneCallbackContext); + } + + float getCurrentSaveOrLoadProgress() + { + return audioSaveAndRecall_.getCurrentProgress(); + } + + bool isSavingOrRecalling() + { + return audioSaveAndRecall_.isSavingOrRecalling(); + } + + void abortLoadOrSaveOperation() + { + if (audioSaveAndRecall_.isSavingOrRecalling()) + { + audioSaveAndRecall_.abort(); + } } - void loadFrom(size_t looperIdx, size_t slot) + void processSaveOrLoadOperation() { - (void) (looperIdx); - (void) (slot); - // TODO + audioSaveAndRecall_.readOrWriteNextChunk(); } void setChannelLayout(size_t looperIdx, ChannelLayout channelLayout) @@ -189,23 +262,37 @@ class LooperController looper.template destroy(); } - ChannelLayout layout = ChannelLayout::stereo; + ChannelLayout layout = ChannelLayout::invalid; Direction direction = Direction::forwards; MotorAcceleration acceleration = MotorAcceleration::medium; LateInitializedObject looper; void initializeToLayout(const ChannelLayout newLayout, MonoOrStereoLooperStoragePtr& storage) { - layout = newLayout; - if (layout == ChannelLayout::mono) + switch (layout) { - looper.template destroy(); - looper.template create(storage); + case ChannelLayout::mono: + looper.template destroy(); + break; + case ChannelLayout::stereo: + looper.template destroy(); + break; + default: + break; } - else + + layout = newLayout; + + switch (newLayout) { - looper.template destroy(); - looper.template create(storage); + case ChannelLayout::mono: + looper.template create(storage); + break; + case ChannelLayout::stereo: + looper.template create(storage); + break; + default: + break; } } }; @@ -298,4 +385,5 @@ class LooperController AudioBufferPtr<1> monoDownmixBuffer_; AudioBufferPtr<1> temporaryBuffer_; const ParameterProviderType& paramProvider_; + AudioSaveAndRecallType& audioSaveAndRecall_; }; \ No newline at end of file diff --git a/firmware/src/hardware/FatFsFileIo.h b/firmware/src/hardware/FatFsFileIo.h new file mode 100644 index 0000000..717fe93 --- /dev/null +++ b/firmware/src/hardware/FatFsFileIo.h @@ -0,0 +1,118 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +extern SD_HandleTypeDef hsd1; + +class FatFsFileIo +{ +public: + FatFsFileIo() + { + Init(); + } + + void ensureVolumeMounted() + { + isMounted_ = f_mount(&fileSystem_.GetSDFileSystem(), fileSystem_.GetSDPath(), 1) == FR_OK; + + // In case of an error, reinitialize the peripheral and try again. + // Someone could have removed the SD and plugged it back in + if (!isMounted_) + { + // unmount + f_mount(nullptr, fileSystem_.GetSDPath(), 0); + fileSystem_.DeInit(); + + HAL_SD_DeInit(&hsd1); + memset(&hsd1, 0, sizeof(hsd1)); + + daisy::System::DelayUs(10000); + + Init(); + + isMounted_ = f_mount(&fileSystem_.GetSDFileSystem(), fileSystem_.GetSDPath(), 1) == FR_OK; + } + } + + bool makeFolderIfNotExistent(const char* folderName) + { + return f_mkdir(folderName) == FR_OK; + } + + bool hasFile(const char* fileName) + { + const auto canOpenForReading = openForReading(fileName); + if (canOpenForReading) + { + closeFile(); + } + return canOpenForReading; + } + + bool openForReading(const char* fileName) + { + const auto result = f_open(&file_, fileName, FA_OPEN_EXISTING | FA_READ); + return result == FR_OK; + } + + bool openForWriting(const char* fileName) + { + const auto result = f_open(&file_, fileName, FA_CREATE_ALWAYS | FA_WRITE); + return result == FR_OK; + } + + int32_t write(const void* data, size_t size) + { + UINT bytesWritten = 0; + const auto result = f_write(&file_, data, UINT(size), &bytesWritten); + return result == FR_OK ? int32_t(bytesWritten) : -1; + } + + int32_t read(void* data, size_t maxSize) + { + UINT bytesRead = 0; + const auto result = f_read(&file_, data, UINT(maxSize), &bytesRead); + return result == FR_OK ? int32_t(bytesRead) : -1; + } + + bool closeFile() + { + const auto result = f_close(&file_); + return result == FR_OK; + } + +private: + void Init() + { + daisy::SdmmcHandler::Config sdCfg; + sdCfg.Defaults(); + sdCfg.speed = daisy::SdmmcHandler::Speed::STANDARD; + sd_.Init(sdCfg); + fileSystem_.Init(daisy::FatFSInterface::Config::MEDIA_SD); + } + + bool isMounted_ = false; + daisy::SdmmcHandler sd_; + daisy::FatFSInterface fileSystem_; + FIL file_; +}; \ No newline at end of file diff --git a/firmware/src/hardware/UiHardware.h b/firmware/src/hardware/UiHardware.h index 7b6e1f2..be864b9 100644 --- a/firmware/src/hardware/UiHardware.h +++ b/firmware/src/hardware/UiHardware.h @@ -40,11 +40,11 @@ class ButtonReader dmaBuffer_ = dmaBuffer; // shift register for the channel buttons - shiftRegisterLoadPin_.pin = { DSY_GPIOA, 4 }; - shiftRegisterLoadPin_.mode = DSY_GPIO_MODE_OUTPUT_PP; - shiftRegisterLoadPin_.pull = DSY_GPIO_NOPULL; - dsy_gpio_init(&shiftRegisterLoadPin_); - dsy_gpio_write(&shiftRegisterLoadPin_, 1); + shiftRegisterLoadPin_.Init({ daisy::GPIOPort::PORTA, 4 }, + daisy::GPIO::Mode::OUTPUT, + daisy::GPIO::Pull::NOPULL, + daisy::GPIO::Speed::MEDIUM); + shiftRegisterLoadPin_.Write(true); daisy::SpiHandle::Config config; config.periph = daisy::SpiHandle::Config::Peripheral::SPI_1; @@ -55,27 +55,27 @@ class ButtonReader config.clock_phase = daisy::SpiHandle::Config::ClockPhase::ONE_EDGE; config.nss = daisy::SpiHandle::Config::NSS::SOFT; config.baud_prescaler = daisy::SpiHandle::Config::BaudPrescaler::PS_16; - config.pin_config.sclk = { DSY_GPIOA, 5 }; - config.pin_config.miso = { DSY_GPIOG, 9 }; + config.pin_config.sclk = { daisy::GPIOPort::PORTA, 5 }; + config.pin_config.miso = { daisy::GPIOPort::PORTG, 9 }; shiftRegisterSpi_.Init(config); // common section buttons - commonGpios_.settings.pin = { DSY_GPIOB, 7 }; - commonGpios_.settings.mode = DSY_GPIO_MODE_INPUT; - commonGpios_.settings.pull = DSY_GPIO_NOPULL; - dsy_gpio_init(&commonGpios_.settings); - commonGpios_.save.pin = { DSY_GPIOG, 11 }; - commonGpios_.save.mode = DSY_GPIO_MODE_INPUT; - commonGpios_.save.pull = DSY_GPIO_NOPULL; - dsy_gpio_init(&commonGpios_.save); - commonGpios_.load.pin = { DSY_GPIOG, 10 }; - commonGpios_.load.mode = DSY_GPIO_MODE_INPUT; - commonGpios_.load.pull = DSY_GPIO_NOPULL; - dsy_gpio_init(&commonGpios_.load); - commonGpios_.rec.pin = { DSY_GPIOB, 12 }; - commonGpios_.rec.mode = DSY_GPIO_MODE_INPUT; - commonGpios_.rec.pull = DSY_GPIO_NOPULL; - dsy_gpio_init(&commonGpios_.rec); + commonGpios_.settings.Init({ daisy::GPIOPort::PORTB, 7 }, + daisy::GPIO::Mode::INPUT, + daisy::GPIO::Pull::NOPULL, + daisy::GPIO::Speed::LOW); + commonGpios_.save.Init({ daisy::GPIOPort::PORTG, 11 }, + daisy::GPIO::Mode::INPUT, + daisy::GPIO::Pull::NOPULL, + daisy::GPIO::Speed::LOW); + commonGpios_.load.Init({ daisy::GPIOPort::PORTG, 10 }, + daisy::GPIO::Mode::INPUT, + daisy::GPIO::Pull::NOPULL, + daisy::GPIO::Speed::LOW); + commonGpios_.rec.Init({ daisy::GPIOPort::PORTB, 12 }, + daisy::GPIO::Mode::INPUT, + daisy::GPIO::Pull::NOPULL, + daisy::GPIO::Speed::LOW); } bool getButtonState(Button buttonId) const @@ -84,13 +84,13 @@ class ButtonReader switch (buttonId) { case Button::save: - return !dsy_gpio_read(&commonGpios_.save); + return !const_cast(&commonGpios_.save)->Read(); case Button::load: - return !dsy_gpio_read(&commonGpios_.load); + return !const_cast(&commonGpios_.load)->Read(); case Button::settings: - return !dsy_gpio_read(&commonGpios_.settings); + return !const_cast(&commonGpios_.settings)->Read(); case Button::record: - return !dsy_gpio_read(&commonGpios_.rec); + return !const_cast(&commonGpios_.rec)->Read(); case Button::chA_play: return !getBit<2>(); case Button::chA_up: @@ -123,12 +123,12 @@ class ButtonReader void triggerReadout() { - dsy_gpio_write(&shiftRegisterLoadPin_, 0); + shiftRegisterLoadPin_.Write(false); constexpr auto kNumBytes = 2; shiftRegisterSpi_.DmaReceive((uint8_t*) dmaBuffer_, kNumBytes, nullptr, nullptr, nullptr); - dsy_gpio_write(&shiftRegisterLoadPin_, 1); + shiftRegisterLoadPin_.Write(true); } private: @@ -140,15 +140,15 @@ class ButtonReader struct { - dsy_gpio settings; - dsy_gpio save; - dsy_gpio load; - dsy_gpio rec; + daisy::GPIO settings; + daisy::GPIO save; + daisy::GPIO load; + daisy::GPIO rec; } commonGpios_; uint16_t* dmaBuffer_ = nullptr; daisy::SpiHandle shiftRegisterSpi_; - dsy_gpio shiftRegisterLoadPin_; + daisy::GPIO shiftRegisterLoadPin_; }; class KnobAndCvReader @@ -163,18 +163,18 @@ class KnobAndCvReader { calibrationStorage_.Init(getDefaultCalibrationData(), kCalibrationDataOffset); - const dsy_gpio_pin mux0 = { DSY_GPIOA, 1 }; - const dsy_gpio_pin mux1 = { DSY_GPIOA, 0 }; - const dsy_gpio_pin mux2 = { DSY_GPIOD, 11 }; + const daisy::Pin mux0 = { daisy::GPIOPort::PORTA, 1 }; + const daisy::Pin mux1 = { daisy::GPIOPort::PORTA, 0 }; + const daisy::Pin mux2 = { daisy::GPIOPort::PORTD, 11 }; constexpr auto kNumChannelsPerAdc = 7; constexpr auto kNumAdcs = 4; daisy::AdcChannelConfig channelConfigs[kNumAdcs]; - channelConfigs[0].InitMux({ DSY_GPIOC, 0 }, kNumChannelsPerAdc, mux0, mux1, mux2); - channelConfigs[1].InitMux({ DSY_GPIOA, 3 }, kNumChannelsPerAdc, mux0, mux1, mux2); - channelConfigs[2].InitMux({ DSY_GPIOB, 1 }, kNumChannelsPerAdc, mux0, mux1, mux2); - channelConfigs[3].InitMux({ DSY_GPIOA, 7 }, kNumChannelsPerAdc, mux0, mux1, mux2); + channelConfigs[0].InitMux({ daisy::GPIOPort::PORTC, 0 }, kNumChannelsPerAdc, mux0, mux1, mux2); + channelConfigs[1].InitMux({ daisy::GPIOPort::PORTA, 3 }, kNumChannelsPerAdc, mux0, mux1, mux2); + channelConfigs[2].InitMux({ daisy::GPIOPort::PORTB, 1 }, kNumChannelsPerAdc, mux0, mux1, mux2); + channelConfigs[3].InitMux({ daisy::GPIOPort::PORTA, 7 }, kNumChannelsPerAdc, mux0, mux1, mux2); adc_.Init(channelConfigs, kNumAdcs); adc_.Start(); @@ -407,6 +407,28 @@ class UiHardware buttons_.triggerReadout(); } + static const std::array allChannelLeds() + { + return { + Led::chA_m2, + Led::chA_m1, + Led::chA_p1, + Led::chA_p2, + Led::chB_m2, + Led::chB_m1, + Led::chB_p1, + Led::chB_p2, + Led::chC_m2, + Led::chC_m1, + Led::chC_p1, + Led::chC_p2, + Led::chD_m2, + Led::chD_m1, + Led::chD_p1, + Led::chD_p2, + }; + } + private: UiHardware(const UiHardware&) = delete; UiHardware& operator=(const UiHardware&) = delete; @@ -423,13 +445,13 @@ class UiHardware daisy::I2CHandle i2c; daisy::I2CHandle::Config i2cCfg; i2cCfg.periph = daisy::I2CHandle::Config::Peripheral::I2C_1; - i2cCfg.pin_config.sda = { DSY_GPIOB, 9 }; - i2cCfg.pin_config.scl = { DSY_GPIOB, 8 }; + i2cCfg.pin_config.sda = { daisy::GPIOPort::PORTB, 9 }; + i2cCfg.pin_config.scl = { daisy::GPIOPort::PORTB, 8 }; i2cCfg.speed = daisy::I2CHandle::Config::Speed::I2C_400KHZ; i2cCfg.mode = daisy::I2CHandle::Config::Mode::I2C_MASTER; i2c.Init(i2cCfg); - ledDriver_.Init(i2c, { 0b00, 0b01, 0b10 }, bufferA, bufferB, { DSY_GPIOB, 6 }); + ledDriver_.Init(i2c, { 0b00, 0b01, 0b10 }, bufferA, bufferB, { daisy::GPIOPort::PORTB, 6 }); } void updateLedBrightnessValues() diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 4ee83ca..62275f6 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -21,6 +21,7 @@ #include "constants.h" #include "dsp/PeakMeter.h" +#include "hardware/FatFsFileIo.h" #include "hardware/UiHardware.h" #include "ui/TapeLooperUi.h" #include "dsp/TapeLooper.h" @@ -37,12 +38,16 @@ struct LooperTypes using MonoLooperType = TapeLooper; using StereoLooperType = TapeLooper; using ParameterProvider = LooperParameterProviderType; + using AudioSaveAndRecallType = AudioSaveAndRecall; }; using LooperControllerType = LooperController; +using LoopLibraryType = loop_library::Library; + using TapeLooperUiType = TapeLooperUi, LooperControllerType, - LooperParameterProviderType>; + LooperParameterProviderType, + LoopLibraryType>; using LooperStoragesType = std::array, numLoopers>; // hardware static objects @@ -53,7 +58,9 @@ daisy::UiEventQueue uiEventQueue; UiHardware::LedDmaBufferType DMA_BUFFER_MEM_SECTION ledDmaBufferA, ledDmaBufferB; uint16_t DMA_BUFFER_MEM_SECTION buttonShiftRegisterDmaBuffer; +LateInitializedObject fileIo; LateInitializedObject uiHardware; +LateInitializedObject loopLibrary; LateInitializedObject ui; // dsp static objects @@ -61,6 +68,7 @@ std::array, 4> peakMeters; #define EXTERNAL_SDRAM_SECTION __attribute__((section(".sdram_bss"))) LateInitializedObject EXTERNAL_SDRAM_SECTION looperStorages; LateInitializedObject looperParameterProvider; +LateInitializedObject audioSaveAndRecall; AudioBuffer<1, blockSize> monoDownmixBuffer; AudioBuffer<1, blockSize> temporaryBuffer; LateInitializedObject looperController; @@ -91,6 +99,8 @@ void initUi() ledDmaBufferB, &buttonShiftRegisterDmaBuffer); + loopLibrary.create(fileIo); + // init the UI ui.create( hardware, @@ -99,7 +109,8 @@ void initUi() uiEventQueue, peakMeters, looperController, - looperParameterProvider); + looperParameterProvider, + loopLibrary); } // =================================================================== @@ -108,9 +119,10 @@ void initUi() void initDsp() { - // init the parameter provider looperParameterProvider.create(); + audioSaveAndRecall.create(fileIo); + // initialize the looper storage in SDRAM const auto& rawStorages = *looperStorages.create(); // build an array of storage pointers @@ -123,7 +135,8 @@ void initDsp() arrayOfStoragePtrs, monoDownmixBuffer, temporaryBuffer, - looperParameterProvider); + looperParameterProvider, + audioSaveAndRecall); // init peak meters for (auto& peakMeter : peakMeters) @@ -142,6 +155,8 @@ void configurePlatform() seed.Init(true /* enable 480MHz boost */); seed.SetAudioBlockSize(blockSize); seed.SetAudioSampleRate(sampleRate); + + fileIo.create(); } void audioCallback(const float* const* in, float** out, size_t size) @@ -211,6 +226,10 @@ int main(void) constexpr auto kUpdateIntervalMs = 20; while (daisy::System::GetNow() < lastUpdate + kUpdateIntervalMs) { + if (looperController->isSavingOrRecalling()) + { + looperController->processSaveOrLoadOperation(); + } } lastUpdate = daisy::System::GetNow(); } diff --git a/firmware/src/ui/TapeLooperUi.h b/firmware/src/ui/TapeLooperUi.h index 7686ffc..c706294 100644 --- a/firmware/src/ui/TapeLooperUi.h +++ b/firmware/src/ui/TapeLooperUi.h @@ -26,14 +26,14 @@ #include "UiBasePage.h" #include "UiCalibrationPage.h" #include "UiSettingsPage.h" -#include "UiSavePage.h" -#include "UiLoadPage.h" +#include "UiLoadOrSavePage.h" #include "UiRecordingPage.h" template + typename ParameterProviderType, + typename LoopLibraryType> class TapeLooperUi { public: @@ -43,15 +43,16 @@ class TapeLooperUi daisy::UiEventQueue& eventQueue, const std::array& peakMeters, LooperControllerType& looperController, - ParameterProviderType& looperParameterProvider) : + ParameterProviderType& looperParameterProvider, + LoopLibraryType& loopLibrary) : uiHardware_(uiHardware), eventQueue_(eventQueue), calibrationPage_(uiHardware), settingsPage_(looperController, looperParameterProvider, calibrationPage_), - savePage_(looperController), - loadPage_(looperController), + savePage_(looperController, loopLibrary), + loadPage_(looperController, loopLibrary), recordingPage_(looperController), basePage_(peakMeters, looperController, @@ -93,10 +94,12 @@ class TapeLooperUi ParameterProviderType> settingsPage_; UiSavePage + UiHardwareType, + LoopLibraryType> savePage_; UiLoadPage + UiHardwareType, + LoopLibraryType> loadPage_; UiRecordingPage diff --git a/firmware/src/ui/UiLoadOrSavePage.h b/firmware/src/ui/UiLoadOrSavePage.h new file mode 100644 index 0000000..d66003e --- /dev/null +++ b/firmware/src/ui/UiLoadOrSavePage.h @@ -0,0 +1,425 @@ +/** + * Copyright (C) Johannes Elliesen, 2021 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include + +#include "../constants.h" +#include "../hardware/UiHardwareTypes.h" +#include "LooperParameterProvider.h" + +template +class UiLoadOrSavePage : public daisy::UiPage +{ +public: + UiLoadOrSavePage(LooperControllerType& looperController, LoopLibraryType& loopLibrary) : + looperController_(looperController), + loopLibrary_(loopLibrary) + { + } + + virtual ~UiLoadOrSavePage() {} + + bool IsOpaque(const daisy::UiCanvasDescriptor&) override { return false; } + + void OnShow() override + { + stage_ = Stage::selectChannel; + selectedChannel_ = -1; + } + + void Draw(const daisy::UiCanvasDescriptor& canvas) override + { + UiHardwareType& hardware = *((UiHardwareType*) canvas.handle_); + + // light up the page led + hardware.setLed(PageLed, LedColour::pulsingRed); + + switch (stage_) + { + case Stage::selectChannel: + drawSelectChannel(hardware); + break; + case Stage::selectBankAndSlot: + drawSelectBankAndSlot(hardware); + break; + case Stage::saveOrLoadInProgress: + drawProgress(hardware); + break; + case Stage::displayError: + drawResult(hardware, AudioSaveAndRecallResult::error); + break; + case Stage::displaySuccess: + drawResult(hardware, AudioSaveAndRecallResult::ok); + break; + default: + break; + } + } + + bool OnButton(uint16_t buttonID, uint8_t numberOfPresses, bool isRetriggering) override + { + (void) (isRetriggering); // ignore this argument + + // swallow but ignore button-up messages + if (numberOfPresses < 1) + return true; + + const Button button = Button(buttonID); + switch (button) + { + case Button::save: + return handleSaveButton(); + case Button::load: + return handleLoadButton(); + default: + break; + } + + switch (stage_) + { + case Stage::selectChannel: + return onButtonInChannelSelection(button); + case Stage::selectBankAndSlot: + return onButtonInBankAndSlotSelection(button); + default: + // any button press while showing success or error closes the page + if (stage_ == Stage::displaySuccess || stage_ == Stage::displayError) + { + Close(); + } + break; + } + + return true; + } + + bool OnPotMoved(uint16_t, float) override + { + return false; // passthrough pot events to the base page + } + + void OnFocusGained() override + { + loopLibrary_.scanOrInitLibrary(); + } + +protected: + UiLoadOrSavePage(const UiLoadOrSavePage&) = delete; + UiLoadOrSavePage& operator=(const UiLoadOrSavePage&) = delete; + + static LedColour getBankColourPulsing(StorageBank bank) + { + if (bank == StorageBank::green) + return LedColour::pulsingGreen; + if (bank == StorageBank::yellow) + return LedColour::pulsingYellow; + if (bank == StorageBank::red) + return LedColour::pulsingRed; + + return LedColour::off; + }; + + static LedColour getBankColour(StorageBank bank) + { + if (bank == StorageBank::green) + return LedColour::green; + if (bank == StorageBank::yellow) + return LedColour::yellow; + if (bank == StorageBank::red) + return LedColour::red; + + return LedColour::off; + }; + + void drawSelectChannel(UiHardwareType& hardware) + { + hardware.setLed(Led::chA_p2, LedColour::pulsingYellow); + hardware.setLed(Led::chA_p1, LedColour::pulsingYellow); + hardware.setLed(Led::chA_m1, LedColour::pulsingYellow); + hardware.setLed(Led::chA_m2, LedColour::pulsingYellow); + + hardware.setLed(Led::chB_p2, LedColour::pulsingYellow); + hardware.setLed(Led::chB_p1, LedColour::pulsingYellow); + hardware.setLed(Led::chB_m1, LedColour::pulsingYellow); + hardware.setLed(Led::chB_m2, LedColour::pulsingYellow); + + hardware.setLed(Led::chC_p2, LedColour::pulsingYellow); + hardware.setLed(Led::chC_p1, LedColour::pulsingYellow); + hardware.setLed(Led::chC_m1, LedColour::pulsingYellow); + hardware.setLed(Led::chC_m2, LedColour::pulsingYellow); + + hardware.setLed(Led::chD_p2, LedColour::pulsingYellow); + hardware.setLed(Led::chD_p1, LedColour::pulsingYellow); + hardware.setLed(Led::chD_m1, LedColour::pulsingYellow); + hardware.setLed(Led::chD_m2, LedColour::pulsingYellow); + } + + void drawSelectBankAndSlot(UiHardwareType& hardware) + { + const auto bankColour = this->getBankColour(this->selectedBank_); + const auto bankColourPulsing = this->getBankColourPulsing(this->selectedBank_); + + hardware.setLed(Led::settings, bankColourPulsing); + + const auto allLeds = UiHardwareType::allChannelLeds(); + for (size_t i = 0; i < allLeds.size(); i++) + { + if (this->selectedSlot_ == int(i)) + { + hardware.setLed(allLeds[i], bankColourPulsing); + } + else + { + hardware.setLed(allLeds[i], + loopLibrary_.hasLoop(selectedBank_, i) + ? bankColour + : LedColour::off); + } + } + } + + void drawProgress(UiHardwareType& hardware) + { + const auto allLeds = UiHardwareType::allChannelLeds(); + for (size_t i = 0; i < allLeds.size(); i++) + { + const auto shouldBeOn = looperController_.getCurrentSaveOrLoadProgress() + > float(i) / float(allLeds.size() - 1); + hardware.setLed(allLeds[i], shouldBeOn ? LedColour::pulsingYellow : LedColour::off); + } + } + + void drawResult(UiHardwareType& hardware, AudioSaveAndRecallResult result) + { + const auto color = result == AudioSaveAndRecallResult::ok ? LedColour::pulsingGreen : LedColour::pulsingRed; + for (auto led : UiHardwareType::allChannelLeds()) + { + hardware.setLed(led, color); + } + + if (daisy::System::GetNow() >= closeAfterTime_) + { + Close(); + } + } + + bool onButtonInChannelSelection(Button button) + { + switch (button) + { + case Button::chA_play: + selectedChannel_ = 0; + stage_ = Stage::selectBankAndSlot; + break; + case Button::chB_play: + selectedChannel_ = 1; + stage_ = Stage::selectBankAndSlot; + break; + case Button::chC_play: + selectedChannel_ = 2; + stage_ = Stage::selectBankAndSlot; + break; + case Button::chD_play: + selectedChannel_ = 3; + stage_ = Stage::selectBankAndSlot; + break; + default: + break; + } + + return true; + } + + bool onButtonInBankAndSlotSelection(Button button) + { + const auto handleChannelUp = [&](int channel) + { + if (selectedSlot_ / 4 != channel) + { + selectedSlot_ = selectedSlot_ % 4 + channel * 4; + return; + } + selectedSlot_ = std::min(selectedSlot_ + 1, channel * 4 + 3); + }; + const auto handleChannelDown = [&](int channel) + { + if (selectedSlot_ / 4 != channel) + { + selectedSlot_ = selectedSlot_ % 4 + channel * 4; + return; + } + selectedSlot_ = std::max(selectedSlot_ - 1, channel * 4); + }; + + switch (button) + { + case Button::chA_down: + handleChannelDown(0); + break; + case Button::chA_up: + handleChannelUp(0); + break; + case Button::chB_down: + handleChannelDown(1); + break; + case Button::chB_up: + handleChannelUp(1); + break; + case Button::chC_down: + handleChannelDown(2); + break; + case Button::chC_up: + handleChannelUp(2); + break; + case Button::chD_down: + handleChannelDown(3); + break; + case Button::chD_up: + handleChannelUp(3); + break; + case Button::settings: + selectedBank_ = StorageBank((int(selectedBank_) + 1) % int(StorageBank::count)); + break; + default: + break; + } + + return true; + } + + virtual bool handleSaveButton() = 0; + virtual bool handleLoadButton() = 0; + + static void doneCallback(void* context, AudioSaveAndRecallResult result) + { + const auto page = static_cast(context); + page->stage_ = result == AudioSaveAndRecallResult::ok ? Stage::displaySuccess : Stage::displayError; + page->closeAfterTime_ = daisy::System::GetNow() + 2000; + } + + enum class Stage + { + selectChannel, + selectBankAndSlot, + saveOrLoadInProgress, + displaySuccess, + displayError, + }; + + Stage stage_ = Stage::selectChannel; + StorageBank selectedBank_ = StorageBank::green; + int selectedChannel_ = -1; + int selectedSlot_ = 0; + LooperControllerType& looperController_; + LoopLibraryType& loopLibrary_; + uint32_t closeAfterTime_ = -1; +}; + +template +class UiSavePage : public UiLoadOrSavePage +{ + using ParentType = UiLoadOrSavePage; + +public: + UiSavePage(LooperControllerType& looperController, LoopLibraryType& loopLibrary) : + ParentType(looperController, loopLibrary) + { + } + + ~UiSavePage() = default; + + bool handleSaveButton() override + { + if (this->stage_ == ParentType::Stage::selectBankAndSlot) + { + this->stage_ = ParentType::Stage::saveOrLoadInProgress; + this->looperController_.saveTo(this->selectedBank_, + this->selectedChannel_, + this->selectedSlot_, + &this->doneCallback, + this); + } + else if (this->stage_ == ParentType::Stage::selectChannel + || this->stage_ == ParentType::Stage::displaySuccess + || this->stage_ == ParentType::Stage::displayError) + { + this->Close(); + } + return true; + } + + bool handleLoadButton() override + { + this->looperController_.abortLoadOrSaveOperation(); + this->Close(); + return false; // pass event to the page below to open the respective page + } +}; + +template +class UiLoadPage : public UiLoadOrSavePage +{ + using ParentType = UiLoadOrSavePage; + +public: + UiLoadPage(LooperControllerType& looperController, LoopLibraryType& loopLibrary) : + ParentType(looperController, loopLibrary) + { + } + + ~UiLoadPage() = default; + + bool handleSaveButton() override + { + this->looperController_.abortLoadOrSaveOperation(); + this->Close(); + return false; // pass event to the page below to open the respective page + } + + bool handleLoadButton() override + { + if (this->stage_ == ParentType::Stage::selectBankAndSlot) + { + this->stage_ = ParentType::Stage::saveOrLoadInProgress; + this->looperController_.loadFrom(this->selectedBank_, + this->selectedChannel_, + this->selectedSlot_, + &this->doneCallback, + this); + } + else if (this->stage_ == ParentType::Stage::selectChannel + || this->stage_ == ParentType::Stage::displaySuccess + || this->stage_ == ParentType::Stage::displayError) + { + this->Close(); + } + return true; + } +}; \ No newline at end of file diff --git a/firmware/src/ui/UiLoadPage.h b/firmware/src/ui/UiLoadPage.h deleted file mode 100644 index 58f3157..0000000 --- a/firmware/src/ui/UiLoadPage.h +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -#include -#include - -#include "../constants.h" -#include "../hardware/UiHardwareTypes.h" -#include "LooperParameterProvider.h" - -template -class UiLoadPage : public daisy::UiPage -{ -public: - UiLoadPage(LooperControllerType& looperController) : - looperController_(looperController) - { - } - - virtual ~UiLoadPage() {} - - bool IsOpaque(const daisy::UiCanvasDescriptor&) override { return false; } - - void Draw(const daisy::UiCanvasDescriptor& canvas) override - { - UiHardwareType& hardware = *((UiHardwareType*) canvas.handle_); - - // light up the load page led - hardware.setLed(Led::load, LedColour::pulsingRed); - - // update channel octave LEDs to show the currently selected slot - // TODO - const auto updateChannelOctaveLeds = [&](size_t looperChannel, std::array ledIds) - { - (void) (looperChannel); - hardware.setLed(ledIds[0], LedColour::off); - hardware.setLed(ledIds[1], LedColour::off); - hardware.setLed(ledIds[2], LedColour::off); - hardware.setLed(ledIds[3], LedColour::off); - }; - updateChannelOctaveLeds(0, { Led::chA_m2, Led::chA_m1, Led::chA_p1, Led::chA_p2 }); - updateChannelOctaveLeds(1, { Led::chB_m2, Led::chB_m1, Led::chB_p1, Led::chB_p2 }); - updateChannelOctaveLeds(2, { Led::chC_m2, Led::chC_m1, Led::chC_p1, Led::chC_p2 }); - updateChannelOctaveLeds(3, { Led::chD_m2, Led::chD_m1, Led::chD_p1, Led::chD_p2 }); - } - - bool OnButton(uint16_t buttonID, uint8_t numberOfPresses, bool isRetriggering) override - { - (void) (isRetriggering); // ignore this argument - - // swallow but ignore button-up messages - if (numberOfPresses < 1) - return true; - - const Button button = Button(buttonID); - switch (button) - { - // channel play buttons - case Button::load: - Close(); // close this page - break; - case Button::record: - case Button::save: - case Button::settings: - Close(); // close this page - return false; // pass event to the page below to open the respective page - default: - break; - } - return true; - } - - bool OnPotMoved(uint16_t, float) override - { - return false; // passthrough pot events to the base page - } - -private: - UiLoadPage(const UiLoadPage&) = delete; - UiLoadPage& operator=(const UiLoadPage&) = delete; - - LooperControllerType& looperController_; -}; \ No newline at end of file diff --git a/firmware/src/ui/UiSavePage.h b/firmware/src/ui/UiSavePage.h deleted file mode 100644 index 338d50f..0000000 --- a/firmware/src/ui/UiSavePage.h +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -#include -#include - -#include "../constants.h" -#include "../hardware/UiHardwareTypes.h" -#include "LooperParameterProvider.h" - -template -class UiSavePage : public daisy::UiPage -{ -public: - UiSavePage(LooperControllerType& looperController) : - looperController_(looperController) - { - } - - virtual ~UiSavePage() {} - - bool IsOpaque(const daisy::UiCanvasDescriptor&) override { return false; } - - void Draw(const daisy::UiCanvasDescriptor& canvas) override - { - UiHardwareType& hardware = *((UiHardwareType*) canvas.handle_); - - // light up the save page led - hardware.setLed(Led::save, LedColour::pulsingRed); - - // update channel octave LEDs to show the currently selected slot - // TODO - const auto updateChannelOctaveLeds = [&](size_t looperChannel, std::array ledIds) - { - (void) (looperChannel); - hardware.setLed(ledIds[0], LedColour::off); - hardware.setLed(ledIds[1], LedColour::off); - hardware.setLed(ledIds[2], LedColour::off); - hardware.setLed(ledIds[3], LedColour::off); - }; - updateChannelOctaveLeds(0, { Led::chA_m2, Led::chA_m1, Led::chA_p1, Led::chA_p2 }); - updateChannelOctaveLeds(1, { Led::chB_m2, Led::chB_m1, Led::chB_p1, Led::chB_p2 }); - updateChannelOctaveLeds(2, { Led::chC_m2, Led::chC_m1, Led::chC_p1, Led::chC_p2 }); - updateChannelOctaveLeds(3, { Led::chD_m2, Led::chD_m1, Led::chD_p1, Led::chD_p2 }); - } - - bool OnButton(uint16_t buttonID, uint8_t numberOfPresses, bool isRetriggering) override - { - (void) (isRetriggering); // ignore this argument - - // swallow but ignore button-up messages - if (numberOfPresses < 1) - return true; - - const Button button = Button(buttonID); - switch (button) - { - // channel play buttons - case Button::save: - Close(); // close this page - break; - case Button::record: - case Button::load: - case Button::settings: - Close(); // close this page - return false; // pass event to the page below to open the respective page - default: - break; - } - return true; - } - - bool OnPotMoved(uint16_t, float) override - { - return false; // passthrough pot events to the base page - } - -private: - UiSavePage(const UiSavePage&) = delete; - UiSavePage& operator=(const UiSavePage&) = delete; - - LooperControllerType& looperController_; -}; \ No newline at end of file diff --git a/firmware/src/util/WavFileFormat.cpp b/firmware/src/util/WavFileFormat.cpp new file mode 100644 index 0000000..e9ae956 --- /dev/null +++ b/firmware/src/util/WavFileFormat.cpp @@ -0,0 +1,61 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "WavFileFormat.h" + +void updateWavFileHeaderSizes(WavFileHeader& header, const int numSamples) +{ + header.dataBlocSize = numSamples * header.numBytesPerBloc; + header.fileSizeMinus8Bytes = header.dataBlocSize + sizeof(WavFileHeader) - 8; +} + +WavFileHeader makeWavFileHeader32bitInt(const int sampleRate, + const int numChannels, + const int numSamples) +{ + WavFileHeader header; + header.audioFormat = WavFileFormatCode::pcm; + header.numAudioChannels = numChannels; + header.sampleRate = sampleRate; + header.numBitsPerSample = 32; + header.numBytesPerBloc = header.numAudioChannels + * header.numBitsPerSample / 8; + header.numBytesPerSec = sampleRate * header.numBytesPerBloc; + updateWavFileHeaderSizes(header, numSamples); + return header; +} + +WavFileHeader makeWavFileHeader32bitFloat(const int sampleRate, + const int numChannels, + const int numSamples) +{ + WavFileHeader header; + header.audioFormat = WavFileFormatCode::ieeeFloat; + header.numAudioChannels = numChannels; + header.sampleRate = sampleRate; + header.numBitsPerSample = 32; + header.numBytesPerBloc = header.numAudioChannels + * header.numBitsPerSample / 8; + header.numBytesPerSec = sampleRate * header.numBytesPerBloc; + updateWavFileHeaderSizes(header, numSamples); + return header; +} + +uint32_t getNumSamples(const WavFileHeader& header) +{ + return uint32_t(header.dataBlocSize / header.numBytesPerBloc); +} diff --git a/firmware/src/util/WavFileFormat.h b/firmware/src/util/WavFileFormat.h new file mode 100644 index 0000000..27c15ea --- /dev/null +++ b/firmware/src/util/WavFileFormat.h @@ -0,0 +1,63 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +enum class WavFileFormatCode +{ + pcm = 0x0001, + ieeeFloat = 0x0003, + aLaw = 0x0006, + uLaw = 0x0007, + extensible = 0xFFFE, +}; + +#ifdef _MSC_VER +# pragma pack(push, 1) +# define PACKED_ATTRIBUTE +#else +# define PACKED_ATTRIBUTE __attribute__((__packed__)) +#endif +struct WavFileHeader +{ + uint32_t fileTypeBlocId = 0x46464952; // "RIFF" + uint32_t fileSizeMinus8Bytes; + uint32_t fileFormatId = 0x45564157; // "WAVE" + uint32_t formatBlocId = 0x20746d66; // "fmt " + uint32_t formatBlocSize = 0x10; + WavFileFormatCode audioFormat : 16; + uint16_t numAudioChannels; + uint32_t sampleRate; + uint32_t numBytesPerSec; + uint16_t numBytesPerBloc; + uint16_t numBitsPerSample; + uint32_t dataBlocId = 0x61746164; // "data" + uint32_t dataBlocSize; +} PACKED_ATTRIBUTE; +#ifdef _MSC_VER +# pragma pack(pop) +#endif + +void updateWavFileHeaderSizes(WavFileHeader& header, int numSamples); + +WavFileHeader makeWavFileHeader32bitInt(int sampleRate, int numChannels, int numSamples = 0); + +WavFileHeader makeWavFileHeader32bitFloat(int sampleRate, int numChannels, int numSamples = 0); + +uint32_t getNumSamples(const WavFileHeader& header); diff --git a/firmware/tests/AudioSaveAndRecall_gtest.cpp b/firmware/tests/AudioSaveAndRecall_gtest.cpp new file mode 100644 index 0000000..13bedd2 --- /dev/null +++ b/firmware/tests/AudioSaveAndRecall_gtest.cpp @@ -0,0 +1,544 @@ +/** + * Copyright (C) Johannes Elliesen, 2024 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +// uncomment to update the test references +// #define WRITE_REFERENCES + +#include "libDaisyCombined.h" +#include "AudioSaveAndRecall.h" +namespace +{ + static constexpr auto kSampleRate = 48000; + + std::vector makeSawWave(double frequency, size_t numSamples) + { + const float kIncrement = float(2.0f * frequency / double(kSampleRate)); + float value = 0.0f; + + std::vector result(numSamples); + for (auto& sample : result) + { + sample = value; + value += kIncrement; + if (value >= 0.9999f) + { + value = -1.0f; + } + } + + return result; + } + + std::vector readReferenceFile(const char* filename) + { + std::ifstream referenceFile(filename, std::ios::binary); + EXPECT_TRUE(referenceFile.is_open()); + + std::vector contents; + + char b; + while (referenceFile.read(&b, sizeof(b))) + { + contents.push_back(std::byte(b)); + } + referenceFile.close(); + + return contents; + } + +#ifdef WRITE_REFERENCES + void writeReferenceFile(const char* filename, const std::vector& data) + { + std::ofstream referenceFile(filename, std::ios::binary); + EXPECT_TRUE(referenceFile.is_open()); + + for (std::byte byte : data) + { + referenceFile << std::ofstream::char_type(byte); + } + referenceFile.close(); + } +#endif +} // namespace + +class TestFileIo +{ +public: + void ensureVolumeMounted() {} + + bool openForReading(const AudioFileName& fileName) + { + lastFileName_ = fileName; + + if (simulateFileOpenError_) + return false; + + readIndex_ = 0; + return true; + } + + bool openForWriting(const AudioFileName& fileName) + { + lastFileName_ = fileName; + + if (simulateFileOpenError_) + return false; + + dataWritten_.clear(); + return true; + } + + int32_t write(void* data, size_t size) + { + if (simulateFileWriteError_) + return -1; + + dataWritten_.insert(dataWritten_.end(), + static_cast(data), + static_cast(data) + size); + + return int32_t(size); + } + + int32_t read(void* data, size_t maxSize) + { + if (simulateFileOpenError_) + { + return -1; + } + + uint8_t* outPtr = reinterpret_cast(data); + + const auto numRead = std::min(maxSize, dataToRead_.size() - readIndex_); + for (size_t i = 0; i < numRead; i++) + { + *outPtr = uint8_t(dataToRead_[readIndex_ + i]); + outPtr++; + } + + readIndex_ += numRead; + return numRead; + } + + void closeFile() + { + closeFileCalled_ = true; + } + + AudioFileName lastFileName_; + std::vector dataWritten_; + std::vector dataToRead_; + size_t readIndex_ = 0; + bool closeFileCalled_ = false; + + bool simulateFileOpenError_ = false; + bool simulateFileWriteError_ = false; +}; + +class AudioSaveAndRecallFixture : public ::testing::Test +{ +public: + AudioSaveAndRecallFixture() : + monoLooper_(monoBuffer_), + stereoLooper_(stereoBuffer_) + { + fillBuffersWithSilence(); + monoLooper_.setPlaybackLength(kNumSamples); + stereoLooper_.setPlaybackLength(kNumSamples); + } + + static constexpr auto kNumSamples = 48000; + LooperStorage monoBuffer_; + LooperStorage stereoBuffer_; + TapeLooper monoLooper_; + TapeLooper stereoLooper_; + TestFileIo fileIo_; + + AudioSaveAndRecallResult doneCallbackResult_ = + AudioSaveAndRecallResult(-1); // invalid + + static void doneCallback(void* contextPtr, AudioSaveAndRecallResult result) + { + auto* resultFlag = reinterpret_cast(contextPtr); + *resultFlag = result; + }; + + void fillBuffersWithSilence() + { + for (size_t i = 0; i < kNumSamples; i++) + { + stereoBuffer_.data[0][i] = 0.0f; + stereoBuffer_.data[1][i] = 0.0f; + monoBuffer_.data[0][i] = 0.0f; + } + } + + void fillBuffersWithSineWaves() + { + const auto freq1 = 480.0f; + const auto phaseIncrement1 = freq1 / float(kSampleRate); + const auto freq2 = 640.0f; + const auto phaseIncrement2 = freq2 / float(kSampleRate); + + auto phase1 = 0.0f; + auto phase2 = 0.0f; + + for (size_t i = 0; i < kNumSamples; i++) + { + stereoBuffer_.data[0][i] = std::sin(phase1 * 2.0f * M_PI); + stereoBuffer_.data[1][i] = std::sin(phase2 * 2.0f * M_PI); + monoBuffer_.data[0][i] = stereoBuffer_.data[0][i]; + + phase1 += phaseIncrement1; + if (phase1 >= 1.0f) + phase1 -= 1.0f; + phase2 += phaseIncrement2; + if (phase2 >= 1.0f) + phase2 -= 1.0f; + } + } +}; + +TEST_F(AudioSaveAndRecallFixture, a_saveToFile_failWhenNoDataInLooper) +{ + AudioSaveAndRecall sut(fileIo_); + + monoLooper_.setPlaybackLength(0); + + fileIo_.simulateFileOpenError_ = true; + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + // doesn't even open the file + EXPECT_STREQ(fileIo_.lastFileName_, ""); + EXPECT_FALSE(fileIo_.closeFileCalled_); + + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::error); + EXPECT_EQ(sut.getCurrentProgress(), -1.0f); +} + +TEST_F(AudioSaveAndRecallFixture, b_saveToFile_failWhenFileDoesntOpen) +{ + AudioSaveAndRecall sut(fileIo_); + + fileIo_.simulateFileOpenError_ = true; + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + EXPECT_STREQ(fileIo_.lastFileName_, "dummyFilename"); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::error); + EXPECT_EQ(sut.getCurrentProgress(), -1.0f); + EXPECT_TRUE(fileIo_.closeFileCalled_); +} + +TEST_F(AudioSaveAndRecallFixture, c_saveToFile_failWhenErrorWhileWriting) +{ + AudioSaveAndRecall sut(fileIo_); + + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + EXPECT_STREQ(fileIo_.lastFileName_, "dummyFilename"); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult(-1)); // not called yet + EXPECT_EQ(sut.getCurrentProgress(), 0.0f); + EXPECT_FALSE(fileIo_.closeFileCalled_); + + // make some progress + sut.readOrWriteNextChunk(); + EXPECT_GT(sut.getCurrentProgress(), 0.0f); + EXPECT_LT(sut.getCurrentProgress(), 1.0f); + + // simulate file IO error + fileIo_.simulateFileWriteError_ = true; + sut.readOrWriteNextChunk(); + + // operation was aborted + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::error); + EXPECT_EQ(sut.getCurrentProgress(), -1.0f); + EXPECT_TRUE(fileIo_.closeFileCalled_); +} + +TEST_F(AudioSaveAndRecallFixture, d_saveToFile_saveFileButWaitUntilRecordingDoneAndPreventNewRecording) +{ + AudioSaveAndRecall sut(fileIo_); + + monoLooper_.switchState(LooperState::recording); + monoLooper_.recorderForTesting().setRecordingProgressForTesting(2000); + + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + EXPECT_STREQ(fileIo_.lastFileName_, "dummyFilename"); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult(-1)); // not called yet + EXPECT_EQ(sut.getCurrentProgress(), 0.0f); + EXPECT_FALSE(fileIo_.closeFileCalled_); + + // the looper was stopped when saving started... + EXPECT_EQ(monoLooper_.getState(), LooperState::stopped); + // the looper should still be recording (crossfading out) + EXPECT_TRUE(monoLooper_.isRecording()); + + // attempt to write + sut.readOrWriteNextChunk(); + // nothing happened yet + EXPECT_EQ(sut.getCurrentProgress(), 0.0f); + + // simulate that crossfading is now complete by brutally aborting the recording + monoLooper_.recorderForTesting().stopRecordingImmediately(); + monoLooper_.setPlaybackLength(kNumSamples); + + // attempt to write again + sut.readOrWriteNextChunk(); + // now we have some progress + EXPECT_GT(sut.getCurrentProgress(), 0.0f); +} + +TEST_F(AudioSaveAndRecallFixture, e_saveToFile_saveFileAndAllowPlaybackButPreventNewRecording) +{ + AudioSaveAndRecall sut(fileIo_); + + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + EXPECT_STREQ(fileIo_.lastFileName_, "dummyFilename"); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult(-1)); // not called yet + EXPECT_EQ(sut.getCurrentProgress(), 0.0f); + EXPECT_FALSE(fileIo_.closeFileCalled_); + + // attempt to start a new recording while we're saving + monoLooper_.switchState(LooperState::recording); + // but new recordings should be prevented + EXPECT_EQ(monoLooper_.getState(), LooperState::stopped); + EXPECT_FALSE(monoLooper_.isRecording()); + + // but we can still play the loop while we're saving + monoLooper_.switchState(LooperState::playing); + EXPECT_EQ(monoLooper_.getState(), LooperState::playing); + + // complete the saving operation + int timeout = 1000; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + } + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::ok); + + // attempt a recording again + monoLooper_.switchState(LooperState::recording); + // and new recordings should be allowed again + EXPECT_EQ(monoLooper_.getState(), LooperState::recording); + EXPECT_TRUE(monoLooper_.isRecording()); +} + +TEST_F(AudioSaveAndRecallFixture, f_saveToFile_saveFileAndReportProgress) +{ + AudioSaveAndRecall sut(fileIo_); + + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + EXPECT_STREQ(fileIo_.lastFileName_, "dummyFilename"); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult(-1)); // not called yet + EXPECT_EQ(sut.getCurrentProgress(), 0.0f); + EXPECT_FALSE(fileIo_.closeFileCalled_); + + // write until completed + int timeout = 1000; + auto lastProgress = 0.0f; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + EXPECT_GT(sut.getCurrentProgress(), lastProgress); + lastProgress = sut.getCurrentProgress(); + } + EXPECT_EQ(sut.getCurrentProgress(), 1.0f); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::ok); + EXPECT_TRUE(fileIo_.closeFileCalled_); +} + +TEST_F(AudioSaveAndRecallFixture, g_saveToFile_producesValidMonoWavFile) +{ + AudioSaveAndRecall sut(fileIo_); + + constexpr auto kNumTestSamples = 4800; + constexpr auto kReferenceFilename = "data/monoSineWaves.wav"; + + fillBuffersWithSineWaves(); + monoLooper_.setPlaybackLength(kNumTestSamples); + + sut.startSavingToFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + // write until completed + int timeout = 1000; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + } + EXPECT_EQ(sut.getCurrentProgress(), 1.0f); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::ok); + EXPECT_TRUE(fileIo_.closeFileCalled_); + +#ifdef WRITE_REFERENCES + writeReferenceFile(kReferenceFilename, fileIo_.dataWritten_); +#endif + + const auto expectedFileContents = readReferenceFile(kReferenceFilename); + EXPECT_EQ(fileIo_.dataWritten_, expectedFileContents); +} + +TEST_F(AudioSaveAndRecallFixture, h_saveToFile_producesValidStereoWavFile) +{ + AudioSaveAndRecall sut(fileIo_); + + constexpr auto kNumTestSamples = 4800; + constexpr auto kReferenceFilename = "data/stereoSineWaves.wav"; + + fillBuffersWithSineWaves(); + stereoLooper_.setPlaybackLength(kNumTestSamples); + + sut.startSavingToFile("dummyFilename", + stereoLooper_, + &doneCallback, + &doneCallbackResult_); + + // write until completed + int timeout = 1000; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + } + EXPECT_EQ(sut.getCurrentProgress(), 1.0f); + EXPECT_EQ(doneCallbackResult_, AudioSaveAndRecallResult::ok); + EXPECT_TRUE(fileIo_.closeFileCalled_); + +#ifdef WRITE_REFERENCES + writeReferenceFile(kReferenceFilename, fileIo_.dataWritten_); +#endif + + const auto expectedFileContents = readReferenceFile(kReferenceFilename); + EXPECT_EQ(fileIo_.dataWritten_, expectedFileContents); +} + +class AudioSaveAndRecallParameterizedFileFormatFixture : + public AudioSaveAndRecallFixture, + public ::testing::WithParamInterface> +{ +}; + +TEST_P(AudioSaveAndRecallParameterizedFileFormatFixture, canReadMonoWav) +{ + const auto [referenceFileNameMono, referenceFileNameStereo, epsilon] = GetParam(); + + AudioSaveAndRecall sut(fileIo_); + + constexpr auto kExpectedNumSamples = 48; + + fileIo_.dataToRead_ = readReferenceFile(referenceFileNameMono); + + sut.startReadingFromFile("dummyFilename", + monoLooper_, + &doneCallback, + &doneCallbackResult_); + + // read until completed + int timeout = 1000; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + } + + const auto kExpectedSawWav = makeSawWave(1000.0, kExpectedNumSamples); + + const auto actualSamples = std::vector(monoBuffer_.data[0], + monoBuffer_.data[0] + monoLooper_.getPlaybackLength()); + + EXPECT_THAT(actualSamples, + ::testing::Pointwise(::testing::FloatNear(epsilon), + kExpectedSawWav)); +} + +TEST_P(AudioSaveAndRecallParameterizedFileFormatFixture, canReadStereoWav) +{ + const auto [referenceFileNameMono, referenceFileNameStereo, epsilon] = GetParam(); + + AudioSaveAndRecall sut(fileIo_); + + constexpr auto kExpectedNumSamples = 48; + + fileIo_.dataToRead_ = readReferenceFile(referenceFileNameStereo); + + sut.startReadingFromFile("dummyFilename", + stereoLooper_, + &doneCallback, + &doneCallbackResult_); + + // read until completed + int timeout = 1000; + while (timeout-- > 0 && doneCallbackResult_ != AudioSaveAndRecallResult::ok) + { + sut.readOrWriteNextChunk(); + } + + const auto kExpectedSawWavLeft = makeSawWave(1000.0, kExpectedNumSamples); + const auto kExpectedSawWavRight = makeSawWave(2000.0, kExpectedNumSamples); + + const auto actualSamplesLeft = std::vector(stereoBuffer_.data[0], + stereoBuffer_.data[0] + stereoLooper_.getPlaybackLength()); + const auto actualSamplesRight = std::vector(stereoBuffer_.data[1], + stereoBuffer_.data[1] + stereoLooper_.getPlaybackLength()); + + EXPECT_THAT(actualSamplesLeft, + ::testing::Pointwise(::testing::FloatNear(epsilon), + kExpectedSawWavLeft)); + EXPECT_THAT(actualSamplesRight, + ::testing::Pointwise(::testing::FloatNear(epsilon), + kExpectedSawWavRight)); +} + +INSTANTIATE_TEST_SUITE_P( + AudioSaveAndRecall_loadFromFile_canReadWavs, + AudioSaveAndRecallParameterizedFileFormatFixture, + ::testing::Values( + std::make_tuple("data/monoSaw_int16.wav", "data/stereoSaw_int16.wav", 3e-4f), + std::make_tuple("data/monoSaw_int24.wav", "data/stereoSaw_int24.wav", 1e-6f), + std::make_tuple("data/monoSaw_int32.wav", "data/stereoSaw_int32.wav", 1e-6f), + std::make_tuple("data/monoSaw_float.wav", "data/stereoSaw_float.wav", 1e-6f), + // Ableton uses the JUNK segment to encode a messsage in the file. + std::make_tuple("data/monoSaw_float.wav", "data/stereoSaw_float_Ableton.wav", 1e-6f))); diff --git a/firmware/tests/LooperController_gtest.cpp b/firmware/tests/LooperController_gtest.cpp index 5828a60..f0a3094 100644 --- a/firmware/tests/LooperController_gtest.cpp +++ b/firmware/tests/LooperController_gtest.cpp @@ -18,6 +18,7 @@ #include #include +#include "libDaisyCombined.h" #include "LooperController.h" #include "../libDaisy/tests/TestIsolator.h" @@ -125,11 +126,18 @@ class ParameterProviderMock MOCK_METHOD(ProcessorParametersMock, getProcessorParameters, (size_t looperChannel), (const)); }; +class AudioSaveAndRecallMock +{ +public: + // MOCK_METHOD(ReturnType, MethodName, (Args...), (Specs...)); +}; + struct LooperMockTypes { using MonoLooperType = ::testing::NiceMock; using StereoLooperType = ::testing::NiceMock; using ParameterProvider = ::testing::NiceMock; + using AudioSaveAndRecallType = ::testing::NiceMock; }; class LooperController_Test : public ::testing::Test @@ -139,7 +147,8 @@ class LooperController_Test : public ::testing::Test controller_(storage_, monoDownmixBuffer_, temporaryBuffer_, - parameterProvider_) + parameterProvider_, + audioSaveAndRecall_) { } @@ -164,6 +173,7 @@ class LooperController_Test : public ::testing::Test AudioBuffer<1, numSamples_> monoDownmixBuffer_; AudioBuffer<1, numSamples_> temporaryBuffer_; ::testing::NiceMock parameterProvider_; + ::testing::NiceMock audioSaveAndRecall_; LooperController controller_; MockInstanceRegistry& looperInstanceRegistry_ = *mockInstances.GetStateForCurrentTest(); }; diff --git a/firmware/tests/Ui_gtest.cpp b/firmware/tests/Ui_gtest.cpp index 9f980cd..c931351 100644 --- a/firmware/tests/Ui_gtest.cpp +++ b/firmware/tests/Ui_gtest.cpp @@ -20,9 +20,11 @@ #include <../tests/TestIsolator.h> // from libDaisy #include // from libDaisy +#include "libDaisyCombined.h" // remaining libDaisy mocks/stubs #include "ui/TapeLooperUi.h" #include "mocks/LooperController_mock.h" +#include "mocks/LoopLibrary_mock.h" #include "mocks/ParameterProvider_mock.h" #include "mocks/PeakMeter_mock.h" #include "mocks/UiHardware_mock.h" @@ -118,7 +120,8 @@ class UiFixture : public ::testing::Test eventQueue_, peakMeters_, looperController_, - looperParameterProvider_) + looperParameterProvider_, + loopLibrary_) { uiTestMockFunctionIsolator.GetStateForCurrentTest()->clearCanvasFunction = [&](const daisy::UiCanvasDescriptor&) @@ -327,10 +330,12 @@ class UiFixture : public ::testing::Test std::array peakMeters_; LooperControllerMock looperController_; ParameterProviderMock looperParameterProvider_; + LoopLibraryMock loopLibrary_; TapeLooperUi, - ParameterProviderMock> + ParameterProviderMock, + LoopLibraryMock> ui_; }; diff --git a/firmware/tests/data/monoSaw_float.wav b/firmware/tests/data/monoSaw_float.wav new file mode 100644 index 0000000..eb2a36c Binary files /dev/null and b/firmware/tests/data/monoSaw_float.wav differ diff --git a/firmware/tests/data/monoSaw_int16.wav b/firmware/tests/data/monoSaw_int16.wav new file mode 100644 index 0000000..bc142e9 Binary files /dev/null and b/firmware/tests/data/monoSaw_int16.wav differ diff --git a/firmware/tests/data/monoSaw_int24.wav b/firmware/tests/data/monoSaw_int24.wav new file mode 100644 index 0000000..d23edd2 Binary files /dev/null and b/firmware/tests/data/monoSaw_int24.wav differ diff --git a/firmware/tests/data/monoSaw_int32.wav b/firmware/tests/data/monoSaw_int32.wav new file mode 100644 index 0000000..4ce029d Binary files /dev/null and b/firmware/tests/data/monoSaw_int32.wav differ diff --git a/firmware/tests/data/monoSineWaves.wav b/firmware/tests/data/monoSineWaves.wav new file mode 100644 index 0000000..635cda9 Binary files /dev/null and b/firmware/tests/data/monoSineWaves.wav differ diff --git a/firmware/tests/data/stereoSaw_float.wav b/firmware/tests/data/stereoSaw_float.wav new file mode 100644 index 0000000..7aaf68f Binary files /dev/null and b/firmware/tests/data/stereoSaw_float.wav differ diff --git a/firmware/tests/data/stereoSaw_float_Ableton.wav b/firmware/tests/data/stereoSaw_float_Ableton.wav new file mode 100644 index 0000000..7aa9666 Binary files /dev/null and b/firmware/tests/data/stereoSaw_float_Ableton.wav differ diff --git a/firmware/tests/data/stereoSaw_int16.wav b/firmware/tests/data/stereoSaw_int16.wav new file mode 100644 index 0000000..25318d4 Binary files /dev/null and b/firmware/tests/data/stereoSaw_int16.wav differ diff --git a/firmware/tests/data/stereoSaw_int24.wav b/firmware/tests/data/stereoSaw_int24.wav new file mode 100644 index 0000000..ed5f422 Binary files /dev/null and b/firmware/tests/data/stereoSaw_int24.wav differ diff --git a/firmware/tests/data/stereoSaw_int32.wav b/firmware/tests/data/stereoSaw_int32.wav new file mode 100644 index 0000000..dcf6788 Binary files /dev/null and b/firmware/tests/data/stereoSaw_int32.wav differ diff --git a/firmware/tests/data/stereoSineWaves.wav b/firmware/tests/data/stereoSineWaves.wav new file mode 100644 index 0000000..136c28a Binary files /dev/null and b/firmware/tests/data/stereoSineWaves.wav differ diff --git a/firmware/tests/firmware_combined.cpp b/firmware/tests/firmware_combined.cpp new file mode 100644 index 0000000..ca7393b --- /dev/null +++ b/firmware/tests/firmware_combined.cpp @@ -0,0 +1,2 @@ +#include "../src/util/WavFileFormat.cpp" +#include "../src/AudioSaveAndRecall.cpp" \ No newline at end of file diff --git a/firmware/tests/libDaisyCombined.cpp b/firmware/tests/libDaisyCombined.cpp index 1f590a7..7f88597 100644 --- a/firmware/tests/libDaisyCombined.cpp +++ b/firmware/tests/libDaisyCombined.cpp @@ -1,4 +1,6 @@ // include unit-testable code from libDaisy // which is required for some of the integration tests // that use the daisy::UI class. -#include <../tests/libDaisyCombined.cpp> + +#include +#include diff --git a/firmware/tests/libDaisyCombined.h b/firmware/tests/libDaisyCombined.h new file mode 100644 index 0000000..027ac7a --- /dev/null +++ b/firmware/tests/libDaisyCombined.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +// substitutes for the stuff provided by the daisy library +#define FBIPMAX 0.999985f /**< close to 1.0f-LSB at 16 bit */ +#define FBIPMIN (-FBIPMAX) /**< - (1 - LSB) */ +#define F2S32_SCALE 2147483647.f /**< (2 ** 31) - 1 */ +#define S322F_SCALE 4.6566129e-10f /**< 1 / (2** 31) */ +#define S242F_SCALE 1.192092896e-07f /**< 1 / (2 ** 23) */ +#define S24SIGN 0x800000 /**< 2 ** 23 */ +#define S162F_SCALE 3.0517578125e-05f /**< 1 / (2** 15) */ + +inline int32_t f2s32(float x) +{ + x = x <= FBIPMIN ? FBIPMIN : x; + x = x >= FBIPMAX ? FBIPMAX : x; + return (int32_t) (x * F2S32_SCALE); +} +inline float s322f(int32_t x) +{ + return (float) x * S322F_SCALE; +} +inline float s242f(int32_t x) +{ + x = (x ^ S24SIGN) - S24SIGN; // sign extend aka ((x<<8)>>8) + return (float) x * S242F_SCALE; +} +inline float s162f(int16_t x) +{ + return (float) x * S162F_SCALE; +} \ No newline at end of file diff --git a/plugin/src/DspDefinitions.h b/firmware/tests/mocks/LoopLibrary_mock.h similarity index 54% rename from plugin/src/DspDefinitions.h rename to firmware/tests/mocks/LoopLibrary_mock.h index f699283..5828635 100644 --- a/plugin/src/DspDefinitions.h +++ b/firmware/tests/mocks/LoopLibrary_mock.h @@ -1,5 +1,5 @@ /** - * Copyright (C) Johannes Elliesen, 2021 + * Copyright (C) Johannes Elliesen, 2025 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,21 +17,22 @@ #pragma once -namespace dspdefs -{ - constexpr size_t numLoopers = 4; - constexpr double loopLengthInS = 60.0; +#include - constexpr float speedMin = 0.25f; - constexpr float speedMax = 4.0f; +#include "AudioSaveAndRecall.h" - namespace paramIds +class LoopLibraryMock +{ +public: + void scanOrInitLibrary() { - const auto chState = "chState"; - const auto chSpeed = "chSpeed"; - const auto chDrive = "chDrive"; - const auto chGrainAmt = "chGrainAmt"; - const auto chWowAndFlutterAmt = "chWowAndFlutterAmt"; - const auto chPostGain = "chPostGain"; - } // namespace paramIds -} // namespace dspdefs + } + + bool hasLoop(StorageBank bank, int slot) + { + (void) (bank); + (void) (slot); + + return false; + } +}; \ No newline at end of file diff --git a/firmware/tests/mocks/LooperController_mock.h b/firmware/tests/mocks/LooperController_mock.h index b3362f9..ebe8da1 100644 --- a/firmware/tests/mocks/LooperController_mock.h +++ b/firmware/tests/mocks/LooperController_mock.h @@ -23,6 +23,7 @@ #include #include "LooperController.h" +#include "AudioSaveAndRecall.h" template class LooperControllerMock @@ -64,20 +65,39 @@ class LooperControllerMock return LooperState::stopped; } - void saveTo(size_t looperIdx, size_t slot) + void saveTo(StorageBank bank, + size_t looperIdx, + size_t slot, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) { + (void) (doneCallback); + (void) (doneCallbackContext); + std::ostringstream stringStream; - stringStream << "saveTo(" << looperIdx << ", " << slot << ")"; + stringStream << "saveTo(" << int(bank) << ", " << looperIdx << ", " << slot << ")"; events += stringStream.str() + "\n"; } - void loadFrom(size_t looperIdx, size_t slot) + void loadFrom(StorageBank bank, + size_t looperIdx, + size_t slot, + AudioSaveAndRecallDoneCallbackPtr doneCallback, + void* doneCallbackContext) { + (void) (doneCallback); + (void) (doneCallbackContext); + std::ostringstream stringStream; - stringStream << "loadFrom(" << looperIdx << ", " << slot << ")"; + stringStream << "loadFrom(" << int(bank) << ", " << looperIdx << ", " << slot << ")"; events += stringStream.str() + "\n"; } + void abortLoadOrSaveOperation() + { + events += "abortLoadOrSaveOperation()\n"; + } + void setChannelLayout(size_t looperIdx, ChannelLayout channelLayout) { std::ostringstream stringStream; @@ -87,6 +107,17 @@ class LooperControllerMock events += stringStream.str() + "\n"; } + float getCurrentSaveOrLoadProgress() + { + events += "getCurrentSaveOrLoadProgress()\n"; + return -1.0f; + } + + bool isSavingOrRecalling() + { + return false; + } + std::function getChannelLayoutMock_; ChannelLayout getChannelLayout(size_t looperIdx) const { diff --git a/firmware/tests/mocks/UiHardware_mock.h b/firmware/tests/mocks/UiHardware_mock.h index de101be..c7d2418 100644 --- a/firmware/tests/mocks/UiHardware_mock.h +++ b/firmware/tests/mocks/UiHardware_mock.h @@ -69,6 +69,28 @@ class UiHardwareMock } } + static const std::array allChannelLeds() + { + return { + Led::chA_m2, + Led::chA_m1, + Led::chA_p1, + Led::chA_p2, + Led::chB_m2, + Led::chB_m1, + Led::chB_p1, + Led::chB_p2, + Led::chC_m2, + Led::chC_m1, + Led::chC_p1, + Led::chC_p2, + Led::chD_m2, + Led::chD_m1, + Led::chD_p1, + Led::chD_p2, + }; + } + KnobsAndCvMock& getKnobsAndCv() { return knobsAndCv_; }; KnobsAndCvMock knobsAndCv_; diff --git a/lib/libDaisy b/lib/libDaisy index 8d106a2..9498417 160000 --- a/lib/libDaisy +++ b/lib/libDaisy @@ -1 +1 @@ -Subproject commit 8d106a2c822f687bcb8c86bed0b6f2c22f680999 +Subproject commit 9498417add4a4c76bc10737d5e9f43750a4da639 diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt deleted file mode 100644 index 9704abe..0000000 --- a/plugin/CMakeLists.txt +++ /dev/null @@ -1,189 +0,0 @@ -# Example Audio Plugin CMakeLists.txt - -# To get started on a new plugin, copy this entire folder (containing this file and C++ sources) to -# a convenient location, and then start making modifications. - -# The first line of any CMake project should be a call to `cmake_minimum_required`, which checks -# that the installed CMake will be able to understand the following CMakeLists, and ensures that -# CMake's behaviour is compatible with the named version. This is a standard CMake command, so more -# information can be found in the CMake docs. - -cmake_minimum_required(VERSION 3.15) - -# The top-level CMakeLists.txt file for a project must contain a literal, direct call to the -# `project()` command. `project()` sets up some helpful variables that describe source/binary -# directories, and the current project version. This is a standard CMake command. - -project(TAPE_LOOPER_PLUGIN VERSION 0.0.1) - -add_subdirectory(lib/JUCE) -add_subdirectory(../dsp build) - -# If you are building a VST2 or AAX plugin, CMake needs to be told where to find these SDKs on your -# system. This setup should be done before calling `juce_add_plugin`. - -# juce_set_vst2_sdk_path(...) -# juce_set_aax_sdk_path(...) - -# `juce_add_plugin` adds a static library target with the name passed as the first argument -# (TapeLooperPlugin here). This target is a normal CMake target, but has a lot of extra properties set -# up by default. As well as this shared code static library, this function adds targets for each of -# the formats specified by the FORMATS arguments. This function accepts many optional arguments. -# Check the readme at `docs/CMake API.md` in the JUCE repo for the full list. - -juce_add_plugin(TapeLooperPlugin - # VERSION ... # Set this if the plugin version is different to the project version - # ICON_BIG ... # ICON_* arguments specify a path to an image file to use as an icon for the Standalone - # ICON_SMALL ... - # COMPANY_NAME ... # Specify the name of the plugin's author - # IS_SYNTH TRUE/FALSE # Is this a synth or an effect? - # NEEDS_MIDI_INPUT TRUE/FALSE # Does the plugin need midi input? - # NEEDS_MIDI_OUTPUT TRUE/FALSE # Does the plugin need midi output? - # IS_MIDI_EFFECT TRUE/FALSE # Is this plugin a MIDI effect? - # EDITOR_WANTS_KEYBOARD_FOCUS TRUE/FALSE # Does the editor need keyboard focus? - # COPY_PLUGIN_AFTER_BUILD TRUE/FALSE # Should the plugin be installed to a default location after building? - PLUGIN_MANUFACTURER_CODE TSG # A four-character manufacturer id with at least one upper-case character - PLUGIN_CODE Tplp # A unique four-character plugin id with exactly one upper-case character - # 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 -# this header will be automatically added to the target. The main function of the JuceHeader is to -# include all your JUCE module headers; if you're happy to include module headers directly, you -# probably don't need to call this. - -juce_generate_juce_header(TapeLooperPlugin) - -# `target_sources` adds source files to a target. We pass the target that needs the sources as the -# first argument, then a visibility parameter for the sources which should normally be PRIVATE. -# Finally, we supply a list of source files that will be built into the target. This is a standard -# CMake command. - -set( - plugin_sourcefiles - - src/PluginEditor.h - src/PluginEditor.cpp - src/PluginProcessor.h - src/PluginProcessor.cpp - - src/DspDefinitions.h - src/TapeLooperProcessor.h - - src/TapeLooperComponent.h -) - -target_sources(TapeLooperPlugin - PRIVATE - ${plugin_sourcefiles} -) - -foreach(source IN LISTS plugin_sourcefiles) - get_filename_component(source_path "${source}" PATH) - string(REPLACE "/" "\\" source_path_msvc "${source_path}") - source_group("${source_path_msvc}" FILES "${source}") -endforeach() - -# `target_compile_definitions` adds some preprocessor definitions to our target. In a Projucer -# project, these might be passed in the 'Preprocessor Definitions' field. JUCE modules also make use -# of compile definitions to switch certain features on/off, so if there's a particular feature you -# need that's not on by default, check the module header for the correct flag to set here. These -# definitions will be visible both to your code, and also the JUCE module code, so for new -# definitions, pick unique names that are unlikely to collide! This is a standard CMake command. - -target_compile_definitions(TapeLooperPlugin - PUBLIC - # JUCE_WEB_BROWSER and JUCE_USE_CURL would be on by default, but you might not need them. - JUCE_WEB_BROWSER=0 # If you remove this, add `NEEDS_WEB_BROWSER TRUE` to the `juce_add_plugin` call - JUCE_USE_CURL=0 # If you remove this, add `NEEDS_CURL TRUE` to the `juce_add_plugin` call - JUCE_MODAL_LOOPS_PERMITTED=1 - JUCE_VST3_CAN_REPLACE_VST2=0 - MANUAL_INLINE=inline # overrides inline statements used to optimize dsp code for the hardware - _USE_MATH_DEFINES -) - -# If your target needs extra binary assets, you can add them here. The first argument is the name of -# a new static library target that will include all the binary resources. There is an optional -# `NAMESPACE` argument that can specify the namespace of the generated binary data class. Finally, -# the SOURCES argument should be followed by a list of source files that should be built into the -# static library. These source files can be of any kind (wav data, images, fonts, icons etc.). -# Conversion to binary-data will happen when your target is built. - -# juce_add_binary_data(AudioPluginData SOURCES ...) - -# `target_link_libraries` links libraries and JUCE modules to other libraries or executables. Here, -# we're linking our executable target to the `juce::juce_audio_utils` module. Inter-module -# dependencies are resolved automatically, so `juce_core`, `juce_events` and so on will also be -# linked automatically. If we'd generated a binary data target above, we would need to link to it -# here too. This is a standard CMake command. - -target_link_libraries(TapeLooperPlugin - PRIVATE - # AudioPluginData # If we'd created a binary data target, we'd link to it here - juce::juce_audio_utils - PUBLIC - tapeLooperDsp - juce::juce_recommended_config_flags - juce::juce_recommended_lto_flags - juce::juce_recommended_warning_flags) - -################################################################################################### -## Googletest console application -################################################################################################### - -# find thread library -find_package(Threads) - -# include directories -include_directories( - ../lib/googletest/googletest/include - ../lib/googletest/googletest - src/ -) - -set( - test_sourcefiles - - tests/Example_gtest.cpp -) - -# This target adds a googletest executable -add_executable(TapeLooperPlugin_Gtest) - -set_target_properties (TapeLooperPlugin_Gtest PROPERTIES - FOLDER TapeLooperPlugin -) - -target_sources(TapeLooperPlugin_Gtest - PRIVATE - ${test_sourcefiles} - ../lib/googletest/googletest/src/gtest-all.cc - ../lib/googletest/googletest/src/gtest_main.cc) - -foreach(source IN LISTS test_sourcefiles) - get_filename_component(source_path "${source}" PATH) - string(REPLACE "/" "\\" source_path_msvc "${source_path}") - source_group("${source_path_msvc}" FILES "${source}") -endforeach() - -# link thread library -target_link_libraries(TapeLooperPlugin_Gtest - PRIVATE - ${CMAKE_THREAD_LIBS_INIT} - juce::juce_gui_extra - juce::juce_dsp - TapeLooperPlugin - PUBLIC - juce::juce_recommended_config_flags - juce::juce_recommended_lto_flags - juce::juce_recommended_warning_flags - ) - - -set_property(TARGET TapeLooperPlugin_Gtest - PROPERTY - CXX_STANDARD 17 -) \ No newline at end of file diff --git a/plugin/lib/JUCE b/plugin/lib/JUCE deleted file mode 160000 index 46ea879..0000000 --- a/plugin/lib/JUCE +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 46ea879739533ca0cdc689b967edfc5390c46ef7 diff --git a/plugin/src/PluginEditor.cpp b/plugin/src/PluginEditor.cpp deleted file mode 100644 index fa85324..0000000 --- a/plugin/src/PluginEditor.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "PluginProcessor.h" -#include "PluginEditor.h" -#include "DspDefinitions.h" - -//============================================================================== -TapeLooperPluginAudioProcessorEditor::TapeLooperPluginAudioProcessorEditor(TapeLooperPluginAudioProcessor& p) : - AudioProcessorEditor(&p), processorRef(p) -{ - juce::ignoreUnused(processorRef); - - for (size_t i = 0; i < dspdefs::numLoopers; i++) - { - const juce::String chStr(i); - auto comp = std::make_unique( - *(juce::AudioParameterChoice*) p.parameters_.getParameter(dspdefs::paramIds::chState + chStr), - *(juce::AudioParameterFloat*) p.parameters_.getParameter(dspdefs::paramIds::chSpeed + chStr), - *(juce::AudioParameterFloat*) p.parameters_.getParameter(dspdefs::paramIds::chDrive + chStr), - *(juce::AudioParameterFloat*) p.parameters_.getParameter(dspdefs::paramIds::chGrainAmt + chStr), - *(juce::AudioParameterFloat*) p.parameters_.getParameter(dspdefs::paramIds::chWowAndFlutterAmt + chStr), - *(juce::AudioParameterFloat*) p.parameters_.getParameter(dspdefs::paramIds::chPostGain + chStr)); - addAndMakeVisible(*comp); - looperComponents_.add(std::move(comp)); - } - - // Make sure that before the constructor has finished, you've set the - // editor's size to whatever you need it to be. - setSize(400, 600); -} - -TapeLooperPluginAudioProcessorEditor::~TapeLooperPluginAudioProcessorEditor() -{ -} - -//============================================================================== -void TapeLooperPluginAudioProcessorEditor::paint(juce::Graphics& g) -{ - // (Our component is opaque, so we must completely fill the background with a solid colour) - g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); -} - -void TapeLooperPluginAudioProcessorEditor::resized() -{ - const auto numComponents = looperComponents_.size(); - const auto looperCompWidth = float(getWidth()) / float(numComponents); - for (int i = 0; i < numComponents; i++) - { - const auto rect = juce::Rectangle(0.0f, 0.0f, looperCompWidth, float(getHeight())); - looperComponents_[i]->setBounds(rect.withPosition(looperCompWidth * float(i), 0.0f).toNearestInt()); - } -} diff --git a/plugin/src/PluginEditor.h b/plugin/src/PluginEditor.h deleted file mode 100644 index fbc030d..0000000 --- a/plugin/src/PluginEditor.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "PluginProcessor.h" -#include "TapeLooperComponent.h" - -//============================================================================== -class TapeLooperPluginAudioProcessorEditor : public juce::AudioProcessorEditor -{ -public: - explicit TapeLooperPluginAudioProcessorEditor(TapeLooperPluginAudioProcessor&); - ~TapeLooperPluginAudioProcessorEditor() override; - - //============================================================================== - void paint(juce::Graphics&) override; - void resized() override; - -private: - // This reference is provided as a quick way for your editor to - // access the processor object that created it. - TapeLooperPluginAudioProcessor& processorRef; - - juce::OwnedArray looperComponents_; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TapeLooperPluginAudioProcessorEditor) -}; diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp deleted file mode 100644 index 8ea2d62..0000000 --- a/plugin/src/PluginProcessor.cpp +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "PluginProcessor.h" -#include "PluginEditor.h" -#include "DspDefinitions.h" - -//============================================================================== -TapeLooperPluginAudioProcessor::TapeLooperPluginAudioProcessor() : - AudioProcessor(BusesProperties() - .withInput("Input", juce::AudioChannelSet::stereo(), true) - .withOutput("Output", juce::AudioChannelSet::stereo(), true)), - parameters_(*this, nullptr, juce::Identifier("TapeLooper"), getParameterLayout()), - loopers_(parameters_) -{ -} - -TapeLooperPluginAudioProcessor::~TapeLooperPluginAudioProcessor() -{ -} - -//============================================================================== -const juce::String TapeLooperPluginAudioProcessor::getName() const -{ - return JucePlugin_Name; -} - -bool TapeLooperPluginAudioProcessor::acceptsMidi() const -{ -#if JucePlugin_WantsMidiInput - return true; -#else - return false; -#endif -} - -bool TapeLooperPluginAudioProcessor::producesMidi() const -{ -#if JucePlugin_ProducesMidiOutput - return true; -#else - return false; -#endif -} - -bool TapeLooperPluginAudioProcessor::isMidiEffect() const -{ -#if JucePlugin_IsMidiEffect - return true; -#else - return false; -#endif -} - -double TapeLooperPluginAudioProcessor::getTailLengthSeconds() const -{ - return 0.0; -} - -int TapeLooperPluginAudioProcessor::getNumPrograms() -{ - return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, - // so this should be at least 1, even if you're not really implementing programs. -} - -int TapeLooperPluginAudioProcessor::getCurrentProgram() -{ - return 0; -} - -void TapeLooperPluginAudioProcessor::setCurrentProgram(int index) -{ - juce::ignoreUnused(index); -} - -const juce::String TapeLooperPluginAudioProcessor::getProgramName(int index) -{ - juce::ignoreUnused(index); - return {}; -} - -void TapeLooperPluginAudioProcessor::changeProgramName(int index, const juce::String& newName) -{ - juce::ignoreUnused(index, newName); -} - -//============================================================================== -void TapeLooperPluginAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) -{ - juce::dsp::ProcessSpec specs; - specs.numChannels = juce::uint32(getMainBusNumInputChannels()); - specs.maximumBlockSize = juce::uint32(samplesPerBlock) * 2u; // safety for some weird hosts - specs.sampleRate = sampleRate; - - loopers_.prepareToPlay(specs); -} - -void TapeLooperPluginAudioProcessor::releaseResources() -{ - // When playback stops, you can use this as an opportunity to free up any - // spare memory, etc. -} - -bool TapeLooperPluginAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const -{ - // This is the place where you check if the layout is supported. - // Some plugin hosts, such as certain GarageBand versions, will only - // load plugins that support stereo bus layouts. - if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) - return false; - if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) - return false; - return true; -} - -void TapeLooperPluginAudioProcessor::processBlock(juce::AudioBuffer& buffer, - juce::MidiBuffer& midiMessages) -{ - juce::ignoreUnused(midiMessages); - - juce::ScopedNoDenormals noDenormals; - - loopers_.processBlock(buffer); -} - -//============================================================================== -bool TapeLooperPluginAudioProcessor::hasEditor() const -{ - return true; // (change this to false if you choose to not supply an editor) -} - -juce::AudioProcessorEditor* TapeLooperPluginAudioProcessor::createEditor() -{ - return new TapeLooperPluginAudioProcessorEditor(*this); -} - -//============================================================================== -void TapeLooperPluginAudioProcessor::getStateInformation(juce::MemoryBlock& 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) -{ - 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() -{ - juce::AudioProcessorValueTreeState::ParameterLayout result; - - for (size_t i = 0; i < dspdefs::numLoopers; i++) - { - const juce::String chStr(i); - - result.add(std::make_unique(juce::String(dspdefs::paramIds::chState) + chStr, // parameterID - "Playback State " + chStr, // parameter name - juce::StringArray({ "<", "stop", ">", "rec" }), // choices - 1) // default index - ); - juce::NormalisableRange speedRange(dspdefs::speedMin, dspdefs::speedMax); - speedRange.setSkewForCentre(1.0f); - result.add(std::make_unique(juce::String(dspdefs::paramIds::chSpeed) + chStr, // parameterID - "Speed " + chStr, // parameter name - speedRange, // mapping - 1.0f, // default value - "", // label - juce::AudioProcessorParameter::genericParameter, // category - [](float value, int) - { return juce::String(value, 2); }) // value-to-string function - ); - juce::NormalisableRange driveRange(0.25f, 4.0f); - driveRange.setSkewForCentre(1.0f); - result.add(std::make_unique(juce::String(dspdefs::paramIds::chDrive) + chStr, // parameterID - "Drive " + chStr, // parameter name - driveRange, // mapping - 1.0f, // default value - "", // label - juce::AudioProcessorParameter::genericParameter, // category - [](float value, int) - { return juce::String(value, 2); }) // value-to-string function - ); - result.add(std::make_unique(juce::String(dspdefs::paramIds::chGrainAmt) + chStr, // parameterID - "GrainAmt " + chStr, // parameter name - 0.0f, // minimum value - 1.0f, // maximum value - 0.25f) // default value - ); - result.add(std::make_unique(juce::String(dspdefs::paramIds::chWowAndFlutterAmt) + chStr, // parameterID - "WowAndFlutterAmt " + chStr, // parameter name - 0.0f, // minimum value - 1.0f, // maximum value - 0.25f) // default value - ); - result.add(std::make_unique(juce::String(dspdefs::paramIds::chPostGain) + chStr, // parameterID - "PostGain " + chStr, // parameter name - 0.0f, // minimum value - 2.0f, // maximum value - 1.0f) // default value - ); - } - - return result; -} - -//============================================================================== -// This creates new instances of the plugin.. -juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() -{ - return new TapeLooperPluginAudioProcessor(); -} diff --git a/plugin/src/PluginProcessor.h b/plugin/src/PluginProcessor.h deleted file mode 100644 index 3a1efaf..0000000 --- a/plugin/src/PluginProcessor.h +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include "TapeLooperProcessor.h" - -//============================================================================== -class TapeLooperPluginAudioProcessor : public juce::AudioProcessor -{ -public: - //============================================================================== - TapeLooperPluginAudioProcessor(); - ~TapeLooperPluginAudioProcessor() override; - - //============================================================================== - void prepareToPlay(double sampleRate, int samplesPerBlock) override; - void releaseResources() override; - - bool isBusesLayoutSupported(const BusesLayout& layouts) const override; - - void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; - using AudioProcessor::processBlock; - - //============================================================================== - juce::AudioProcessorEditor* createEditor() override; - bool hasEditor() const override; - - //============================================================================== - const juce::String getName() const override; - - bool acceptsMidi() const override; - bool producesMidi() const override; - bool isMidiEffect() const override; - double getTailLengthSeconds() const override; - - //============================================================================== - int getNumPrograms() override; - int getCurrentProgram() override; - void setCurrentProgram(int index) override; - const juce::String getProgramName(int index) override; - void changeProgramName(int index, const juce::String& newName) override; - - //============================================================================== - void getStateInformation(juce::MemoryBlock& destData) override; - void setStateInformation(const void* data, int sizeInBytes) override; - - juce::AudioProcessorValueTreeState parameters_; - -private: - //============================================================================== - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TapeLooperPluginAudioProcessor) - - TapeLooperBank loopers_; - - juce::AudioProcessorValueTreeState::ParameterLayout getParameterLayout(); -}; diff --git a/plugin/src/TapeLooperComponent.h b/plugin/src/TapeLooperComponent.h deleted file mode 100644 index 5864700..0000000 --- a/plugin/src/TapeLooperComponent.h +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -class PlayStopRecButtons : public juce::Component, private juce::Button::Listener -{ -public: - PlayStopRecButtons(juce::AudioParameterChoice& playStateParameter) : - paramAttachment_(playStateParameter, [this](float val) - { updateButtons(val); }) - { - recButton_.setButtonText("Rec"); - addAndMakeVisible(recButton_); - recButton_.addListener(this); - - revButton_.setButtonText("<"); - addAndMakeVisible(revButton_); - revButton_.addListener(this); - - stopButton_.setButtonText("Stop"); - addAndMakeVisible(stopButton_); - stopButton_.addListener(this); - - forwButton_.setButtonText(">"); - addAndMakeVisible(forwButton_); - forwButton_.addListener(this); - - paramAttachment_.sendInitialUpdate(); - } - - ~PlayStopRecButtons() override - { - recButton_.removeListener(this); - revButton_.removeListener(this); - stopButton_.removeListener(this); - forwButton_.removeListener(this); - } - - void resized() override - { - auto bounds = getLocalBounds(); - const auto quarterOfWidth = int(float(getWidth()) / 4.0f); - - recButton_.setBounds(bounds.removeFromTop(getHeight() / 2)); - revButton_.setBounds(bounds.removeFromLeft(quarterOfWidth)); - forwButton_.setBounds(bounds.removeFromRight(quarterOfWidth)); - stopButton_.setBounds(bounds); - } - -private: - juce::TextButton recButton_; - juce::TextButton revButton_; - juce::TextButton stopButton_; - juce::TextButton forwButton_; - juce::ParameterAttachment paramAttachment_; - - void updateButtons(float val) - { - const auto choice = int(std::round(val)); - revButton_.setToggleState(choice == 0, juce::NotificationType::dontSendNotification); - stopButton_.setToggleState(choice == 1, juce::NotificationType::dontSendNotification); - forwButton_.setToggleState(choice == 2, juce::NotificationType::dontSendNotification); - recButton_.setToggleState(choice == 3, juce::NotificationType::dontSendNotification); - } - - void buttonClicked(juce::Button* bttn) override - { - if (bttn == &revButton_) - paramAttachment_.setValueAsCompleteGesture(0.0f); - else if (bttn == &stopButton_) - paramAttachment_.setValueAsCompleteGesture(1.0f); - else if (bttn == &forwButton_) - paramAttachment_.setValueAsCompleteGesture(2.0f); - else if (bttn == &recButton_) - paramAttachment_.setValueAsCompleteGesture(3.0f); - } -}; - -class TapeLooperComponent : public juce::Component -{ -public: - TapeLooperComponent(juce::AudioParameterChoice& playStateParameter, - juce::AudioParameterFloat& speedParameter, - juce::AudioParameterFloat& driveParameter, - juce::AudioParameterFloat& grainAmtParameter, - juce::AudioParameterFloat& wowAndFlutterAmtParameter, - juce::AudioParameterFloat& postGainParameter) : - playStopRecButtons_(playStateParameter), - speedSliderAttachment_(speedParameter, speedSlider_), - driveSliderAttachment_(driveParameter, driveSlider_), - grainAmtSliderAttachment_(grainAmtParameter, grainAmtSlider_), - wowAndFlutterAmtSliderAttachment_(wowAndFlutterAmtParameter, wowAndFlutterAmtSlider_), - postGainSliderAttachment_(postGainParameter, postGainSlider_) - { - addAndMakeVisible(playStopRecButtons_); - - speedSlider_.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - speedSlider_.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 40, 25); - addAndMakeVisible(speedSlider_); - - driveSlider_.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - driveSlider_.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 40, 25); - addAndMakeVisible(driveSlider_); - - grainAmtSlider_.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - grainAmtSlider_.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 40, 25); - addAndMakeVisible(grainAmtSlider_); - - wowAndFlutterAmtSlider_.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - wowAndFlutterAmtSlider_.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 40, 25); - addAndMakeVisible(wowAndFlutterAmtSlider_); - - postGainSlider_.setSliderStyle(juce::Slider::SliderStyle::LinearVertical); - postGainSlider_.setTextBoxStyle(juce::Slider::TextEntryBoxPosition::TextBoxBelow, false, 40, 25); - addAndMakeVisible(postGainSlider_); - - speedSliderAttachment_.sendInitialUpdate(); - driveSliderAttachment_.sendInitialUpdate(); - grainAmtSliderAttachment_.sendInitialUpdate(); - wowAndFlutterAmtSliderAttachment_.sendInitialUpdate(); - postGainSliderAttachment_.sendInitialUpdate(); - } - - void resized() override - { - auto bounds = getLocalBounds().reduced(1); - - const auto buttonHeight = 25; - - playStopRecButtons_.setBounds(bounds.removeFromTop(2 * buttonHeight)); - bounds.removeFromTop(1); // padding - const auto rotarySliderWidth = std::min(bounds.getWidth(), 60); - speedSlider_.setBounds(bounds.removeFromTop(rotarySliderWidth + buttonHeight)); - bounds.removeFromTop(1); // padding - driveSlider_.setBounds(bounds.removeFromTop(rotarySliderWidth + buttonHeight)); - bounds.removeFromTop(1); // padding - grainAmtSlider_.setBounds(bounds.removeFromTop(rotarySliderWidth + buttonHeight)); - bounds.removeFromTop(1); // padding - wowAndFlutterAmtSlider_.setBounds(bounds.removeFromTop(rotarySliderWidth + buttonHeight)); - bounds.removeFromTop(1); // padding - postGainSlider_.setBounds(bounds); - } - - void paint(juce::Graphics& g) override - { - const auto drawVerticalText = [&](const juce::String& text, juce::Rectangle boundingBox) - { - g.saveState(); - g.setOrigin(boundingBox.getCentre()); - g.addTransform(juce::AffineTransform().rotated(-juce::MathConstants::halfPi)); - g.drawSingleLineText(text, - 0, - 0, - juce::Justification::horizontallyCentred); - g.restoreState(); - }; - - g.setColour(findColour(juce::Label::ColourIds::textColourId)); - drawVerticalText("Speed", speedSlider_.getBounds().withWidth(20)); - drawVerticalText("Drive", driveSlider_.getBounds().withWidth(20)); - drawVerticalText("Grain", grainAmtSlider_.getBounds().withWidth(20)); - drawVerticalText("Wow & Flutter", wowAndFlutterAmtSlider_.getBounds().withWidth(20)); - drawVerticalText("Volume", postGainSlider_.getBounds().withWidth(20)); - } - -private: - PlayStopRecButtons playStopRecButtons_; - juce::Slider speedSlider_; - juce::SliderParameterAttachment speedSliderAttachment_; - juce::Slider driveSlider_; - juce::SliderParameterAttachment driveSliderAttachment_; - juce::Slider grainAmtSlider_; - juce::SliderParameterAttachment grainAmtSliderAttachment_; - juce::Slider wowAndFlutterAmtSlider_; - juce::SliderParameterAttachment wowAndFlutterAmtSliderAttachment_; - juce::Slider postGainSlider_; - juce::SliderParameterAttachment postGainSliderAttachment_; -}; diff --git a/plugin/src/TapeLooperProcessor.h b/plugin/src/TapeLooperProcessor.h deleted file mode 100644 index df6a75c..0000000 --- a/plugin/src/TapeLooperProcessor.h +++ /dev/null @@ -1,391 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#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 -{ -public: - virtual ~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 -class TapeLooperProcessor : public ITapeLooperProcessor -{ -public: - TapeLooperProcessor(juce::AudioParameterChoice& stateParameter, - juce::AudioParameterFloat& speedParameter, - juce::AudioParameterFloat& driveParameter, - juce::AudioParameterFloat& grainAmtParameter, - juce::AudioParameterFloat& wowAndFlutterAmtParameter, - juce::AudioParameterFloat& postGainParameter, - const LooperStoragePtr<2>& storage) : - stateParameter_(stateParameter), - speedParameter_(speedParameter), - driveParameter_(driveParameter), - grainAmtParameter_(grainAmtParameter), - wowAndFlutterAmtParameter_(wowAndFlutterAmtParameter), - postGainParameter_(postGainParameter), - looper_(storage) - { - } - - void prepareToPlay() override - { - looper_.reset(); - } - - void processBlock(juce::dsp::AudioBlock input, - juce::dsp::AudioBlock outputToAddTo) override - { - const auto speed = speedParameter_.get(); - const auto drive = driveParameter_.get(); - const auto grainAmt = grainAmtParameter_.get(); - const auto wowAndFlutterAmt = wowAndFlutterAmtParameter_.get(); - const auto postGain = postGainParameter_.get(); - - Direction direction = Direction::forwards; - LooperState nextState = LooperState::stopped; - const auto stateIndex = stateParameter_.getIndex(); - if (stateIndex == 0) - { - nextState = LooperState::playing; - direction = Direction::backwards; - } - else if (stateIndex == 1) - nextState = LooperState::stopped; - else if (stateIndex == 2) - { - nextState = LooperState::playing; - direction = Direction::forwards; - } - else if (stateIndex == 3) - nextState = LooperState::recording; - - if (nextState != looper_.getState()) - looper_.switchState(nextState); - - typename TapeLooper::ProcessorType::Parameters params; - params.driveGain = drive; - params.grainAmt = grainAmt; - - const float* inChPtrs[2] = { input.getChannelPointer(0), - input.getChannelPointer(1) }; - float* outChPtrs[2] = { outputToAddTo.getChannelPointer(0), - outputToAddTo.getChannelPointer(1) }; - looper_.process(speed, - wowAndFlutterAmt, - direction, - params, - postGain, - AudioBufferPtr<2, const float>( - inChPtrs, input.getNumSamples()), - AudioBufferPtr<2, float>( - 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_; - juce::AudioParameterFloat& driveParameter_; - juce::AudioParameterFloat& grainAmtParameter_; - juce::AudioParameterFloat& wowAndFlutterAmtParameter_; - juce::AudioParameterFloat& postGainParameter_; - - struct ParameterValues - { - uint8_t state; - float speed; - float drive; - float grainAmt; - float wowAndFlutter; - float postGain; - }; - - using TapeLooperType = TapeLooper; - TapeLooperType looper_; -}; - -class TapeLooperBank -{ -public: - TapeLooperBank(juce::AudioProcessorValueTreeState& parameters) : - parameters_(parameters), - currentSampleRate_(0) - { - } - - void prepareToPlay(const juce::dsp::ProcessSpec& specs) - { - inputTmpBuffer_.setSize(2, int(specs.maximumBlockSize)); - inputTmpBuffer_.clear(); - - createInstancesFor(specs.sampleRate); - - for (auto processor : processors_) - processor->prepareToPlay(); - } - - void processBlock(juce::dsp::AudioBlock buffer) - { - for (size_t ch = 0; ch < std::min(buffer.getNumChannels(), size_t(inputTmpBuffer_.getNumChannels())); ch++) - inputTmpBuffer_.copyFrom(int(ch), 0, buffer.getChannelPointer(ch), int(buffer.getNumSamples())); - const auto inputBuffer = juce::dsp::AudioBlock( - inputTmpBuffer_.getArrayOfWritePointers(), - buffer.getNumChannels(), - buffer.getNumSamples()); - - // process loopers - buffer.clear(); - for (auto processor : processors_) - 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) - { - static constexpr auto supportedSampleRates = { 44100, 48000, 88200, 96000 }; - - if (sampleRate != currentSampleRate_) - { - // init storage - const int numSamples = int(std::ceil(dspdefs::loopLengthInS * sampleRate)); - storage_.setSize(int(dspdefs::numLoopers) * 2 /* stereo */, - numSamples, - false, - true); - - // init processors - const auto sampleRateToUse = getClosestSampleRate(supportedSampleRates, sampleRate); - switch (sampleRateToUse) - { - default: - case 44100u: - createProcessorInstances>(); - break; - case 48000u: - createProcessorInstances>(); - break; - case 88200u: - createProcessorInstances>(); - break; - case 96000u: - createProcessorInstances>(); - break; - } - } - - currentSampleRate_ = sampleRate; - } - - size_t getClosestSampleRate(const std::initializer_list& supportedSampleRates, double sampleRate) - { - size_t result = 0; - double currentDistance = 10.0e12; - for (const auto sr : supportedSampleRates) - { - const auto distance = std::abs(double(sr) - sampleRate); - if (distance < currentDistance) - { - result = size_t(sr); - currentDistance = distance; - } - } - return result; - } - - template - void createProcessorInstances() - { - processors_.clear(); - for (size_t i = 0; i < dspdefs::numLoopers; i++) - { - LooperStoragePtr<2> storagePtr; - storagePtr.data[0] = storage_.getWritePointer(int(2 * i)); - storagePtr.data[1] = storage_.getWritePointer(int(2 * i + 1)); - storagePtr.numSamples = size_t(storage_.getNumSamples()); - - const juce::String chStr(i); - auto& stateParameter = *(juce::AudioParameterChoice*) parameters_.getParameter(dspdefs::paramIds::chState + chStr); - auto& speedParameter = *(juce::AudioParameterFloat*) parameters_.getParameter(dspdefs::paramIds::chSpeed + chStr); - auto& driveParameter = *(juce::AudioParameterFloat*) parameters_.getParameter(dspdefs::paramIds::chDrive + chStr); - auto& grainAmtParameter = *(juce::AudioParameterFloat*) parameters_.getParameter(dspdefs::paramIds::chGrainAmt + chStr); - auto& wowAndFlutterAmtParameter = *(juce::AudioParameterFloat*) parameters_.getParameter(dspdefs::paramIds::chWowAndFlutterAmt + chStr); - auto& postGainParameter = *(juce::AudioParameterFloat*) parameters_.getParameter(dspdefs::paramIds::chPostGain + chStr); - - processors_.add(new ProcessorType(stateParameter, - speedParameter, - driveParameter, - grainAmtParameter, - wowAndFlutterAmtParameter, - postGainParameter, - storagePtr)); - } - } - - juce::AudioProcessorValueTreeState& parameters_; - - double currentSampleRate_; - juce::AudioBuffer storage_; - juce::AudioBuffer inputTmpBuffer_; - juce::OwnedArray processors_; -}; diff --git a/plugin/tests/Example_gtest.cpp b/plugin/tests/Example_gtest.cpp deleted file mode 100644 index 24ec4f1..0000000 --- a/plugin/tests/Example_gtest.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (C) Johannes Elliesen, 2021 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "PluginProcessor.h" -#include - -/* This example test shows how to create an instance of the plugin as - part of a test. */ -TEST(ExampleTest, CreatePluginAndGUITest) -{ - juce::ScopedJuceInitialiser_GUI juceInitializer; - auto messageManager = juce::MessageManager::getInstance(); - messageManager->setCurrentThreadAsMessageThread(); - - auto pluginInstance = std::make_unique(); - EXPECT_NE(pluginInstance, nullptr); - auto gui = pluginInstance->createEditorIfNeeded(); - EXPECT_NE(gui, nullptr); - - messageManager->runDispatchLoopUntil(200); - - pluginInstance->editorBeingDeleted(gui); - delete gui; - - pluginInstance.reset(); - messageManager->deleteInstance(); -} \ No newline at end of file