Skip to content

Commit

Permalink
feat: implement AudioSaveAndRecall
Browse files Browse the repository at this point in the history
  • Loading branch information
TheSlowGrowth committed Sep 4, 2024
1 parent 31f2897 commit b28aa33
Show file tree
Hide file tree
Showing 3 changed files with 696 additions and 0 deletions.
371 changes: 371 additions & 0 deletions firmware/src/AudioSaveAndRecall.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

#pragma once

#include <util/FixedCapStr.h>
#include <dsp/TapeLooper.h>
#include "util/WavFileFormat.h"

using AudioFileName = daisy::FixedCapStr<32>;

enum class AudioSaveAndRecallResult
{
ok,
Expand All @@ -37,4 +43,369 @@ class AudioSaveAndRecall
{
public:
typedef void (*DoneCallbackPtr)(void* context, AudioSaveAndRecallResult result);

AudioSaveAndRecall()
{
isStorageSlotUsedBitfield_[0] = 0;
isStorageSlotUsedBitfield_[1] = 0;
isStorageSlotUsedBitfield_[2] = 0;
}

void scanOrInitLibrary()
{
fileIo_.makeFolderIfNotExistent(libraryBaseFolder_);

for (size_t bank = 0; bank < kNumBanks; bank++)
{
for (size_t ch = 0; ch < kNumChannels; ch++)
{
for (size_t slot = 0; slot < kNumSlotsPerChannel; slot++)
{
const auto filename = makeWavFileNameFor(StorageBank(bank), ch, slot);

const auto indexInBitfield = ch * kNumSlotsPerChannel + slot;

if (const auto isInUse = fileIo_.hasFile(filename))
isStorageSlotUsedBitfield_[bank] |= (1 << indexInBitfield);
else
isStorageSlotUsedBitfield_[bank] &= ~(1 << indexInBitfield);
}
}
}
}

template <typename LooperType>
void startSavingToFile(StorageBank bank,
int channel,
int slot,
LooperType& looper,
DoneCallbackPtr doneCallback,
void* doneCallbackContext)
{
startSavingToFile(makeWavFileNameFor(bank, channel, slot),
looper,
doneCallback,
doneCallbackContext);
}

template <typename LooperType>
void startSavingToFile(const AudioFileName& filename,
LooperType& looper,
DoneCallbackPtr doneCallback,
void* doneCallbackContext)
{
static_assert(LooperType::getNumChannels() <= kMaxNumChannels_);

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

looper.preventRecording();
liftRestrictionsFunc_ = [](void* looperPtr)
{
LooperType* looper = reinterpret_cast<LooperType*>(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<LooperType*>(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;
};
}

template <typename LooperType>
void startReadingFromFile(StorageBank bank,
int channel,
int slot,
LooperType& looper,
DoneCallbackPtr doneCallback,
void* doneCallbackContext)
{
startReadingFromFile(makeWavFileNameFor(bank, channel, slot),
looper,
doneCallback,
doneCallbackContext);
}

template <typename LooperType>
void startReadingFromFile(const AudioFileName& filename,
LooperType& looper,
DoneCallbackPtr doneCallback,
void* doneCallbackContext)
{
static_assert(LooperType::getNumChannels() <= kMaxNumChannels_);

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

looper.preventPlaybackAndRecording();
looper.stopRecordingImmediately();
liftRestrictionsFunc_ = [](void* looperPtr)
{
LooperType* looper = reinterpret_cast<LooperType*>(looperPtr);
looper->liftRestrictions();
};

if (!fileIo_.openForReading(filename))
{
abort(true);
return;
}
// 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 ReadOrWriteResult::completed;
}
return ReadOrWriteResult::inProgress;
};
}

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

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

if (result == ReadOrWriteResult::error)
{
abort(true);
return;
}
else if (result == ReadOrWriteResult::completed)
{
liftRestrictionsFunc_(looperPtr_);
doneCallback_(doneCallbackContext_, AudioSaveAndRecallResult::ok);

doneCallback_ = nullptr;
doneCallbackContext_ = nullptr;
liftRestrictionsFunc_ = nullptr;
looperPtr_ = nullptr;
}
}
}

FileIoProvider& getFileIoProviderForTesting() { return fileIo_; }

private:
void abort(bool shouldCloseFile)
{
if (shouldCloseFile)
fileIo_.closeFile();

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_ = makeWavFileHeader32bitFloat(sampleRate, numChannelsPerFrame, totalNumFrames);

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

template <size_t numChannels>
bool writeBuffer(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 = &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--;
}

return fileIo_.write(writeBuffer_, sizeof(writeBuffer_));
}

AudioFileName makeFileNameWithoutExtensionFor(const StorageBank bank,
const int channel,
const int slot)
{
auto filename = libraryBaseFolder_;
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 (channel)
{
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)
{
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 channel,
const int slot)
{
return makeFileNameWithoutExtensionFor(bank, channel, slot) + ".wav";
}

static constexpr size_t kNumFramesPerChunk_ = 1000;
static constexpr size_t kBitsPerSample_ = 32u;
static constexpr size_t kMaxNumChannels_ = 2u;
static constexpr size_t kWriteBufferSize_ = 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;
DoneCallbackPtr doneCallback_ = nullptr;
void* doneCallbackContext_ = nullptr;

int32_t writeBuffer_[kWriteBufferSize_];

static constexpr auto kNumSlotsPerChannel = 4;
static constexpr auto kNumChannels = 4;
static constexpr auto kNumBanks = int(StorageBank::count);
int16_t isStorageSlotUsedBitfield_[kNumBanks];

const AudioFileName libraryBaseFolder_ = "loops";
};
Loading

0 comments on commit b28aa33

Please sign in to comment.