parent
1d340db871
commit
60169cdd91
@ -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
|
Loading…
Reference in new issue