diff --git a/src/AudioLib/Mixer.cpp b/src/AudioLib/Mixer.cpp index c929c02..723b2f8 100644 --- a/src/AudioLib/Mixer.cpp +++ b/src/AudioLib/Mixer.cpp @@ -2,10 +2,10 @@ #include "../AudioSetup.hpp" //clipping wave to avoid overflows -int16_t clip(int32_t input){ +int16_t clip(int32_t input){ - if (input > 0x7FFF) return 0x7FFFF; - if (input < - 0x7FFF) return -0x7FFF; + if (input > INT16_MAX) return INT16_MAX; + if (input < INT16_MIN) return INT16_MIN; return input; } @@ -23,7 +23,7 @@ Mixer::~Mixer() size_t Mixer::generate(int16_t *outBuffer){ memset(outBuffer, 0, BUFFER_SIZE); - std::vector receivedSamples(sourceList.size(), 0); + std::fill(receivedSamples.begin(), receivedSamples.end(), 0); for(uint8_t i = 0; i < sourceList.size(); i++){ if(pauseList[i]) continue; @@ -31,9 +31,6 @@ size_t Mixer::generate(int16_t *outBuffer){ int16_t* buffer = bufferList[i]; if(generator != nullptr && buffer != nullptr){ receivedSamples[i] = generator->generate(buffer); - if(receivedSamples[i] == 0){ - pauseList[i] = true; - } } } @@ -41,7 +38,7 @@ size_t Mixer::generate(int16_t *outBuffer){ int32_t wave = 0; for(uint8_t j = 0; j < sourceList.size(); j++){ if(pauseList[j]) continue; - if(bufferList[j] == nullptr || receivedSamples[j] < i/NUM_CHANNELS) break; + if(bufferList[j] == nullptr || receivedSamples[j] <= i/NUM_CHANNELS) continue; if(sourceList.size() == 2){ wave += bufferList[j][i] * (float)((j == 1 ? (float)(mixRatio) : (float)(255.0 - mixRatio))/255.0); //use the mixer if only 2 tracks found @@ -51,17 +48,14 @@ size_t Mixer::generate(int16_t *outBuffer){ } outBuffer[i] = clip(wave); } - size_t longestBuffer = *std::max_element(receivedSamples.begin(), receivedSamples.end()); + size_t longestBuffer = receivedSamples.empty() + ? 0 + : *std::max_element(receivedSamples.begin(), receivedSamples.end()); if(longestBuffer == 0){ - bool allPaused = true; - for(bool p : pauseList){ - allPaused &= p; - } - - if(allPaused){ - return BUFFER_SAMPLES; - } + // SD-backed decoders can briefly underflow; silence this block without + // turning a recoverable read delay into a persistent deck pause. + return BUFFER_SAMPLES; } return longestBuffer; @@ -86,6 +80,7 @@ void Mixer::addSource(Generator* generator){ } bufferList.push_back(buffer); + receivedSamples.push_back(0); pauseList.push_back(false); } diff --git a/src/AudioLib/Mixer.h b/src/AudioLib/Mixer.h index 1fbb9af..0a2c496 100644 --- a/src/AudioLib/Mixer.h +++ b/src/AudioLib/Mixer.h @@ -28,6 +28,7 @@ class Mixer : public Generator private: std::vector sourceList; std::vector bufferList; + std::vector receivedSamples; uint8_t mixRatio = 122; //half-half by default, 0 = only first track, 255 = only second track std::vector pauseList; }; diff --git a/src/AudioLib/OutputWAV.cpp b/src/AudioLib/OutputWAV.cpp index 520e594..49d869f 100644 --- a/src/AudioLib/OutputWAV.cpp +++ b/src/AudioLib/OutputWAV.cpp @@ -1,25 +1,7 @@ #include "OutputWAV.h" -#include #include "../AudioSetup.hpp" #include "../PerfMon.h" - -struct WavHeader{ - char RIFF[4]; - uint32_t chunkSize; - char WAVE[4]; - char fmt[3]; - uint32_t fmtSize; - uint16_t audioFormat; - uint16_t numChannels; - uint32_t sampleRate; - uint32_t byteRate; // == SampleRate * NumChannels * BitsPerSample/8 - uint16_t blockAlign; // == NumChannels * BitsPerSample/8 - uint16_t bitsPerSample; - char data[4]; - uint32_t dataSize; // == NumSamples * NumChannels * BitsPerSample/8 -}; - OutputWAV::OutputWAV(){ freeBuffers.reserve(OUTWAV_BUFCOUNT); for(int i = 0; i < OUTWAV_BUFCOUNT; i++){ @@ -33,6 +15,10 @@ OutputWAV::OutputWAV(const fs::File& file) : OutputWAV(){ } OutputWAV::~OutputWAV(){ + delete finalizeResult; + for(auto result : writeResult){ + delete result; + } for(auto& outBuffer : outBuffers){ delete outBuffer; } @@ -48,71 +34,234 @@ void OutputWAV::setFile(const fs::File& file){ void OutputWAV::output(size_t numSamples){ Profiler.start("WAV write process"); - processWriteJob(); + service(); Profiler.end(); - while(freeBuffers.empty()){ - processWriteJob(); + const size_t size = numSamples * NUM_CHANNELS * BYTES_PER_SAMPLE; + if(error != RecordingError::NONE || finalizeStage != FinalizeStage::ACTIVE){ + droppedBytes += size; + return; + } + + if(freeBuffers.empty()){ + droppedBytes += size; + fail(RecordingError::BUFFER_OVERRUN); + return; } DataBuffer* buffer = outBuffers[freeBuffers.front()]; - - size_t size = numSamples * NUM_CHANNELS * BYTES_PER_SAMPLE; - dataLength += size; - memcpy(buffer->writeData(), this->inBuffer, size); buffer->writeMove(size); if(buffer->readAvailable() >= OUTWAV_WRITESIZE){ Profiler.start("WAV write add"); - addWriteJob(); + if(!addWriteJob()){ + droppedBytes += buffer->readAvailable(); + buffer->clear(); + fail(RecordingError::QUEUE_FULL); + } Profiler.end(); } } void OutputWAV::init(){ - dataLength = 0; + if(!prepared && file) begin(file); + if(!prepared || !file){ + Serial.println("Output file not open"); + fail(RecordingError::OPEN_FAILED); + } +} + +void OutputWAV::deinit(){ + finish(); +} + +bool OutputWAV::begin(const fs::File& outputFile){ + if(finalizeStage != FinalizeStage::DONE || hasPendingWrites()) return false; + + file = outputFile; + bytesWritten = 0; + droppedBytes = 0; + error = RecordingError::NONE; + prepared = false; + fileValid = false; + finalizeQueueRetries = 0; + freeBuffers.clear(); + for(uint8_t i = 0; i < OUTWAV_BUFCOUNT; i++){ + outBuffers[i]->clear(); + writePending[i] = false; + writeSize[i] = 0; + freeBuffers.push_back(i); + } if(!file){ - Serial.println("Output file not open"); - return; + fail(RecordingError::OPEN_FAILED); + return false; } - writeHeaderWAV(0); + header = makeWavHeader(0, NUM_CHANNELS, SAMPLE_RATE, BYTES_PER_SAMPLE); + prepared = true; + finalizeStage = FinalizeStage::INIT_SEEK_QUEUE; + return true; } -void OutputWAV::deinit(){ - Serial.println("Stopping WAV"); - if(!freeBuffers.empty() && outBuffers[freeBuffers.front()]->readAvailable() > 0){ - Serial.println("Writing last buffer"); - addWriteJob(); +void OutputWAV::finish(){ + if(finalizeStage != FinalizeStage::ACTIVE || !prepared) return; + + if(!freeBuffers.empty()){ + DataBuffer* buffer = outBuffers[freeBuffers.front()]; + if(buffer->readAvailable() > 0){ + if(error != RecordingError::NONE){ + droppedBytes += buffer->readAvailable(); + buffer->clear(); + }else if(!addWriteJob()){ + droppedBytes += buffer->readAvailable(); + buffer->clear(); + fail(RecordingError::QUEUE_FULL); + } + } + } + finalizeStage = FinalizeStage::DRAIN; +} + +void OutputWAV::service(){ + processWriteJob(); + if(finalizeStage == FinalizeStage::DONE || finalizeStage == FinalizeStage::ACTIVE) return; + + if(finalizeStage == FinalizeStage::INIT_SEEK_QUEUE){ + const FinalizeEnqueueResult result = + tryQueueFinalizeJob(SDJob::SD_SEEK, FinalizeStage::INIT_SEEK); + if(result == FinalizeEnqueueResult::EXHAUSTED){ + failInitialize(RecordingError::QUEUE_FULL); + } + return; + } + + if(finalizeStage == FinalizeStage::INIT_HEADER_QUEUE){ + const FinalizeEnqueueResult result = + tryQueueFinalizeJob(SDJob::SD_WRITE, FinalizeStage::INIT_HEADER); + if(result == FinalizeEnqueueResult::EXHAUSTED){ + failInitialize(RecordingError::QUEUE_FULL); + } + return; + } + + if(finalizeStage == FinalizeStage::INIT_SEEK || + finalizeStage == FinalizeStage::INIT_HEADER){ + if(finalizeResult == nullptr) return; + const bool success = finalizeResult->error == 0 && + (finalizeStage == FinalizeStage::INIT_SEEK || + finalizeResult->size == sizeof(WavHeader)); + delete finalizeResult; + finalizeResult = nullptr; + if(!success){ + failInitialize(RecordingError::WRITE_FAILED); + }else if(finalizeStage == FinalizeStage::INIT_SEEK){ + finalizeStage = FinalizeStage::INIT_HEADER_QUEUE; + }else{ + finalizeQueueRetries = 0; + finalizeStage = FinalizeStage::ACTIVE; + } + return; + } + + if(finalizeStage == FinalizeStage::DRAIN && hasPendingWrites()) return; + + if(finalizeStage == FinalizeStage::DRAIN){ + header = makeWavHeader(bytesWritten, NUM_CHANNELS, SAMPLE_RATE, BYTES_PER_SAMPLE); + if(tryQueueFinalizeJob(SDJob::SD_SEEK, FinalizeStage::SEEK) == + FinalizeEnqueueResult::EXHAUSTED){ + failFinalize(); + } + return; } - Serial.println("Waiting jobs"); + if(finalizeStage == FinalizeStage::HEADER_QUEUE){ + if(tryQueueFinalizeJob(SDJob::SD_WRITE, FinalizeStage::HEADER) == + FinalizeEnqueueResult::EXHAUSTED){ + failFinalize(); + } + return; + } + + if(finalizeResult == nullptr) return; + const bool success = finalizeResult->error == 0 && + (finalizeStage == FinalizeStage::SEEK || finalizeResult->size == sizeof(WavHeader)); + delete finalizeResult; + finalizeResult = nullptr; + if(!success){ + failFinalize(); + return; + } - while(freeBuffers.size() != OUTWAV_BUFCOUNT){ - processWriteJob(); + if(finalizeStage == FinalizeStage::SEEK){ + finalizeStage = FinalizeStage::HEADER_QUEUE; + }else{ + finalizeStage = FinalizeStage::DONE; + prepared = false; + fileValid = true; } +} + +bool OutputWAV::isFinalized() const{ + return finalizeStage == FinalizeStage::DONE; +} + +bool OutputWAV::isPrepared() const{ + return prepared; +} + +bool OutputWAV::isReady() const{ + return finalizeStage == FinalizeStage::ACTIVE; +} + +bool OutputWAV::isFileValid() const{ + return fileValid; +} + +void OutputWAV::invalidateFile(){ + fileValid = false; +} + +RecordingError OutputWAV::getError() const{ + return error; +} - Serial.println("Writing header"); +uint32_t OutputWAV::getBytesWritten() const{ + return bytesWritten; +} + +uint32_t OutputWAV::getDroppedBytes() const{ + return droppedBytes; +} + +uint32_t OutputWAV::getDurationMs() const{ + const uint32_t byteRate = SAMPLE_RATE * NUM_CHANNELS * BYTES_PER_SAMPLE; + return byteRate == 0 ? 0 : static_cast(bytesWritten) * 1000 / byteRate; +} - writeHeaderWAV(dataLength); +uint8_t OutputWAV::getFinalizeQueueRetries() const{ + return finalizeQueueRetries; } -void OutputWAV::addWriteJob(){ - if(freeBuffers.empty()) return; - uint8_t i = freeBuffers.front(); +bool OutputWAV::addWriteJob(){ + if(freeBuffers.empty()) return false; + const uint8_t i = freeBuffers.front(); + const size_t size = outBuffers[i]->readAvailable(); + if(size == 0) return true; - Sched.addJob(new SDJob{ + if(!Sched.tryAddJob(new SDJob{ .type = SDJob::SD_WRITE, .file = file, - .size = outBuffers[i]->readAvailable(), + .size = size, .buffer = const_cast(outBuffers[i]->readData()), .result = &writeResult[i] - }); + })) return false; freeBuffers.erase(freeBuffers.begin()); writePending[i] = true; + writeSize[i] = size; + return true; } void OutputWAV::processWriteJob(){ @@ -120,60 +269,68 @@ void OutputWAV::processWriteJob(){ if(!writePending[i]) continue; if(writeResult[i] == nullptr) continue; - outBuffers[i]->clear(); + const size_t actual = writeResult[i]->size > writeSize[i] + ? writeSize[i] + : writeResult[i]->size; + bytesWritten += actual; + if(writeResult[i]->error != 0 || actual != writeSize[i]){ + droppedBytes += writeSize[i] - actual; + fail(RecordingError::WRITE_FAILED); + } + outBuffers[i]->clear(); delete writeResult[i]; writeResult[i] = nullptr; - writePending[i] = false; + writeSize[i] = 0; freeBuffers.push_back(i); } } -void OutputWAV::writeHeaderWAV(size_t size){ - WavHeader header; - memcpy(header.RIFF, "RIFF", 4); - header.chunkSize = size + 36; - memcpy(header.WAVE, "WAVE", 4); - memcpy(header.fmt, "fmt ", 4); - header.fmtSize = 16; - header.audioFormat = 1; //PCM - header.numChannels = NUM_CHANNELS; //2 channels - header.sampleRate = SAMPLE_RATE; - header.byteRate = SAMPLE_RATE * NUM_CHANNELS * BYTES_PER_SAMPLE; - header.blockAlign = NUM_CHANNELS * BYTES_PER_SAMPLE; - header.bitsPerSample = BYTES_PER_SAMPLE * 8; - memcpy(header.data, "data", 4); - header.dataSize = size; - - Sched.addJob(new SDJob { - .type = SDJob::SD_SEEK, - .file = file, - .size = 0, - .buffer = nullptr, - .result = nullptr - }); +bool OutputWAV::hasPendingWrites() const{ + for(bool pending : writePending){ + if(pending) return true; + } + return false; +} - Sched.addJob(new SDJob { - .type = SDJob::SD_SEEK, +bool OutputWAV::queueFinalizeJob(SDJob::Type type){ + return Sched.tryAddJob(new SDJob { + .type = type, .file = file, - .size = 0, - .buffer = nullptr, - .result = nullptr + .size = type == SDJob::SD_SEEK ? 0 : sizeof(WavHeader), + .buffer = type == SDJob::SD_SEEK ? nullptr : reinterpret_cast(&header), + .result = &finalizeResult }); +} - SDResult* result = nullptr; - Sched.addJob(new SDJob { - .type = SDJob::SD_WRITE, - .file = file, - .size = sizeof(WavHeader), - .buffer = reinterpret_cast(&header), - .result = &result - }); +FinalizeEnqueueResult OutputWAV::tryQueueFinalizeJob( + SDJob::Type type, + FinalizeStage queuedStage +){ + const FinalizeEnqueueResult result = recordFinalizeEnqueue( + queueFinalizeJob(type), + finalizeQueueRetries, + OUTWAV_FINALIZE_QUEUE_FAILURES + ); + if(result == FinalizeEnqueueResult::QUEUED) finalizeStage = queuedStage; + return result; +} - while(result == nullptr){ - delayMicroseconds(1); - } +void OutputWAV::failInitialize(RecordingError recordingError){ + error = recordingError; + fileValid = false; + prepared = false; + finalizeStage = FinalizeStage::DONE; +} + +void OutputWAV::failFinalize(){ + error = RecordingError::FINALIZE_FAILED; + fileValid = false; + prepared = false; + finalizeStage = FinalizeStage::DONE; +} - delete result; +void OutputWAV::fail(RecordingError recordingError){ + if(error == RecordingError::NONE) error = recordingError; } diff --git a/src/AudioLib/OutputWAV.h b/src/AudioLib/OutputWAV.h index 0bbe51a..3716cda 100644 --- a/src/AudioLib/OutputWAV.h +++ b/src/AudioLib/OutputWAV.h @@ -7,10 +7,23 @@ #include #include #include "../Services/SDScheduler.h" +#include "RecordingFinalize.h" +#include "WavHeader.h" #define OUTWAV_BUFSIZE 2 * 1024 * NUM_CHANNELS #define OUTWAV_WRITESIZE 1 * 1024 * NUM_CHANNELS // should be smaller than BUFSIZE #define OUTWAV_BUFCOUNT 16 +#define OUTWAV_FINALIZE_QUEUE_FAILURES 32 + +enum class RecordingError : uint8_t { + NONE, + SD_UNAVAILABLE, + OPEN_FAILED, + WRITE_FAILED, + FINALIZE_FAILED, + BUFFER_OVERRUN, + QUEUE_FULL +}; class OutputWAV : public Output { @@ -22,24 +35,63 @@ class OutputWAV : public Output void deinit() override; const fs::File& getFile() const; void setFile(const fs::File& file); + bool begin(const fs::File& file); + void finish(); + void service(); + bool isFinalized() const; + bool isPrepared() const; + bool isReady() const; + bool isFileValid() const; + void invalidateFile(); + RecordingError getError() const; + uint32_t getBytesWritten() const; + uint32_t getDroppedBytes() const; + uint32_t getDurationMs() const; + uint8_t getFinalizeQueueRetries() const; protected: - void output(size_t numBytes) override; + void output(size_t numSamples) override; private: - const char* path; fs::File file; - size_t dataLength; - - void writeHeaderWAV(size_t size); + uint32_t bytesWritten = 0; + uint32_t droppedBytes = 0; + RecordingError error = RecordingError::NONE; + bool prepared = false; bool writePending[OUTWAV_BUFCOUNT] = { false }; SDResult* writeResult[OUTWAV_BUFCOUNT] = { nullptr }; - void addWriteJob(); + size_t writeSize[OUTWAV_BUFCOUNT] = { 0 }; + bool addWriteJob(); void processWriteJob(); + bool hasPendingWrites() const; DataBuffer* outBuffers[OUTWAV_BUFCOUNT] = { nullptr }; std::vector freeBuffers; + + enum class FinalizeStage : uint8_t { + DONE, + INIT_SEEK_QUEUE, + INIT_SEEK, + INIT_HEADER_QUEUE, + INIT_HEADER, + ACTIVE, + DRAIN, + SEEK, + HEADER_QUEUE, + HEADER + }; + FinalizeStage finalizeStage = FinalizeStage::DONE; + SDResult* finalizeResult = nullptr; + WavHeader header = {}; + bool fileValid = false; + uint8_t finalizeQueueRetries = 0; + + bool queueFinalizeJob(SDJob::Type type); + FinalizeEnqueueResult tryQueueFinalizeJob(SDJob::Type type, FinalizeStage queuedStage); + void failInitialize(RecordingError recordingError); + void failFinalize(); + void fail(RecordingError recordingError); }; diff --git a/src/AudioLib/RecordingFinalize.h b/src/AudioLib/RecordingFinalize.h new file mode 100644 index 0000000..15551fd --- /dev/null +++ b/src/AudioLib/RecordingFinalize.h @@ -0,0 +1,24 @@ +#ifndef JAYD_LIBRARY_RECORDINGFINALIZE_H +#define JAYD_LIBRARY_RECORDINGFINALIZE_H + +#include + +enum class FinalizeEnqueueResult : uint8_t { + QUEUED, + RETRY, + EXHAUSTED +}; + +inline FinalizeEnqueueResult recordFinalizeEnqueue( + bool queued, + uint8_t& failures, + uint8_t maxFailures +){ + if(queued) return FinalizeEnqueueResult::QUEUED; + if(failures < maxFailures) failures++; + return failures >= maxFailures + ? FinalizeEnqueueResult::EXHAUSTED + : FinalizeEnqueueResult::RETRY; +} + +#endif //JAYD_LIBRARY_RECORDINGFINALIZE_H diff --git a/src/AudioLib/SourceAAC.cpp b/src/AudioLib/SourceAAC.cpp index 54b3bbf..a30ecca 100644 --- a/src/AudioLib/SourceAAC.cpp +++ b/src/AudioLib/SourceAAC.cpp @@ -47,10 +47,14 @@ void SourceAAC::setSongDoneCallback(void (*callback)()) { songDoneCallback = callback; } +bool SourceAAC::isReadReady() const { + return !readJobPending || readResult != nullptr; +} + void SourceAAC::close(){ if(readJobPending){ while(readResult == nullptr){ - delayMicroseconds(1); + Sched.loop(0); } free(readResult->buffer); @@ -306,4 +310,3 @@ void SourceAAC::resetDecoding() { void SourceAAC::setRepeat(bool repeat) { SourceAAC::repeat = repeat; } - diff --git a/src/AudioLib/SourceAAC.h b/src/AudioLib/SourceAAC.h index 172df00..e8fae7a 100644 --- a/src/AudioLib/SourceAAC.h +++ b/src/AudioLib/SourceAAC.h @@ -33,6 +33,7 @@ class SourceAAC : public Source void setRepeat(bool repeat); void setSongDoneCallback(void (*callback)()); + bool isReadReady() const; private: fs::File file; diff --git a/src/AudioLib/Systems/MixSystem.cpp b/src/AudioLib/Systems/MixSystem.cpp index ed51cc7..f53b9e1 100644 --- a/src/AudioLib/Systems/MixSystem.cpp +++ b/src/AudioLib/Systems/MixSystem.cpp @@ -13,7 +13,7 @@ MixSystem::MixSystem(const fs::File& f1, const fs::File& f2) : MixSystem(){ open(1, f2); } -MixSystem::MixSystem() : audioTask("MixAudio", audioThread, 16 * 1024, this), queue(6, sizeof(MixRequest*)){ +MixSystem::MixSystem() : audioTask("MixAudio", audioThread, 16 * 1024, this), queue(requestCapacity, sizeof(uint8_t)){ mixer = new Mixer(); for(int i = 0; i < 2; i++){ @@ -64,85 +64,222 @@ MixSystem::~MixSystem(){ delete speed[i]; delete source[i]; + delete retiredSource[i]; } } bool MixSystem::open(uint8_t c, const fs::File& file){ - this->file[c] = file; - if(!file){ + if(c >= 2 || !file){ Serial.println("MixSystem: file not open"); return false; } - delete source[c]; - auto source = this->source[c] = new SourceAAC(file); + auto newSource = new SourceAAC(file); + if(newSource == nullptr){ + Serial.println("MixSystem: source allocation failed"); + return false; + } + newSource->setRepeat(true); + while(!newSource->isReadReady()) Sched.loop(0); + return replaceSource(c, newSource); +} - source->setRepeat(true); +bool MixSystem::replaceSource(uint8_t c, SourceAAC* newSource){ + if(c >= 2 || newSource == nullptr){ + delete newSource; + return false; + } + sourceMutex.lock(); + if(retiredSource[c] != nullptr){ + sourceMutex.unlock(); + delete newSource; + return false; + } + newSource->setVolume(volume[c]); + auto oldSource = source[c]; + source[c] = newSource; if(speed[c]){ - speed[c]->setSource(source); + speed[c]->setSource(newSource); }else{ - effector[c]->setSource(source); + effector[c]->setSource(newSource); } + retiredSource[c] = oldSource; + sourceMutex.unlock(); return true; } +bool MixSystem::openChannel(uint8_t channel, const fs::File& file){ + if(channel >= 2 || !file) return false; + + if(!out->isRunning()){ + return open(channel, file); + } + + cleanupRetiredSources(); + sourceMutex.lock(); + const bool canReplace = retiredSource[channel] == nullptr; + sourceMutex.unlock(); + if(!canReplace) return false; + + const int8_t requestIndex = reserveRequest({ MixRequest::OPEN, channel }); + if(requestIndex < 0) return false; + + auto newSource = new SourceAAC(file); + if(newSource == nullptr){ + releaseRequest(requestIndex); + return false; + } + newSource->setRepeat(true); + while(!newSource->isReadReady()) Sched.loop(0); + requests[requestIndex].value = reinterpret_cast(newSource); + + if(sendRequest(requestIndex)) return true; + + delete newSource; + releaseRequest(requestIndex); + return false; +} + +void MixSystem::_openChannel(uint8_t channel, SourceAAC* newSource){ + if(channel >= 2 || newSource == nullptr){ + delete newSource; + return; + } + + const bool wasPaused = mixer->isChannelPaused(channel); + mixer->pauseChannel(channel); + if(replaceSource(channel, newSource) || !wasPaused) mixer->resumeChannel(channel); +} + +int8_t MixSystem::reserveRequest(const MixRequest& request){ + queueMutex.lock(); + for(uint8_t i = 0; i < requestCapacity; i++){ + if(requestUsed[i] && request.type == MixRequest::OPEN && + requests[i].type == MixRequest::OPEN && requests[i].channel == request.channel){ + queueMutex.unlock(); + return -1; + } + } + + for(uint8_t i = 0; i < requestCapacity; i++){ + if(requestUsed[i]) continue; + requests[i] = request; + requestUsed[i] = true; + queueMutex.unlock(); + return i; + } + queueMutex.unlock(); + return -1; +} + +bool MixSystem::sendRequest(uint8_t index){ + if(index >= requestCapacity || !requestUsed[index]) return false; + return queue.send(&index); +} + +bool MixSystem::enqueueRequest(const MixRequest& request){ + const int8_t index = reserveRequest(request); + if(index < 0) return false; + if(sendRequest(index)) return true; + releaseRequest(index); + return false; +} + +void MixSystem::releaseRequest(uint8_t index){ + if(index >= requestCapacity) return; + queueMutex.lock(); + requests[index] = {}; + requestUsed[index] = false; + queueMutex.unlock(); +} + +void MixSystem::clearRequests(){ + uint8_t index; + while(queue.count()){ + if(!queue.receive(&index)) break; + if(index < requestCapacity && requestUsed[index] && requests[index].type == MixRequest::OPEN){ + delete reinterpret_cast(requests[index].value); + } + releaseRequest(index); + } +} + +void MixSystem::cleanupRetiredSources(){ + SourceAAC* ready[2] = {}; + sourceMutex.lock(); + for(uint8_t channel = 0; channel < 2; channel++){ + if(retiredSource[channel] && retiredSource[channel]->isReadReady()){ + ready[channel] = retiredSource[channel]; + retiredSource[channel] = nullptr; + } + } + sourceMutex.unlock(); + delete ready[0]; + delete ready[1]; +} + void MixSystem::audioThread(Task* task){ MixSystem* system = static_cast(task->arg); Serial.println("-- MixSystem started --"); while(task->running){ - MixRequest* request; + system->serviceRecording(); + uint8_t requestIndex; while(system->queue.count()){ - system->queue.receive(&request); + if(!system->queue.receive(&requestIndex)) break; + if(requestIndex >= requestCapacity || !system->requestUsed[requestIndex]) continue; + const MixRequest request = system->requests[requestIndex]; - switch(request->type){ + switch(request.type){ case MixRequest::ADD_SPEED: - system->_addSpeed(request->channel); + system->_addSpeed(request.channel); break; case MixRequest::REMOVE_SPEED: - system->_removeSpeed(request->channel); + system->_removeSpeed(request.channel); break; case MixRequest::SET_SPEED: - system->_setSpeed(request->channel, request->value); + system->_setSpeed(request.channel, request.value); break; case MixRequest::SET_EFFECT: - system->_setEffect(request->channel, request->slot, static_cast(request->value)); + system->_setEffect(request.channel, request.slot, static_cast(request.value)); break; case MixRequest::SET_EFFECT_INTENSITY: - system->_setEffectIntensity(request->channel, request->slot, request->value); + system->_setEffectIntensity(request.channel, request.slot, request.value); break; case MixRequest::SET_INFO: - system->_setInfoGenerator(request->channel, (InfoGenerator*) request->value); + system->_setInfoGenerator(request.channel, reinterpret_cast(request.value)); break; case MixRequest::SET_SEEK: - system->_seekChannel(request->channel, (uint16_t) request->value); + system->_seekChannel(request.channel, static_cast(request.value)); break; case MixRequest::RECORD: - if(request->value == system->isRecording()) break; - if(request->value){ + if(request.value){ system->_startRecording(); }else{ system->_stopRecording(); } break; + case MixRequest::OPEN: + system->_openChannel(request.channel, reinterpret_cast(request.value)); + break; } - - delete request; + system->releaseRequest(requestIndex); } if(system->out->isRunning()){ Profiler.init(); system->out->loop(0); + system->serviceRecording(); Profiler.report(); }else{ system->running = false; } } - system->fsOut->stop(); + system->_stopRecording(); } void MixSystem::start(){ @@ -154,18 +291,19 @@ void MixSystem::start(){ } void MixSystem::stop(){ - if(!running) return; - - audioTask.stop(); - - while(!audioTask.isStopped()){ - Sched.loop(0); + if(!audioTask.isStopped()){ + audioTask.stop(); + while(!audioTask.isStopped()){ + Sched.loop(0); + } } + running = false; _stopRecording(); - fileOut.close(); + finishRecordingSync(); out->stop(); + clearRequests(); } bool MixSystem::isRunning(){ @@ -173,21 +311,49 @@ bool MixSystem::isRunning(){ } uint16_t MixSystem::getDuration(uint8_t c){ - if(c >= 2 || !source[c]) return 0; - return source[c]->getDuration(); + if(c >= 2) return 0; + cleanupRetiredSources(); + sourceMutex.lock(); + uint16_t duration = source[c] ? source[c]->getDuration() : 0; + sourceMutex.unlock(); + return duration; } uint16_t MixSystem::getElapsed(uint8_t c){ - if(c >= 2 || !source[c]) return 0; + if(c >= 2) return 0; + cleanupRetiredSources(); if(seekPending[c] > 0){ return seek[c]; } - return source[c]->getElapsed(); + sourceMutex.lock(); + uint16_t elapsed = source[c] ? source[c]->getElapsed() : 0; + sourceMutex.unlock(); + return elapsed; +} + +bool MixSystem::hasChannel(uint8_t c){ + if(c >= 2) return false; + cleanupRetiredSources(); + sourceMutex.lock(); + bool loaded = source[c] != nullptr; + sourceMutex.unlock(); + return loaded; +} + +uint8_t MixSystem::getVolume(uint8_t c){ + return c < 2 ? volume[c] : 0; +} + +uint8_t MixSystem::getMix(){ + return mixer ? mixer->getMixRatio() : 128; } void MixSystem::setVolume(uint8_t c, uint8_t volume){ - if(c >= 2 || !source[c]) return; - source[c]->setVolume(volume); + if(c >= 2) return; + this->volume[c] = volume; + sourceMutex.lock(); + if(source[c]) source[c]->setVolume(volume); + sourceMutex.unlock(); } void MixSystem::setMix(uint8_t ratio){ @@ -201,9 +367,7 @@ void MixSystem::addSpeed(uint8_t channel){ return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::ADD_SPEED, channel }); - queue.send(&request); + enqueueRequest({ MixRequest::ADD_SPEED, channel }); } void MixSystem::removeSpeed(uint8_t channel){ @@ -212,9 +376,7 @@ void MixSystem::removeSpeed(uint8_t channel){ return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::REMOVE_SPEED, channel }); - queue.send(&request); + enqueueRequest({ MixRequest::REMOVE_SPEED, channel }); } void MixSystem::setSpeed(uint8_t channel, uint8_t speed){ @@ -223,9 +385,7 @@ void MixSystem::setSpeed(uint8_t channel, uint8_t speed){ return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::SET_SPEED, channel, 0, speed }); - queue.send(&request); + enqueueRequest({ MixRequest::SET_SPEED, channel, 0, speed }); } void MixSystem::setEffect(uint8_t channel, uint8_t slot, EffectType type){ @@ -234,9 +394,7 @@ void MixSystem::setEffect(uint8_t channel, uint8_t slot, EffectType type){ return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::SET_EFFECT, channel, slot, static_cast(type) }); - queue.send(&request); + enqueueRequest({ MixRequest::SET_EFFECT, channel, slot, static_cast(type) }); } void MixSystem::setEffectIntensity(uint8_t channel, uint8_t slot, uint8_t intensity){ @@ -245,13 +403,11 @@ void MixSystem::setEffectIntensity(uint8_t channel, uint8_t slot, uint8_t intens return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::SET_EFFECT_INTENSITY, channel, slot, intensity }); - queue.send(&request); + enqueueRequest({ MixRequest::SET_EFFECT_INTENSITY, channel, slot, intensity }); } void MixSystem::_addSpeed(uint8_t c){ - if(c >= 2 || !effector[c] || speed[c]) return; + if(c >= 2 || !effector[c] || !source[c] || speed[c]) return; auto speed = this->speed[c] = new SpeedModifier(source[c]); effector[c]->setSource(speed); } @@ -309,9 +465,7 @@ void MixSystem::setChannelInfo(uint8_t channel, InfoGenerator* channelInfoGen){ return; } - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::SET_INFO, channel, 0, (size_t) channelInfoGen }); - queue.send(&request); + enqueueRequest({ MixRequest::SET_INFO, channel, 0, reinterpret_cast(channelInfoGen) }); } void MixSystem::pauseChannel(uint8_t channel){ @@ -326,6 +480,8 @@ void MixSystem::resumeChannel(uint8_t channel){ } void MixSystem::seekChannel(uint8_t channel, uint16_t time){ + if(channel >= 2) return; + if(!out->isRunning()){ _seekChannel(channel, time); return; @@ -333,75 +489,217 @@ void MixSystem::seekChannel(uint8_t channel, uint16_t time){ seek[channel] = time; seekPending[channel]++; - - if(queue.count() == queue.getQueueSize()) return; - MixRequest* request = new MixRequest({ MixRequest::SET_SEEK, channel, 0, time }); - queue.send(&request); + if(!enqueueRequest({ MixRequest::SET_SEEK, channel, 0, time })) seekPending[channel]--; } void MixSystem::_seekChannel(uint8_t channel, uint16_t time){ if(channel > 1) return; + if(seekPending[channel] > 0) seekPending[channel]--; + if(!source[channel]) return; - if(i2s->isRunning()){ - seekPending[channel]--; - //i2s_zero_dma_buffer((i2s_port_t) 0); - } source[channel]->seek(time, SeekSet); } bool MixSystem::isRecording(){ - return out->getOutput(1) != nullptr; -} + return recordingState == RecordingState::RECORDING; +} + +RecordingStatus MixSystem::getRecordingStatus() const{ + if(!recordingApplied){ + return { + recordingState, + recordingError, + 0, + 0, + 0, + 0, + false + }; + } + const RecordingError outputError = fsOut->getError(); + return { + recordingState, + recordingError == RecordingError::NONE ? outputError : recordingError, + fsOut->getBytesWritten(), + fsOut->getDurationMs(), + fsOut->getDroppedBytes(), + fsOut->getFinalizeQueueRetries(), + fsOut->isFileValid() + }; +} + +bool MixSystem::startRecording(){ + recordingMutex.lock(); + if(recordingState == RecordingState::STARTING || + recordingState == RecordingState::RECORDING || + recordingState == RecordingState::STOPPING){ + recordingMutex.unlock(); + return false; + } + + recordingState = RecordingState::STARTING; + recordingError = RecordingError::NONE; + recordingApplied = false; -void MixSystem::startRecording(){ if(!out->isRunning()){ _startRecording(); - return; + const bool accepted = recordingState != RecordingState::FAILED; + recordingMutex.unlock(); + return accepted; + } + + if(enqueueRequest({ MixRequest::RECORD, 0, 0, 1 })){ + recordingMutex.unlock(); + return true; } - MixRequest* request = new MixRequest({ MixRequest::RECORD, 0, 0, 1 }); - queue.send(&request); + recordingError = RecordingError::QUEUE_FULL; + recordingState = RecordingState::FAILED; + recordingMutex.unlock(); + return false; } -void MixSystem::stopRecording(){ +bool MixSystem::stopRecording(){ + recordingMutex.lock(); + if(recordingState == RecordingState::IDLE || + recordingState == RecordingState::COMPLETE || + recordingState == RecordingState::FAILED || + recordingState == RecordingState::STOPPING){ + recordingMutex.unlock(); + return true; + } + if(!out->isRunning()){ + recordingState = RecordingState::STOPPING; _stopRecording(); - return; + finishRecordingSync(); + const bool complete = recordingState == RecordingState::COMPLETE; + recordingMutex.unlock(); + return complete; } - MixRequest* request = new MixRequest({ MixRequest::RECORD, 0, 0, 0 }); - queue.send(&request); + if(enqueueRequest({ MixRequest::RECORD, 0, 0, 0 })){ + if(recordingState == RecordingState::STARTING || + recordingState == RecordingState::RECORDING){ + recordingState = RecordingState::STOPPING; + } + recordingMutex.unlock(); + return true; + } + recordingMutex.unlock(); + return false; } void MixSystem::_startRecording(){ - if(isRecording()) return; + if(out->getOutput(1) != nullptr) return; + if(recordingState != RecordingState::STARTING && + recordingState != RecordingState::STOPPING) return; + fsOut->invalidateFile(); + + if(SD.cardType() == CARD_NONE){ + recordingError = RecordingError::SD_UNAVAILABLE; + recordingState = RecordingState::FAILED; + return; + } - if(SD.exists(recordPath)){ - SD.remove(recordPath); + if(SD.exists(recordPath) && !SD.remove(recordPath)){ + recordingError = RecordingError::SD_UNAVAILABLE; + recordingState = RecordingState::FAILED; + return; } fileOut = SD.open(recordPath, "w"); if(!fileOut){ Serial.printf("Failed opening %s for writing\n", recordPath); + recordingError = RecordingError::OPEN_FAILED; + recordingState = RecordingState::FAILED; return; } - fsOut->setFile(fileOut); - - out->addOutput(fsOut); - - if(out->isRunning()){ - fsOut->start(); + if(!fsOut->begin(fileOut)){ + recordingError = fsOut->getError(); + recordingState = RecordingState::FAILED; + fileOut.close(); + return; } + recordingApplied = true; } void MixSystem::_stopRecording(){ - if(!isRecording()) return; + if(out->getOutput(1) != nullptr){ + out->removeOutput(1); + } + if(fsOut->isRunning()){ + fsOut->stop(); + }else{ + fsOut->finish(); + } + if(!recordingApplied && + (recordingState == RecordingState::STARTING || + recordingState == RecordingState::STOPPING)){ + recordingState = RecordingState::IDLE; + return; + } + if(recordingState == RecordingState::STARTING || + recordingState == RecordingState::RECORDING) recordingState = RecordingState::STOPPING; +} + +void MixSystem::serviceRecording(){ + fsOut->service(); + if(recordingState == RecordingState::STARTING && recordingApplied){ + if(fsOut->getError() != RecordingError::NONE){ + recordingError = fsOut->getError(); + recordingState = RecordingState::FAILED; + fileOut.close(); + return; + } + if(fsOut->isReady()){ + recordingMutex.lock(); + if(recordingState == RecordingState::STARTING){ + out->addOutput(fsOut); + if(out->isRunning()) fsOut->start(); + recordingState = RecordingState::RECORDING; + } + recordingMutex.unlock(); + } + } + + if(recordingState == RecordingState::STOPPING && + recordingApplied && + fsOut->isReady() && + !fsOut->isRunning()){ + fsOut->finish(); + } - out->removeOutput(1); + if(recordingState == RecordingState::RECORDING && + fsOut->getError() != RecordingError::NONE){ + recordingState = RecordingState::STOPPING; + _stopRecording(); + } - fsOut->stop(); + if(recordingState != RecordingState::STOPPING || + !recordingApplied || + !fsOut->isFinalized()) return; fileOut.close(); + if(fsOut->getError() == RecordingError::NONE && + recordingError == RecordingError::NONE && + fsOut->isFileValid()){ + recordingState = RecordingState::COMPLETE; + }else{ + if(recordingError == RecordingError::NONE){ + recordingError = fsOut->getError() == RecordingError::NONE + ? RecordingError::FINALIZE_FAILED + : fsOut->getError(); + } + recordingState = RecordingState::FAILED; + } +} + +void MixSystem::finishRecordingSync(){ + while(recordingState == RecordingState::STOPPING){ + Sched.loop(0); + serviceRecording(); + } } bool MixSystem::isChannelPaused(uint8_t channel){ @@ -410,5 +708,8 @@ bool MixSystem::isChannelPaused(uint8_t channel){ } void MixSystem::setChannelDoneCallback(uint8_t channel, void(*callback)()) { - source[channel]->setSongDoneCallback(callback); + if(channel >= 2) return; + sourceMutex.lock(); + if(source[channel]) source[channel]->setSongDoneCallback(callback); + sourceMutex.unlock(); } diff --git a/src/AudioLib/Systems/MixSystem.h b/src/AudioLib/Systems/MixSystem.h index 6bae1e0..eba9dd6 100644 --- a/src/AudioLib/Systems/MixSystem.h +++ b/src/AudioLib/Systems/MixSystem.h @@ -13,16 +13,36 @@ #include "../EffectType.hpp" #include "../SourceAAC.h" #include +#include #include "../InfoGenerator.h" #include "../OutputWAV.h" struct MixRequest { - enum { ADD_SPEED, REMOVE_SPEED, SET_SPEED, SET_EFFECT, SET_EFFECT_INTENSITY, SET_INFO, SET_SEEK, RECORD } type; + enum { ADD_SPEED, REMOVE_SPEED, SET_SPEED, SET_EFFECT, SET_EFFECT_INTENSITY, SET_INFO, SET_SEEK, RECORD, OPEN } type; uint8_t channel; uint8_t slot; size_t value; }; +enum class RecordingState : uint8_t { + IDLE, + STARTING, + RECORDING, + STOPPING, + COMPLETE, + FAILED +}; + +struct RecordingStatus { + RecordingState state; + RecordingError error; + uint32_t bytes; + uint32_t durationMs; + uint32_t droppedBytes; + uint8_t finalizeQueueRetries; + bool fileValid; +}; + class MixSystem { public: MixSystem(); @@ -32,6 +52,7 @@ class MixSystem { constexpr static const char* const recordPath = "/.Jay-D_Recording.wav"; bool open(uint8_t channel, const fs::File& file); + bool openChannel(uint8_t channel, const fs::File& file); Task audioTask; static void audioThread(Task* task); @@ -42,6 +63,9 @@ class MixSystem { uint16_t getDuration(uint8_t channel); uint16_t getElapsed(uint8_t channel); + bool hasChannel(uint8_t channel); + uint8_t getVolume(uint8_t channel); + uint8_t getMix(); void setVolume(uint8_t channel, uint8_t volume); void setMix(uint8_t ratio); @@ -61,27 +85,40 @@ class MixSystem { void seekChannel(uint8_t channel, uint16_t time); - void startRecording(); - void stopRecording(); + // Return values report whether the request was accepted. Poll status for + // asynchronous write/finalization failures. + bool startRecording(); + bool stopRecording(); bool isRecording(); + RecordingStatus getRecordingStatus() const; void setChannelDoneCallback(uint8_t channel, void(*callback)()); private: + static constexpr uint8_t requestCapacity = 6; bool running = false; Queue queue; + Mutex queueMutex; + Mutex sourceMutex; + Mutex recordingMutex; + MixRequest requests[requestCapacity] = {}; + bool requestUsed[requestCapacity] = {}; - fs::File file[2]; fs::File fileOut; SourceAAC* source[2] = { nullptr }; + SourceAAC* retiredSource[2] = { nullptr }; + uint8_t volume[2] = { 255, 255 }; EffectProcessor* effector[2]; Mixer* mixer; OutputI2S* i2s; OutputWAV* fsOut; OutputSplitter* out; + volatile RecordingState recordingState = RecordingState::IDLE; + volatile RecordingError recordingError = RecordingError::NONE; + volatile bool recordingApplied = false; SpeedModifier* speed[2] = { nullptr }; @@ -94,12 +131,21 @@ class MixSystem { void _seekChannel(uint8_t channel, uint16_t time); void _startRecording(); void _stopRecording(); + void serviceRecording(); + void finishRecordingSync(); + void _openChannel(uint8_t channel, SourceAAC* source); + bool replaceSource(uint8_t channel, SourceAAC* source); + int8_t reserveRequest(const MixRequest& request); + bool sendRequest(uint8_t index); + bool enqueueRequest(const MixRequest& request); + void releaseRequest(uint8_t index); + void clearRequests(); + void cleanupRetiredSources(); static Effect* (* getEffect[EffectType::COUNT])(); uint16_t seek[2]; int seekPending[2] = { 0 }; - }; #endif //JAYD_LIBRARY_MIXSYSTEM_H diff --git a/src/AudioLib/WavHeader.h b/src/AudioLib/WavHeader.h new file mode 100644 index 0000000..81dbc9d --- /dev/null +++ b/src/AudioLib/WavHeader.h @@ -0,0 +1,48 @@ +#ifndef JAYD_LIBRARY_WAVHEADER_H +#define JAYD_LIBRARY_WAVHEADER_H + +#include +#include + +struct WavHeader { + char RIFF[4]; + uint32_t chunkSize; + char WAVE[4]; + char fmt[4]; + uint32_t fmtSize; + uint16_t audioFormat; + uint16_t numChannels; + uint32_t sampleRate; + uint32_t byteRate; + uint16_t blockAlign; + uint16_t bitsPerSample; + char data[4]; + uint32_t dataSize; +}; + +static_assert(sizeof(WavHeader) == 44, "PCM WAV header must be 44 bytes"); + +inline WavHeader makeWavHeader( + uint32_t dataSize, + uint16_t channels, + uint32_t sampleRate, + uint16_t bytesPerSample +){ + WavHeader header = {}; + memcpy(header.RIFF, "RIFF", 4); + header.chunkSize = dataSize + 36; + memcpy(header.WAVE, "WAVE", 4); + memcpy(header.fmt, "fmt ", 4); + header.fmtSize = 16; + header.audioFormat = 1; + header.numChannels = channels; + header.sampleRate = sampleRate; + header.byteRate = sampleRate * channels * bytesPerSample; + header.blockAlign = channels * bytesPerSample; + header.bitsPerSample = bytesPerSample * 8; + memcpy(header.data, "data", 4); + header.dataSize = dataSize; + return header; +} + +#endif //JAYD_LIBRARY_WAVHEADER_H diff --git a/src/Services/SDScheduler.cpp b/src/Services/SDScheduler.cpp index 8971dc4..6b92b8a 100644 --- a/src/Services/SDScheduler.cpp +++ b/src/Services/SDScheduler.cpp @@ -3,23 +3,35 @@ SDScheduler Sched; -SDScheduler::SDScheduler() :jobs(8, sizeof(SDJob*)){ +SDScheduler::SDScheduler() : jobs(xQueueCreate(jobCapacity, sizeof(SDJob*))){ +} +SDScheduler::~SDScheduler(){ + vQueueDelete(jobs); } void SDScheduler::addJob(SDJob *job){ - jobs.send(&job); + if(job == nullptr) return; + if(xQueueSend(jobs, &job, portMAX_DELAY) == pdTRUE) return; + delete job; +} + +bool SDScheduler::tryAddJob(SDJob *job){ + if(job == nullptr) return false; + if(xQueueSend(jobs, &job, 0) == pdTRUE) return true; + delete job; + return false; } void SDScheduler::loop(uint micros) { - if (jobs.count() == 0) { + if (uxQueueMessagesWaiting(jobs) == 0) { return; } SDJob* request = nullptr; - while(jobs.count() > 0){ - if(!jobs.receive(&request)){ + while(uxQueueMessagesWaiting(jobs) > 0){ + if(xQueueReceive(jobs, &request, 0) != pdTRUE){ Serial.println("Receive error"); return; } @@ -42,7 +54,7 @@ void SDScheduler::doJob(SDJob* job){ SDResult* result = new SDResult(); result->size = job->size * success; result->buffer = job->buffer; - result->error = 0; + result->error = success ? 0 : 1; *job->result = result; } @@ -56,11 +68,10 @@ void SDScheduler::doJob(SDJob* job){ if(job->result != nullptr){ SDResult* result = new SDResult(); - result->error = 0; + result->error = job->type == SDJob::SD_WRITE && size != job->size; result->buffer = job->buffer; result->size = size; *job->result = result; } } - diff --git a/src/Services/SDScheduler.h b/src/Services/SDScheduler.h index 431b1d3..3e034df 100644 --- a/src/Services/SDScheduler.h +++ b/src/Services/SDScheduler.h @@ -2,9 +2,8 @@ #define JAYD_LIBRARY_SDSCHEDULER_H #include -#include #include -#include +#include struct SDResult { uint8_t error; @@ -13,7 +12,7 @@ struct SDResult { }; struct SDJob { - enum { SD_WRITE, SD_READ, SD_SEEK } type; + enum Type { SD_WRITE, SD_READ, SD_SEEK } type; fs::File file; size_t size; uint8_t* buffer; @@ -23,11 +22,14 @@ struct SDJob { class SDScheduler : public LoopListener { public: SDScheduler(); + ~SDScheduler(); void addJob(SDJob *job); + bool tryAddJob(SDJob *job); void loop(uint micros) override; private: - Queue jobs; + static constexpr uint8_t jobCapacity = 8; + QueueHandle_t jobs; void doJob(SDJob* job); diff --git a/tests/wav_header_selfcheck.cpp b/tests/wav_header_selfcheck.cpp new file mode 100644 index 0000000..af00f39 --- /dev/null +++ b/tests/wav_header_selfcheck.cpp @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include +#include "../src/AudioLib/RecordingFinalize.h" +#include "../src/AudioLib/WavHeader.h" + +int main(){ + const WavHeader empty = makeWavHeader(0, 1, 24000, 2); + assert(sizeof(empty) == 44); + assert(memcmp(empty.RIFF, "RIFF", 4) == 0); + assert(memcmp(empty.WAVE, "WAVE", 4) == 0); + assert(memcmp(empty.fmt, "fmt ", 4) == 0); + assert(memcmp(empty.data, "data", 4) == 0); + assert(empty.chunkSize == 36); + assert(empty.dataSize == 0); + assert(empty.byteRate == 48000); + assert(empty.blockAlign == 2); + assert(empty.bitsPerSample == 16); + + const WavHeader finalized = makeWavHeader(4096, 2, 44100, 2); + unsigned char fileHeader[sizeof(WavHeader)] = {}; + memcpy(fileHeader, &empty, sizeof(empty)); + memcpy(fileHeader, &finalized, sizeof(finalized)); + WavHeader rewritten = {}; + memcpy(&rewritten, fileHeader, sizeof(rewritten)); + assert(rewritten.chunkSize == 4132); + assert(rewritten.dataSize == 4096); + assert(rewritten.byteRate == 176400); + assert(rewritten.blockAlign == 4); + + uint8_t queueFailures = 0; + assert(recordFinalizeEnqueue(false, queueFailures, 3) == FinalizeEnqueueResult::RETRY); + assert(recordFinalizeEnqueue(false, queueFailures, 3) == FinalizeEnqueueResult::RETRY); + assert(recordFinalizeEnqueue(true, queueFailures, 3) == FinalizeEnqueueResult::QUEUED); + assert(queueFailures == 2); + assert(recordFinalizeEnqueue(false, queueFailures, 3) == FinalizeEnqueueResult::EXHAUSTED); + + std::ifstream sourceFile("src/AudioLib/Systems/MixSystem.cpp"); + assert(sourceFile.good()); + const std::string source( + (std::istreambuf_iterator(sourceFile)), + std::istreambuf_iterator() + ); + const size_t startBegin = source.find("bool MixSystem::startRecording()"); + const size_t startEnd = source.find("bool MixSystem::stopRecording()", startBegin); + assert(startBegin != std::string::npos); + assert(startEnd != std::string::npos); + const std::string start = source.substr(startBegin, startEnd - startBegin); + assert(start.find("enqueueRequest") != std::string::npos); + assert(start.find("fsOut->") == std::string::npos); + assert(start.find("SD.") == std::string::npos); + assert(start.find("fileOut") == std::string::npos); + + const size_t appliedBegin = source.find("void MixSystem::_startRecording()"); + const size_t appliedEnd = source.find("void MixSystem::_stopRecording()", appliedBegin); + assert(appliedBegin != std::string::npos); + assert(appliedEnd != std::string::npos); + const std::string applied = source.substr(appliedBegin, appliedEnd - appliedBegin); + assert(applied.find("SD.open") != std::string::npos); + assert(applied.find("fsOut->begin") != std::string::npos); + assert(applied.find("fsOut->invalidateFile") != std::string::npos); + assert(applied.find("recordingApplied = true") != std::string::npos); + assert(applied.find("out->addOutput") == std::string::npos); + assert(applied.find("while(") == std::string::npos); + assert(applied.find("Sched.loop") == std::string::npos); + assert(applied.find("delayMicroseconds") == std::string::npos); + + std::ifstream wavFile("src/AudioLib/OutputWAV.cpp"); + assert(wavFile.good()); + const std::string wavSource( + (std::istreambuf_iterator(wavFile)), + std::istreambuf_iterator() + ); + const size_t beginBegin = wavSource.find("bool OutputWAV::begin("); + const size_t beginEnd = wavSource.find("void OutputWAV::finish()", beginBegin); + assert(beginBegin != std::string::npos); + assert(beginEnd != std::string::npos); + const std::string begin = wavSource.substr(beginBegin, beginEnd - beginBegin); + assert(begin.find("while(") == std::string::npos); + assert(begin.find("Sched.loop") == std::string::npos); + const size_t wavServiceBegin = wavSource.find("void OutputWAV::service()"); + const size_t wavServiceEnd = wavSource.find("bool OutputWAV::isFinalized()", wavServiceBegin); + assert(wavServiceBegin != std::string::npos); + assert(wavServiceEnd != std::string::npos); + const std::string wavService = + wavSource.substr(wavServiceBegin, wavServiceEnd - wavServiceBegin); + assert(wavService.find("while(") == std::string::npos); + assert(wavService.find("Sched.loop") == std::string::npos); + assert(wavSource.find("FinalizeStage::INIT_SEEK_QUEUE") != std::string::npos); + assert(wavSource.find("FinalizeStage::INIT_HEADER_QUEUE") != std::string::npos); + assert(wavSource.find("if(finalizeResult == nullptr) return;") != std::string::npos); + + const size_t serviceBegin = source.find("void MixSystem::serviceRecording()"); + const size_t serviceEnd = source.find("void MixSystem::finishRecordingSync()", serviceBegin); + assert(serviceBegin != std::string::npos); + assert(serviceEnd != std::string::npos); + const std::string service = source.substr(serviceBegin, serviceEnd - serviceBegin); + assert(service.find("fsOut->isReady()") != std::string::npos); + assert(service.find("recordingMutex.lock()") != std::string::npos); + assert(service.find("out->addOutput") != std::string::npos); + + const size_t stopBegin = source.find("bool MixSystem::stopRecording()"); + const size_t stopEnd = source.find("void MixSystem::_startRecording()", stopBegin); + assert(stopBegin != std::string::npos); + assert(stopEnd != std::string::npos); + const std::string stop = source.substr(stopBegin, stopEnd - stopBegin); + assert(stop.find("recordingMutex.lock()") != std::string::npos); + assert(stop.find("recordingState = previousState") == std::string::npos); + + std::ifstream schedulerFile("src/Services/SDScheduler.cpp"); + assert(schedulerFile.good()); + const std::string scheduler( + (std::istreambuf_iterator(schedulerFile)), + std::istreambuf_iterator() + ); + const size_t tryAddBegin = scheduler.find("bool SDScheduler::tryAddJob("); + const size_t tryAddEnd = scheduler.find("void SDScheduler::loop(", tryAddBegin); + assert(tryAddBegin != std::string::npos); + assert(tryAddEnd != std::string::npos); + const std::string tryAdd = scheduler.substr(tryAddBegin, tryAddEnd - tryAddBegin); + assert(tryAdd.find("xQueueSend(jobs, &job, 0)") != std::string::npos); + assert(tryAdd.find("portMAX_DELAY") == std::string::npos); + assert(wavSource.find("Sched.tryAddJob(") != std::string::npos); + assert(wavSource.find("Sched.addJob(") == std::string::npos); + return 0; +}