WIP on our own pipeline
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
|
||||
FatfsAudioReader
|
||||
- input if a queue of filenames.
|
||||
- output is a cbor stream
|
||||
- 1 header, like "this is a new file! this is the file type!
|
||||
- followed by length-prefixed chunks of bytes
|
||||
- runs in a task, which prompts it to read/write one chunk, then returns.
|
||||
- task watches for kill signal, owns storage, etc.
|
||||
|
||||
AudioDecoder
|
||||
- input is the chunked bytes above.
|
||||
- output is also a cbor stream
|
||||
- 1 header, which is like a reconfiguration packet thing.
|
||||
- "data that follows is this depth, this sample rate"
|
||||
- also indicates whether the configuration is 'sudden' for soft muting?
|
||||
- then length-prefixed chunks of bytes
|
||||
|
||||
AudioOutput
|
||||
- input is the output of the decoder
|
||||
- outputs via writing to i2s_write, which copies data to a dma buffer
|
||||
- therefore, safe for us to consume any kind of reconfiguration here.
|
||||
- only issue is that we will need to wait for the dma buffers to drain before
|
||||
we can reconfigure the driver. (i2s_zero_dma_buffer)
|
||||
- this is important for i2s speed; we should avoid extra copy steps for the raw
|
||||
- pcm stream
|
||||
- input therefore needs to be two channels: one configuration channel, one bytes
|
||||
channel
|
||||
|
||||
|
||||
How do things like seeking, and progress work?
|
||||
- Reader knows where we are in terms of file size and position
|
||||
- Decoder knows sample rate, frames, etc. for knowing how that maps into
|
||||
- the time progress
|
||||
- Output knows where we are as well in a sense, but only in terms of the PCM
|
||||
output. this doesn't correspond to anything very well.
|
||||
|
||||
So, to seek:
|
||||
- come up with your position. this is likely "where we are plus 10", or a
|
||||
specific timecode. the decoder has what we need for the byte position of this
|
||||
- tell the reader "hey we need to be in this file at this byte position
|
||||
- reader clears its own output buffer (since it's been doing readahead) and
|
||||
starts again at the given location
|
||||
For current position, the decoder will need to track where in the file it's up
|
||||
to.
|
||||
|
||||
HEADERS + DATA:
|
||||
- cbor seems sensible for headers. allocate a little working buffer, encode the
|
||||
data, then send it out on the ringbuffer.
|
||||
- the data itself is harder, since tinycbor doesn't support writing chunked indefinite
|
||||
length stuff. this is a problem bc we need to give cbor the buffer up front, but
|
||||
we don't know exactly how long things will be, so it ends up being slightly awkward
|
||||
and inefficient.
|
||||
- we could also just like... write the struct i guess? that might be okay.
|
||||
- gives us a format like <TYPE ENUM> <LENGTH> <DATA>
|
||||
- could be smart with the type, use like a 32 bit int, and encode the length
|
||||
- in there?
|
||||
- then from the reader's perspective, it's:
|
||||
- read 4 bytes, work out what's next
|
||||
- read the next X bytes
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "audio_task.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/stream_buffer.h"
|
||||
|
||||
#include "audio_element.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
static const TickType_t kCommandWaitTicks = 1;
|
||||
|
||||
void audio_task(void* args) {
|
||||
AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args);
|
||||
std::shared_ptr<IAudioElement> element = real_args->element;
|
||||
delete real_args;
|
||||
|
||||
QueueHandle_t commands = element->InputCommandQueue();
|
||||
StreamBufferHandle_t stream = element->InputBuffer();
|
||||
|
||||
// TODO: think about overflow.
|
||||
uint8_t current_sequence_number;
|
||||
uint8_t* frame_buffer =
|
||||
(uint8_t*)heap_caps_malloc(kFrameSize, MALLOC_CAP_SPIRAM);
|
||||
|
||||
while (1) {
|
||||
IAudioElement::Command command;
|
||||
if (!xQueueReceive(commands, &command, kCommandWaitTicks)) {
|
||||
element->ProcessIdle();
|
||||
continue;
|
||||
};
|
||||
|
||||
if (command.type == IAudioElement::SEQUENCE_NUMBER) {
|
||||
if (command.sequence_number > current_sequence_number) {
|
||||
current_sequence_number = command.sequence_number;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command.type == IAudioElement::READ) {
|
||||
assert(command.read_size <= kFrameSize);
|
||||
assert(stream != NULL);
|
||||
xStreamBufferReceive(stream, &frame_buffer, command.read_size, 0);
|
||||
|
||||
if (command.sequence_number == current_sequence_number) {
|
||||
element->ProcessData(frame_buffer, command.read_size);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command.type == IAudioElement::ELEMENT) {
|
||||
assert(command.data != NULL);
|
||||
if (command.sequence_number == current_sequence_number) {
|
||||
element->ProcessElementCommand(command.data);
|
||||
} else {
|
||||
element->SkipElementCommand(command.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (command.type == IAudioElement::QUIT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
element = nullptr;
|
||||
free(frame_buffer);
|
||||
|
||||
xTaskDelete(NULL);
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "fatfs_audio_input.hpp"
|
||||
#include <memory>
|
||||
|
||||
#include "esp-adf/components/input_key_service/include/input_key_service.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
#include "audio_element.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
static const size_t kQueueItems = 0;
|
||||
static constexpr size_t kQueueItemSize = sizeof(IAudioElement::Command);
|
||||
static constexpr size_t kQueueSize = kQueueItems * kQueueItemSize;
|
||||
|
||||
static const size_t kOutputBufferSize = 1024;
|
||||
|
||||
FatfsAudioInput::FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage)
|
||||
: IAudioElement(), storage_(storage) {
|
||||
input_queue_memory_ = heap_caps_malloc(kQueueSize, MALLOC_CAP_SPIRAM);
|
||||
input_queue_ = xQueueCreateStatic(
|
||||
kQueueItems, kQueueItemSize, input_queue_memory_, &input_queue_metadata_);
|
||||
|
||||
output_queue_memory_ = heap_caps_malloc(kQueueSize, MALLOC_CAP_SPIRAM);
|
||||
output_queue_ =
|
||||
xQueueCreateStatic(kQueueItems, kQueueItemSize, output_queue_memory_,
|
||||
&output_queue_metadata_);
|
||||
|
||||
output_buffer_memory_ =
|
||||
heap_caps_malloc(kOutputBufferSize, MALLOC_CAP_SPIRAM);
|
||||
output_buffer_ =
|
||||
xStreamBufferCreateStatic(kOutputBufferSize - 1, 1, output_buffer_memory_,
|
||||
&output_buffer_metadata_);
|
||||
}
|
||||
|
||||
FatfsAudioInput::~FatfsAudioInput() {
|
||||
vStreamBufferDelete(output_buffer_);
|
||||
free(output_buffer_memory_);
|
||||
vQueueDelete(output_queue_);
|
||||
free(output_queue_memory_);
|
||||
vQueueDelete(input_queue_);
|
||||
free(input_queue_memory_);
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::InputCommandQueue() -> QueueHandle_t {
|
||||
return input_queue_;
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::OutputCommandQueue() -> QueueHandle_t {
|
||||
return output_queue_;
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::InputBuffer() -> StreamBufferHandle_t {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::OutputBuffer() -> StreamBufferHandle_t {
|
||||
return output_buffer_;
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::ProcessElementCommand(void* command) -> void {
|
||||
InputCommand *real = std::reinterpret_pointer_cast<input_key_service_add_key*>(command);
|
||||
|
||||
// TODO.
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::SkipElementCommand(void* command) -> void {
|
||||
InputCommand *real = std::reinterpret_pointer_cast<input_key_service_add_key*>(command);
|
||||
delete real;
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::ProcessData(uint8_t* data, uint16_t length) -> void {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
auto FatfsAudioInput::ProcessIdle() -> void {
|
||||
// TODO.
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
namespace drivers {
|
||||
|
||||
class IAudioBackend {
|
||||
public:
|
||||
virtual ~IAudioBackend() {}
|
||||
|
||||
enum SampleRate {};
|
||||
enum BitDepth {};
|
||||
|
||||
virtual auto Configure(SampleRate sample_rate, BitDepth bit_depth)
|
||||
-> bool = 0;
|
||||
virtual auto WritePcmData(uint8_t* data, size_t length) -> bool = 0;
|
||||
|
||||
virtual auto SetVolume(uint8_t percent) -> void = 0;
|
||||
};
|
||||
|
||||
} // namespace drivers
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include "ff.h"
|
||||
|
||||
namespace audio {
|
||||
|
||||
enum SampleRate {};
|
||||
enum BitDepth {};
|
||||
|
||||
struct PcmStreamHeader {
|
||||
SampleRate sample_rate;
|
||||
BitDepth bit_depth;
|
||||
bool configure_now;
|
||||
};
|
||||
|
||||
class AudioDecoder {
|
||||
public:
|
||||
AudioDecoder();
|
||||
~AudioDecoder();
|
||||
|
||||
auto SetSource(RingbufHandle_t& source) -> void;
|
||||
|
||||
enum Status {};
|
||||
auto ProcessChunk() -> Status;
|
||||
|
||||
auto GetOutputStream() const -> RingbufHandle_t;
|
||||
|
||||
private:
|
||||
RingbufHandle_t input_;
|
||||
RingbufHandle_t output_;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace audio {
|
||||
|
||||
extern const std::size_t kMaxFrameSize;
|
||||
|
||||
class IAudioElement {
|
||||
public:
|
||||
virtual ~IAudioElement();
|
||||
|
||||
enum CommandType {
|
||||
/*
|
||||
* Sets the sequence number of the most recent byte stream. Any commands
|
||||
* received that have a lower sequence number than this will be discarded.
|
||||
*/
|
||||
SEQUENCE_NUMBER,
|
||||
/*
|
||||
* Instructs this element to read a specific number of bytes from its
|
||||
* input buffer.
|
||||
*/
|
||||
READ_FRAME,
|
||||
/*
|
||||
* Represents an element-specific command. This handling of this is
|
||||
* delegated to element implementations.
|
||||
*/
|
||||
ELEMENT,
|
||||
/* Instructs this element to shut down. */
|
||||
QUIT,
|
||||
};
|
||||
|
||||
struct Command {
|
||||
CommandType type;
|
||||
uint8_t sequence_number;
|
||||
union {
|
||||
void* data;
|
||||
std::size_t frame_size;
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns a queue that should be used for all communication with this
|
||||
* element.
|
||||
*/
|
||||
virtual auto InputCommandQueue() -> QueueHandle_t = 0;
|
||||
|
||||
/*
|
||||
* Returns a buffer that will be used to stream input bytes to this element.
|
||||
* This may be NULL, if this element represents a source, e.g. a FATFS
|
||||
* reader.
|
||||
*/
|
||||
virtual auto InputBuffer() -> StreamBufferHandle_t = 0;
|
||||
|
||||
/*
|
||||
* Called when an element-specific command has been received.
|
||||
*/
|
||||
virtual auto ProcessElementCommand(void* command) -> void = 0;
|
||||
|
||||
virtual auto SkipElementCommand(void* command) -> void = 0;
|
||||
|
||||
/*
|
||||
* Called with the result of a read bytes command.
|
||||
*/
|
||||
virtual auto ProcessData(uint8_t* data, uint16_t length) -> void = 0;
|
||||
|
||||
/*
|
||||
* Called periodically when there are no pending commands.
|
||||
*/
|
||||
virtual auto ProcessIdle() -> void = 0;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio_element.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
struct AudioTaskArgs {
|
||||
std::shared_ptr<IAudioElement>& element;
|
||||
};
|
||||
|
||||
void audio_task(void* args);
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/stream_buffer.h"
|
||||
|
||||
#include "audio_element.hpp"
|
||||
#include "storage.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
class FatfsAudioInput : public IAudioElement {
|
||||
public:
|
||||
struct InputCommand {
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
struct OutputCommand {
|
||||
// TODO: does this actually need any special output?
|
||||
};
|
||||
|
||||
FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage);
|
||||
~FatfsAudioInput();
|
||||
|
||||
auto OutputCommandQueue() -> QueueHandle_t;
|
||||
auto OutputBuffer() -> StreamBufferHandle_t;
|
||||
|
||||
private:
|
||||
std::shared_ptr<drivers::SdStorage> storage_;
|
||||
|
||||
uint8_t current_sequence = 0;
|
||||
|
||||
uint8_t* input_queue_memory_;
|
||||
StaticQueue_t input_queue_metadata_;
|
||||
QueueHandle_t input_queue_;
|
||||
|
||||
uint8_t* output_queue_memory_;
|
||||
StaticQueue_t output_queue_metadata_;
|
||||
QueueHandle_t output_queue_;
|
||||
|
||||
uint8_t* output_buffer_memory_;
|
||||
StaticStreamBuffer_t output_buffer_metadata_;
|
||||
StreamBufferHandle_t output_buffer_;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
@@ -0,0 +1,3 @@
|
||||
# Software Codecs
|
||||
|
||||
This component contains a collection of software decoders for various
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace codecs {
|
||||
|
||||
class IAudioDecoder {
|
||||
public:
|
||||
virtual ~IAudioDecoder() {}
|
||||
|
||||
virtual auto ProcessData(
|
||||
uint8_t *input,
|
||||
size_t input_len,
|
||||
uint8_t *output) -> size_t = 0;
|
||||
};
|
||||
|
||||
} // namespace codecs
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
namespace drivers {
|
||||
|
||||
class FatfsAudioInput {
|
||||
public:
|
||||
FatfsAudioInput(std::shared_ptr<SdStorage> storage);
|
||||
~FatfsAudioInput();
|
||||
|
||||
enum Status {
|
||||
/*
|
||||
* Successfully read data into the output buffer, and there is still
|
||||
* data remaining in the file.
|
||||
*/
|
||||
OKAY,
|
||||
|
||||
/*
|
||||
* The ringbuffer was full. No data was read.
|
||||
*/
|
||||
RINGBUF_FULL,
|
||||
|
||||
/*
|
||||
* Some data may have been read into the output buffer, but the file is
|
||||
* now empty.
|
||||
*/
|
||||
FILE_EMPTY,
|
||||
};
|
||||
auto Process() -> Status;
|
||||
|
||||
auto GetOutputBuffer() -> RingbufHandle_t;
|
||||
|
||||
private:
|
||||
std::shared_ptr<SdStorage> storage_;
|
||||
RingbufHandle_t output_;
|
||||
|
||||
std::string path_;
|
||||
|
||||
};
|
||||
|
||||
} // namespace drivers
|
||||
Reference in New Issue
Block a user