Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
TheSlowGrowth committed Aug 10, 2024
1 parent 3a80d11 commit 1a482ca
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 31 deletions.
222 changes: 222 additions & 0 deletions firmware/src/AudioSaveAndRecall.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <fatfs.h>
#include <util/FixedCapStr.h>

template <typename FileIoProvider>
class AudioSaveAndRecall
{
public:
using Filename = daisy::FixedCapStr<32>;
typedef void (*DoneCallbackPtr)(void* context, bool wasSuccessful);

AudioSaveAndRecall()
{
}

template <typename LooperType>
void startSavingToFile(const Filename& filename, LooperType& looper, DoneCallbackPtr doneCallback, void* doneCallbackContext)
{
static_assert(numChannels <= kMaxNumChannels);

looperPtr_ = &looper;
doneCallback_ = doneCallback;
doneCallbackContext_ = doneCallbackContext;

looper.preventRecording();

totalNumFrames_ = looper.getPlaybackLength();

fileIo_.openForWriting(filename);
if (!writeWavHeader(totalNumFrames_, looper.getNumChannels(), looper.getSampleRate()))
{
abort();
}

writeOrReadFunc_ = [](AudioSaveAndRecall* storage, void* looperPtr)
{
LooperType* looper = reinterpret_cast<LooperType*>(looperPtr);

// wait while the current recording is still crossfading out
if (looper->isRecording())
return { 0, false };

const auto numSamplesLeft = storage->totalNumFrames_ - storage->numFramesDone_;
const auto numFramesInThisChunk = std::min(numSamplesLeft, kChunkSize);

storage->prepareWriteBuffer(looper->getSampleStoragePtr(), storage->numFramesDone_, numFramesInThisChunk);
storage->fileIo_.write();

const auto isNowDone = numSamplesLeft == numFramesInThisChunk;
if (isNowDone)
{
storage->fileIo_.closeFile();
}

return { numFramesInThisChunk, isNowDone };
;
};
}

template <typename LooperType>
void startReadingFromFile(const Filename& filename, LooperType& looper, DoneCallbackPtr doneCallback, void* doneCallbackContext)
{
static_assert(numChannels <= kMaxNumChannels);

looperPtr_ = &looper;
doneCallback_ = doneCallback;
doneCallbackContext_ = doneCallbackContext;

looper.preventPlaybackAndRecording();
looper.stopRecordingImmediately();

fileIo_.openForReading(filename);
// TODO: read file header

totalNumFrames_ = 1000; // TODO

writeOrReadFunc_ = [](AudioSaveAndRecall* storage, void* looperPtr)
{
constexpr auto kChunkSize = 1000u;

LooperType* looper = reinterpret_cast<LooperType*>(looperPtr);

const auto numSamplesLeft = storage->totalNumFrames_ - storage->numFramesDone_;
const auto numFramesInThisChunk = std::min(numSamplesLeft, kChunkSize);

// TODO: read a chunk

const auto isNowDone = numSamplesLeft == numFramesInThisChunk;
if (isNowDone)
{
storage->fileIo_.closeFile();
}

return { numFramesInThisChunk, isNowDone };
};
}

float getCurrentProgress() const
{
if (totalNumFrames_ <= 0)
return 0.0f;
return float(numFramesDone_) / float(totalNumFrames_);
}

void readOrWriteNextChunk()
{
if (writeOrReadFunc_)
{
const auto result = writeOrReadFunc_(this, looperPtr_);

numFramesDone_ += result.numFramesCompleted;

if (result.isComplete)
{
doneCallback_(doneCallbackContext_);
writeOrReadFunc_ = nullptr;
}
}
}

private:
void abort()
{
fileIo_.closeFile();
if (doneCallback_)
{
doneCallback_(doneCallbackContext_, false);
}
doneCallback_ = nullptr;
doneCallbackContext_ = nullptr;
looperPtr_ = nullptr;
totalNumFrames_ = 0;
numFramesDone_ = 0;
}

bool writeWavHeader(size_t totalNumFrames, size_t numChannelsPerFrame, int sampleRate)
{
const auto byteRate = samplerate * numChannelsPerFrame * kBitsPerSample / 8;

wavHeader_.ChunkId = kWavFileChunkId; /** "RIFF" */
wavHeader_.FileFormat = kWavFileWaveId; /** "WAVE" */
wavHeader_.SubChunk1ID = kWavFileSubChunk1Id; /** "fmt " */
wavHeader_.SubChunk1Size = 16; // for PCM
wavHeader_.AudioFormat = WAVE_FORMAT_PCM;
wavHeader_.NbrChannels = numChannelsPerFrame;
wavHeader_.SampleRate = sampleRate;
wavHeader_.ByteRate = byteRate;
wavHeader_.BlockAlign = numChannelsPerFrame * kBitsPerSample / 8;
wavHeader_.BitPerSample = bitsPerSample;
wavHeader_.SubChunk2ID = kWavFileSubChunk2Id; /** "data" */
wavHeader_.SubCHunk2Size = totalNumFrames * numChannelsPerFrame * kBitsPerSample / 8;
wavHeader_.FileSize = 36 + wavHeader_.SubCHunk2Size;

return fileIo_.write(&wavHeader_, sizeof(wavHeader_)) == sizeof(wavHeader_);
}

template <size_t numChannels>
bool prepareWriteBuffer(LooperStoragePtr<numChannels>& 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 = &kWriteBuffer_[0];

size_t numFramesLeft = numFrames;
while (numFramesLeft > 0)
{
for (size_t ch = 0; ch < numChannels; ch++)
{
*outPtr = f2s32(*inPtr[ch]);
outPtr++;
inPtr[ch]++;
}
numFramesLeft--;
}
}

static constexpr auto kNumFramesPerChunk = 1000u;
static constexpr auto kBitsPerSample = 32u;
static constexpr auto kMaxNumChannels = 2u;
static constexpr auto kWriteBufferSize = kNumFramesPerChunk * kMaxNumChannels * kBitsPerSample / 8u;

WAV_FormatTypeDef wavHeader_;

struct ReadOrWriteResult
{
size_t numFramesCompleted = 0;
bool isComplete = false;
};
typedef ReadOrWriteResult (*WriteOrReadFuncPtr)(AudioSaveAndRecall* storage, void* looper);

FileIoProvider fileIo_;

size_t totalNumFrames_ = 0;
size_t numFramesDone_ = 0;
WriteOrReadFuncPtr writeOrReadFunc_ = nullptr;
void* looperPtr_ = nullptr;
DoneCallbackPtr doneCallback_ = nullptr;
void* doneCallbackContext_ = nullptr;

int32_t writeBuffer_[kWriteBufferSize_];
};
18 changes: 16 additions & 2 deletions firmware/src/LooperController.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ enum class MotorAcceleration
verySlow
};

enum class StorageBank
{
green,
yellow,
red
};

template <typename LooperTypes, size_t numLoopers>
class LooperController
{
Expand Down Expand Up @@ -77,16 +84,23 @@ class LooperController
return loopers_[looperIdx].looper.template as<StereoLooperType>().getState();
}

void saveTo(size_t looperIdx, size_t slot)
void saveTo(size_t looperIdx, StorageBank bank, size_t slot)
{
(void) (looperIdx);
(void) (bank);
(void) (slot);
// TODO
}

void loadFrom(size_t looperIdx, size_t slot)
float getCurrentSaveOrLoadProgress()
{
return 0.0f; // TODO
}

void loadFrom(size_t looperIdx, StorageBank bank, size_t slot)
{
(void) (looperIdx);
(void) (bank);
(void) (slot);
// TODO
}
Expand Down
60 changes: 47 additions & 13 deletions firmware/src/ui/UiSavePage.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,33 @@ class UiSavePage : public daisy::UiPage

bool IsOpaque(const daisy::UiCanvasDescriptor&) override { return false; }

void OnShow() override
{
stage_ = Stage::selectChannel;
channel_ = -1;
}

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<Led, 4> ledIds)
switch (stage_)
{
(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 });
case Stage::selectChannel:
drawSelectChannel(hardware);
break;
case Stage::selectBankAndSlot:
drawSelectBankAndSlot(hardware);
break;
case Stage::saveInProgress:
drawSaveInProgress(hardware);
break;
default:
break;
}
}

bool OnButton(uint16_t buttonID, uint8_t numberOfPresses, bool isRetriggering) override
Expand Down Expand Up @@ -98,5 +104,33 @@ class UiSavePage : public daisy::UiPage
UiSavePage(const UiSavePage&) = delete;
UiSavePage& operator=(const UiSavePage&) = delete;

void drawSelectChannel(UiHardwareType& /*hardware*/) {}

void drawSelectBankAndSlot(UiHardwareType& hardware)
{
const auto updateChannelOctaveLeds = [&](size_t looperChannel, std::array<Led, 4> 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 });
}

void drawSaveInProgress(UiHardwareType& /*hardware*/) {}

enum class Stage
{
selectChannel,
selectBankAndSlot,
saveInProgress,
};
Stage stage_ = Stage::selectChannel;
int channel_ = -1;
LooperControllerType& looperController_;
};
Empty file.
32 changes: 16 additions & 16 deletions firmware/tests/LooperController_gtest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ class StereoLooperMock : public LooperMock

// clang-format off
MOCK_METHOD(void, process, (
float speedParam,
float wowAndFlutterParam,
Direction direction,
ProcessorParametersMock processorParams,
float gainParam,
(AudioBufferPtr<2, const float>) input,
AudioBufferPtr<2> output,
ExponentialSmoother::TimeConstant postGainSmootherTimeConstant,
float speedParam,
float wowAndFlutterParam,
Direction direction,
ProcessorParametersMock processorParams,
float gainParam,
(AudioBufferPtr<2, const float>) input,
AudioBufferPtr<2> output,
ExponentialSmoother::TimeConstant postGainSmootherTimeConstant,
ExponentialSmoother::TimeConstant speedSmootherTimeConstant), ());
// clang-format on

Expand All @@ -101,14 +101,14 @@ class MonoLooperMock : public LooperMock

// clang-format off
MOCK_METHOD(void, process, (
float speedParam,
float wowAndFlutterParam,
Direction direction,
ProcessorParametersMock processorParams,
float gainParam,
(AudioBufferPtr<1, const float>) input,
AudioBufferPtr<1> output,
ExponentialSmoother::TimeConstant postGainSmootherTimeConstant,
float speedParam,
float wowAndFlutterParam,
Direction direction,
ProcessorParametersMock processorParams,
float gainParam,
(AudioBufferPtr<1, const float>) input,
AudioBufferPtr<1> output,
ExponentialSmoother::TimeConstant postGainSmootherTimeConstant,
ExponentialSmoother::TimeConstant speedSmootherTimeConstant), ());
// clang-format on

Expand Down

0 comments on commit 1a482ca

Please sign in to comment.