Merge branch 'main' into leveldb

custom
jacqueline 2 years ago
commit 7972bd4567
  1. 2
      CMakeLists.txt
  2. 2
      src/audio/CMakeLists.txt
  3. 139
      src/audio/audio_decoder.cpp
  4. 56
      src/audio/audio_element.cpp
  5. 61
      src/audio/audio_playback.cpp
  6. 241
      src/audio/audio_task.cpp
  7. 78
      src/audio/fatfs_audio_input.cpp
  8. 102
      src/audio/i2s_audio_output.cpp
  9. 23
      src/audio/include/audio_decoder.hpp
  10. 62
      src/audio/include/audio_element.hpp
  11. 18
      src/audio/include/audio_playback.hpp
  12. 44
      src/audio/include/audio_sink.hpp
  13. 25
      src/audio/include/audio_task.hpp
  14. 18
      src/audio/include/fatfs_audio_input.hpp
  15. 27
      src/audio/include/i2s_audio_output.hpp
  16. 43
      src/audio/include/pipeline.hpp
  17. 91
      src/audio/include/stream_info.hpp
  18. 57
      src/audio/pipeline.cpp
  19. 70
      src/audio/stream_info.cpp
  20. 2
      src/codecs/codec.cpp
  21. 7
      src/codecs/include/codec.hpp
  22. 4
      src/codecs/include/mad.hpp
  23. 31
      src/codecs/mad.cpp
  24. 4
      src/drivers/CMakeLists.txt
  25. 4
      src/drivers/battery.cpp
  26. 321
      src/drivers/dac.cpp
  27. 43
      src/drivers/display.cpp
  28. 43
      src/drivers/driver_cache.cpp
  29. 11
      src/drivers/i2c.cpp
  30. 127
      src/drivers/include/dac.hpp
  31. 3
      src/drivers/include/display.hpp
  32. 54
      src/drivers/include/driver_cache.hpp
  33. 76
      src/drivers/include/gpio_expander.hpp
  34. 2
      src/drivers/include/i2c.hpp
  35. 7
      src/drivers/include/storage.hpp
  36. 49
      src/drivers/include/touchwheel.hpp
  37. 17
      src/drivers/storage.cpp
  38. 95
      src/drivers/touchwheel.cpp
  39. 2
      src/main/CMakeLists.txt
  40. 117
      src/main/main.cpp
  41. 2
      src/memory/CMakeLists.txt
  42. 83
      src/memory/include/himem.hpp
  43. 3
      src/tasks/tasks.cpp
  44. 3
      src/tasks/tasks.hpp
  45. 5
      src/ui/CMakeLists.txt
  46. 17
      src/ui/include/lvgl_task.hpp
  47. 108
      src/ui/lvgl_task.cpp

@ -10,4 +10,4 @@ idf_build_set_property(COMPILE_OPTIONS "-DTCB_SPAN_NO_CONTRACT_CHECKING" APPEND)
# Include all app components. # Include all app components.
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/src") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/src")
project(gay-ipod-fw) project(tangara)

@ -1,7 +1,7 @@
idf_component_register( idf_component_register(
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
"stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp"
"audio_playback.cpp" "stream_event.cpp" "audio_element.cpp" "audio_playback.cpp" "stream_event.cpp" "pipeline.cpp" "stream_info.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory") REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory")

@ -2,13 +2,17 @@
#include <string.h> #include <string.h>
#include <algorithm>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <variant>
#include "cbor/tinycbor/src/cborinternal_p.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/message_buffer.h" #include "freertos/message_buffer.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
@ -21,127 +25,112 @@ namespace audio {
static const char* kTag = "DEC"; static const char* kTag = "DEC";
static const std::size_t kChunkSize = 1024;
static const std::size_t kReadahead = 8;
AudioDecoder::AudioDecoder() AudioDecoder::AudioDecoder()
: IAudioElement(), : IAudioElement(),
arena_(kChunkSize, kReadahead, MALLOC_CAP_SPIRAM), current_codec_(),
stream_info_({}), current_input_format_(),
has_samples_to_send_(false), current_output_format_(),
needs_more_input_(true) {} has_samples_to_send_(false) {}
AudioDecoder::~AudioDecoder() {} AudioDecoder::~AudioDecoder() {}
auto AudioDecoder::HasUnprocessedInput() -> bool { auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
return !needs_more_input_ || has_samples_to_send_; if (!std::holds_alternative<StreamInfo::Encoded>(info.format)) {
} return false;
auto AudioDecoder::IsOverBuffered() -> bool {
return arena_.BlocksFree() == 0;
}
auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> void {
stream_info_ = info;
if (info.chunk_size) {
chunk_reader_.emplace(info.chunk_size.value());
} else {
ESP_LOGE(kTag, "no chunk size given");
return;
} }
ESP_LOGI(kTag, "got new stream");
const auto& encoded = std::get<StreamInfo::Encoded>(info.format);
// Reuse the existing codec if we can. This will help with gapless playback, // Reuse the existing codec if we can. This will help with gapless playback,
// since we can potentially just continue to decode as we were before, // since we can potentially just continue to decode as we were before,
// without any setup overhead. // without any setup overhead.
if (current_codec_ != nullptr && if (current_codec_ != nullptr &&
current_codec_->CanHandleFile(info.path.value_or(""))) { current_codec_->CanHandleType(encoded.type)) {
current_codec_->ResetForNewStream(); current_codec_->ResetForNewStream();
return; ESP_LOGI(kTag, "reusing existing decoder");
return true;
} }
auto result = codecs::CreateCodecForFile(info.path.value_or("")); // TODO: use audio type from stream
auto result = codecs::CreateCodecForType(encoded.type);
if (result.has_value()) { if (result.has_value()) {
ESP_LOGI(kTag, "creating new decoder");
current_codec_ = std::move(result.value()); current_codec_ = std::move(result.value());
} else { } else {
ESP_LOGE(kTag, "no codec for this file"); ESP_LOGE(kTag, "no codec for this file");
return; return false;
} }
stream_info_ = info; return true;
has_sent_stream_info_ = false;
} }
auto AudioDecoder::ProcessChunk(const cpp::span<std::byte>& chunk) -> void { auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
if (current_codec_ == nullptr || !chunk_reader_) { OutputStream* output) -> void {
// Should never happen, but fail explicitly anyway. auto input = inputs.begin();
ESP_LOGW(kTag, "received chunk without chunk size or codec"); const StreamInfo& info = input->info();
if (std::holds_alternative<std::monostate>(info.format) ||
info.bytes_in_stream == 0) {
// TODO(jacqueline): should we clear the stream format?
// output->prepare({});
return; return;
} }
ESP_LOGI(kTag, "received new chunk (size %u)", chunk.size()); if (!current_input_format_ || *current_input_format_ != info.format) {
current_codec_->SetInput(chunk_reader_->HandleNewData(chunk)); // The input stream has changed! Immediately throw everything away and
needs_more_input_ = false; // start from scratch.
} current_input_format_ = info.format;
auto AudioDecoder::ProcessEndOfStream() -> void {
has_samples_to_send_ = false; has_samples_to_send_ = false;
needs_more_input_ = true;
current_codec_.reset();
SendOrBufferEvent(std::unique_ptr<StreamEvent>( ProcessStreamInfo(info);
StreamEvent::CreateEndOfStream(input_events_))); }
}
current_codec_->SetInput(input->data());
auto AudioDecoder::Process() -> void { while (true) {
if (has_samples_to_send_) { if (has_samples_to_send_) {
// Writing samples is relatively quick (it's just a bunch of memcopy's), so if (!current_output_format_) {
// do them all at once.
while (has_samples_to_send_ && !IsOverBuffered()) {
if (!has_sent_stream_info_) {
has_sent_stream_info_ = true;
auto format = current_codec_->GetOutputFormat(); auto format = current_codec_->GetOutputFormat();
stream_info_->bits_per_sample = format.bits_per_sample; current_output_format_ = StreamInfo::Pcm{
stream_info_->sample_rate = format.sample_rate_hz; .channels = format.num_channels,
stream_info_->channels = format.num_channels; .bits_per_sample = format.bits_per_sample,
stream_info_->chunk_size = kChunkSize; .sample_rate = format.sample_rate_hz,
};
auto event =
StreamEvent::CreateStreamInfo(input_events_, *stream_info_);
SendOrBufferEvent(std::unique_ptr<StreamEvent>(event));
} }
auto block = arena_.Acquire(); if (!output->prepare(*current_output_format_)) {
if (!block) { break;
return;
} }
auto write_res = auto write_res = current_codec_->WriteOutputSamples(output->data());
current_codec_->WriteOutputSamples({block->start, block->size}); output->add(write_res.first);
block->used_size = write_res.first;
has_samples_to_send_ = !write_res.second; has_samples_to_send_ = !write_res.second;
auto chunk = std::unique_ptr<StreamEvent>( if (has_samples_to_send_) {
StreamEvent::CreateArenaChunk(input_events_, *block)); // We weren't able to fit all the generated samples into the output
if (!SendOrBufferEvent(std::move(chunk))) { // buffer. Stop trying; we'll finish up during the next pass.
return; break;
}
} }
// We will process the next frame during the next call to this method.
} }
if (!needs_more_input_) {
auto res = current_codec_->ProcessNextFrame(); auto res = current_codec_->ProcessNextFrame();
if (res.has_error()) { if (res.has_error()) {
// TODO(jacqueline): Handle errors. // TODO(jacqueline): Handle errors.
return; return;
} }
needs_more_input_ = res.value();
has_samples_to_send_ = true;
if (needs_more_input_) { if (res.value()) {
chunk_reader_->HandleBytesUsed(current_codec_->GetInputPosition()); // We're out of useable data in this buffer. Finish immediately; there's
// nothing to send.
input->mark_incomplete();
break;
} else {
has_samples_to_send_ = true;
}
} }
std::size_t pos = current_codec_->GetInputPosition();
if (pos > 0) {
input->consume(pos - 1);
} }
} }

@ -1,56 +0,0 @@
#include "audio_element.hpp"
#include <memory>
namespace audio {
IAudioElement::IAudioElement()
: input_events_(xQueueCreate(kEventQueueSize, sizeof(void*))),
output_events_(nullptr),
buffered_output_() {}
IAudioElement::~IAudioElement() {
// Ensure we don't leak any memory from events leftover in the queue.
while (uxQueueSpacesAvailable(input_events_) < kEventQueueSize) {
StreamEvent* event;
if (xQueueReceive(input_events_, &event, 0)) {
free(event);
} else {
break;
}
}
// Technically there's a race here if someone is still adding to the queue,
// but hopefully the whole pipeline is stopped if an element is being
// destroyed.
vQueueDelete(input_events_);
}
auto IAudioElement::SendOrBufferEvent(std::unique_ptr<StreamEvent> event)
-> bool {
if (!buffered_output_.empty()) {
// To ensure we send data in order, don't try to send if we've already
// failed to send something.
buffered_output_.push_back(std::move(event));
return false;
}
StreamEvent* raw_event = event.release();
if (!xQueueSend(output_events_, &raw_event, 0)) {
event.reset(raw_event);
buffered_output_.push_back(std::move(event));
return false;
}
return true;
}
auto IAudioElement::FlushBufferedOutput() -> bool {
while (!buffered_output_.empty()) {
StreamEvent* raw_event = buffered_output_.front().release();
buffered_output_.pop_front();
if (!xQueueSend(output_events_, &raw_event, 0)) {
buffered_output_.emplace_front(raw_event);
return false;
}
}
return true;
}
} // namespace audio

@ -5,71 +5,46 @@
#include <memory> #include <memory>
#include <string_view> #include <string_view>
#include "driver_cache.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "audio_decoder.hpp" #include "audio_decoder.hpp"
#include "audio_element.hpp"
#include "audio_task.hpp" #include "audio_task.hpp"
#include "chunk.hpp" #include "chunk.hpp"
#include "fatfs_audio_input.hpp" #include "fatfs_audio_input.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "i2s_audio_output.hpp" #include "i2s_audio_output.hpp"
#include "pipeline.hpp"
#include "storage.hpp" #include "storage.hpp"
#include "stream_buffer.hpp" #include "stream_buffer.hpp"
#include "stream_info.hpp" #include "stream_info.hpp"
#include "stream_message.hpp" #include "stream_message.hpp"
namespace audio { namespace audio {
AudioPlayback::AudioPlayback(drivers::DriverCache* drivers)
auto AudioPlayback::create(drivers::GpioExpander* expander, : file_source_(std::make_unique<FatfsAudioInput>()),
std::shared_ptr<drivers::SdStorage> storage) i2s_output_(std::make_unique<I2SAudioOutput>(drivers->AcquireGpios(),
-> cpp::result<std::unique_ptr<AudioPlayback>, Error> { drivers->AcquireDac())) {
// Create everything AudioDecoder* codec = new AudioDecoder();
auto source = std::make_shared<FatfsAudioInput>(storage); elements_.emplace_back(codec);
auto codec = std::make_shared<AudioDecoder>();
Pipeline* pipeline = new Pipeline(elements_.front().get());
auto sink_res = I2SAudioOutput::create(expander); pipeline->AddInput(file_source_.get());
if (sink_res.has_error()) {
return cpp::fail(ERR_INIT_ELEMENT); task::StartPipeline(pipeline, i2s_output_.get());
} // task::StartDrain(i2s_output_.get());
auto sink = sink_res.value();
auto playback = std::make_unique<AudioPlayback>();
// Configure the pipeline
playback->ConnectElements(source.get(), codec.get());
playback->ConnectElements(codec.get(), sink.get());
// Launch!
StartAudioTask("src", {}, source);
StartAudioTask("dec", {}, codec);
StartAudioTask("sink", 0, sink);
playback->input_handle_ = source->InputEventQueue();
return playback;
} }
AudioPlayback::AudioPlayback() {}
AudioPlayback::~AudioPlayback() {} AudioPlayback::~AudioPlayback() {}
auto AudioPlayback::Play(const std::string& filename) -> void { auto AudioPlayback::Play(const std::string& filename) -> void {
StreamInfo info; // TODO: concurrency, yo!
info.path = filename; file_source_->OpenFile(filename);
auto event = StreamEvent::CreateStreamInfo(input_handle_, info);
xQueueSend(input_handle_, &event, portMAX_DELAY);
event = StreamEvent::CreateEndOfStream(input_handle_);
xQueueSend(input_handle_, &event, portMAX_DELAY);
} }
auto AudioPlayback::LogStatus() -> void { auto AudioPlayback::LogStatus() -> void {
auto event = StreamEvent::CreateLogStatus(); i2s_output_->Log();
xQueueSendToFront(input_handle_, &event, portMAX_DELAY);
}
auto AudioPlayback::ConnectElements(IAudioElement* src, IAudioElement* sink)
-> void {
src->OutputEventQueue(sink->InputEventQueue());
} }
} // namespace audio } // namespace audio

@ -2,16 +2,23 @@
#include <stdlib.h> #include <stdlib.h>
#include <algorithm>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <memory> #include <memory>
#include <variant>
#include "audio_sink.hpp"
#include "cbor.h" #include "cbor.h"
#include "dac.hpp"
#include "esp_err.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "pipeline.hpp"
#include "span.hpp" #include "span.hpp"
#include "arena.hpp" #include "arena.hpp"
@ -25,25 +32,32 @@
namespace audio { namespace audio {
namespace task {
static const char* kTag = "task"; static const char* kTag = "task";
static const std::size_t kStackSize = 24 * 1024;
static const std::size_t kDrainStackSize = 1024;
auto StartPipeline(Pipeline* pipeline, IAudioSink* sink) -> void {
// Newly created task will free this.
AudioTaskArgs* args = new AudioTaskArgs{.pipeline = pipeline, .sink = sink};
auto StartAudioTask(const std::string& name, ESP_LOGI(kTag, "starting audio pipeline task");
std::optional<BaseType_t> core_id, xTaskCreatePinnedToCore(&AudioTaskMain, "pipeline", kStackSize, args,
std::shared_ptr<IAudioElement> element) -> void { kTaskPriorityAudioPipeline, NULL, 1);
auto task_handle = std::make_unique<TaskHandle_t>(); }
auto StartDrain(IAudioSink* sink) -> void {
auto command = new std::atomic<Command>(PLAY);
// Newly created task will free this. // Newly created task will free this.
AudioTaskArgs* args = new AudioTaskArgs{.element = element}; AudioDrainArgs* drain_args = new AudioDrainArgs{
.sink = sink,
ESP_LOGI(kTag, "starting audio task %s", name.c_str()); .command = command,
if (core_id) { };
xTaskCreatePinnedToCore(&AudioTaskMain, name.c_str(),
element->StackSizeBytes(), args, kTaskPriorityAudio, ESP_LOGI(kTag, "starting audio drain task");
task_handle.get(), *core_id); xTaskCreate(&AudioDrainMain, "drain", kDrainStackSize, drain_args,
} else { kTaskPriorityAudioDrain, NULL);
xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args,
kTaskPriorityAudio, task_handle.get());
}
} }
void AudioTaskMain(void* args) { void AudioTaskMain(void* args) {
@ -51,118 +65,129 @@ void AudioTaskMain(void* args) {
// called before the task quits. // called before the task quits.
{ {
AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args); AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args);
std::shared_ptr<IAudioElement> element = std::move(real_args->element); std::unique_ptr<Pipeline> pipeline(real_args->pipeline);
IAudioSink* sink = real_args->sink;
delete real_args; delete real_args;
// Queue of events that we have received on our input queue, but not yet std::optional<StreamInfo::Format> output_format;
// processed.
std::deque<std::unique_ptr<StreamEvent>> pending_events; std::vector<Pipeline*> elements = pipeline->GetIterationOrder();
std::size_t max_inputs =
// TODO(jacqueline): quit event (*std::max_element(elements.begin(), elements.end(),
while (true) { [](Pipeline const* first, Pipeline const* second) {
// First, we pull events from our input queue into pending_events. This return first->NumInputs() < second->NumInputs();
// keeps us responsive to any events that need to be handled immediately. }))
// Then we check if there's any events to flush downstream. ->NumInputs();
// Then we pass anything requiring processing to the element.
// We need to be able to simultaneously map all of an element's inputs, plus
bool has_work_to_do = // its output. So preallocate that many ranges.
(!pending_events.empty() || element->HasUnflushedOutput() || std::vector<MappableRegion<kPipelineBufferSize>> in_regions(max_inputs);
element->HasUnprocessedInput()) && MappableRegion<kPipelineBufferSize> out_region;
!element->IsOverBuffered(); std::for_each(in_regions.begin(), in_regions.end(),
[](const auto& region) { assert(region.is_valid); });
if (has_work_to_do) { assert(out_region.is_valid);
ESP_LOGD(kTag, "checking for events");
} else { // Each element has exactly one output buffer.
ESP_LOGD(kTag, "waiting for events"); std::vector<HimemAlloc<kPipelineBufferSize>> buffers(elements.size());
} std::vector<StreamInfo> buffer_infos(buffers.size());
std::for_each(buffers.begin(), buffers.end(),
// If we have no new events to process and the element has nothing left to [](const HimemAlloc<kPipelineBufferSize>& alloc) {
// do, then just delay forever waiting for a new event. assert(alloc.is_valid);
TickType_t ticks_to_wait = has_work_to_do ? 0 : portMAX_DELAY; });
StreamEvent* new_event = nullptr; bool playing = true;
bool has_event = bool quit = false;
xQueueReceive(element->InputEventQueue(), &new_event, ticks_to_wait); while (!quit) {
if (playing) {
if (has_event) { for (int i = 0; i < elements.size(); i++) {
if (new_event->tag == StreamEvent::UNINITIALISED) { std::vector<RawStream> raw_in_streams;
ESP_LOGE(kTag, "discarding invalid event!!"); elements.at(i)->InStreams(&in_regions, &raw_in_streams);
} else if (new_event->tag == StreamEvent::CHUNK_NOTIFICATION) { RawStream raw_out_stream = elements.at(i)->OutStream(&out_region);
delete new_event;
} else if (new_event->tag == StreamEvent::LOG_STATUS) { // Crop the input and output streams to the ranges that are safe to
element->ProcessLogStatus(); // touch. For the input streams, this is the region that contains
if (element->OutputEventQueue() != nullptr) { // data. For the output stream, this is the region that does *not*
xQueueSendToFront(element->OutputEventQueue(), &new_event, 0); // already contain data.
} else { std::vector<InputStream> in_streams;
delete new_event; std::for_each(raw_in_streams.begin(), raw_in_streams.end(),
} [&](RawStream& s) { in_streams.emplace_back(&s); });
} else { OutputStream out_stream(&raw_out_stream);
// This isn't an event that needs to be actioned immediately. Add it
// to our work queue. elements.at(i)->OutputElement()->Process(in_streams, &out_stream);
pending_events.emplace_back(new_event);
ESP_LOGD(kTag, "deferring event"); std::for_each(in_regions.begin(), in_regions.end(),
} [](auto&& r) { r.Unmap(); });
// Loop again, so that we service all incoming events before doing our out_region.Unmap();
// possibly expensive processing.
continue;
} }
if (element->HasUnflushedOutput()) { RawStream raw_sink_stream = elements.front()->OutStream(&out_region);
ESP_LOGD(kTag, "flushing output"); InputStream sink_stream(&raw_sink_stream);
}
// We have no new events. Next, see if there's anything that needs to be if (sink_stream.info().bytes_in_stream == 0) {
// flushed. out_region.Unmap();
if (element->HasUnflushedOutput() && !element->FlushBufferedOutput()) {
// We had things to flush, but couldn't send it all. This probably
// implies that the downstream element is having issues servicing its
// input queue, so hold off for a moment before retrying.
ESP_LOGW(kTag, "failed to flush buffered output");
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
continue; continue;
} }
if (element->HasUnprocessedInput()) { if (!output_format || output_format != sink_stream.info().format) {
ESP_LOGD(kTag, "processing input events"); // The format of the stream within the sink stream has changed. We
element->Process(); // need to reconfigure the sink, but shouldn't do so until we've fully
continue; // drained the current buffer.
if (xStreamBufferIsEmpty(sink->buffer())) {
ESP_LOGI(kTag, "reconfiguring dac");
output_format = sink_stream.info().format;
sink->Configure(*output_format);
}
} }
// The element ran out of data, so now it's time to let it process more // We've reconfigured the sink, or it was already configured correctly.
// input. // Send through some data.
while (!pending_events.empty()) { if (output_format == sink_stream.info().format &&
std::unique_ptr<StreamEvent> event; !std::holds_alternative<std::monostate>(*output_format)) {
pending_events.front().swap(event); // TODO: tune the delay on this, as it's currently the only way to
pending_events.pop_front(); // throttle this task's CPU time. Maybe also hold off on the pipeline
ESP_LOGD(kTag, "processing event, tag %i", event->tag); // if the buffer is already close to full?
std::size_t sent = xStreamBufferSend(
if (event->tag == StreamEvent::STREAM_INFO) { sink->buffer(), sink_stream.data().data(),
ESP_LOGD(kTag, "processing stream info"); sink_stream.data().size_bytes(), pdMS_TO_TICKS(10));
if (sent > 0) {
element->ProcessStreamInfo(*event->stream_info); ESP_LOGI(kTag, "sunk %u bytes out of %u (%d %%)", sent,
sink_stream.info().bytes_in_stream,
} else if (event->tag == StreamEvent::ARENA_CHUNK) { (int)(((float)sent /
ESP_LOGD(kTag, "processing arena data"); (float)sink_stream.info().bytes_in_stream) *
100));
}
sink_stream.consume(sent);
}
memory::ArenaRef ref(event->arena_chunk); out_region.Unmap();
auto callback = }
StreamEvent::CreateChunkNotification(element->InputEventQueue()); }
if (!xQueueSend(event->source, &callback, 0)) {
ESP_LOGW(kTag, "failed to send chunk notif");
continue;
} }
vTaskDelete(NULL);
}
// TODO(jacqueline): Consider giving the element a full ArenaRef here, static std::byte sDrainBuf[8 * 1024];
// so that it can hang on to it and potentially save an alloc+copy.
element->ProcessChunk({ref.ptr.start, ref.ptr.used_size});
// TODO: think about whether to do the whole queue void AudioDrainMain(void* args) {
break; {
} AudioDrainArgs* real_args = reinterpret_cast<AudioDrainArgs*>(args);
IAudioSink* sink = real_args->sink;
std::atomic<Command>* command = real_args->command;
delete real_args;
// TODO(jacqueline): implement PAUSE without busy-waiting.
while (*command != QUIT) {
std::size_t len = xStreamBufferReceive(sink->buffer(), sDrainBuf,
sizeof(sDrainBuf), portMAX_DELAY);
if (len > 0) {
sink->Send({sDrainBuf, len});
} }
} }
} }
vTaskDelete(NULL); vTaskDelete(NULL);
} }
} // namespace task
} // namespace audio } // namespace audio

@ -4,9 +4,12 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
#include <variant>
#include "arena.hpp" #include "arena.hpp"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h"
#include "ff.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "audio_element.hpp" #include "audio_element.hpp"
@ -15,43 +18,23 @@
#include "stream_event.hpp" #include "stream_event.hpp"
#include "stream_info.hpp" #include "stream_info.hpp"
#include "stream_message.hpp" #include "stream_message.hpp"
#include "types.hpp"
static const char* kTag = "SRC"; static const char* kTag = "SRC";
namespace audio { namespace audio {
static const std::size_t kChunkSize = 24 * 1024; FatfsAudioInput::FatfsAudioInput()
static const std::size_t kChunkReadahead = 2; : IAudioElement(), current_file_(), is_file_open_(false) {}
FatfsAudioInput::FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage)
: IAudioElement(),
arena_(kChunkSize, kChunkReadahead, MALLOC_CAP_SPIRAM),
storage_(storage),
current_file_(),
is_file_open_(false) {}
FatfsAudioInput::~FatfsAudioInput() {} FatfsAudioInput::~FatfsAudioInput() {}
auto FatfsAudioInput::HasUnprocessedInput() -> bool { auto FatfsAudioInput::OpenFile(const std::string& path) -> void {
return is_file_open_;
}
auto FatfsAudioInput::IsOverBuffered() -> bool {
return arena_.BlocksFree() == 0;
}
auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
if (is_file_open_) { if (is_file_open_) {
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
} }
ESP_LOGI(kTag, "opening file %s", path.c_str());
if (!info.path) {
// TODO(jacqueline): Handle errors.
return;
}
ESP_LOGI(kTag, "opening file %s", info.path->c_str());
std::string path = *info.path;
FRESULT res = f_open(&current_file_, path.c_str(), FA_READ); FRESULT res = f_open(&current_file_, path.c_str(), FA_READ);
if (res != FR_OK) { if (res != FR_OK) {
ESP_LOGE(kTag, "failed to open file! res: %i", res); ESP_LOGE(kTag, "failed to open file! res: %i", res);
@ -60,52 +43,37 @@ auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
} }
is_file_open_ = true; is_file_open_ = true;
StreamInfo new_info(info);
new_info.chunk_size = kChunkSize;
ESP_LOGI(kTag, "chunk size: %u bytes", kChunkSize);
auto event = StreamEvent::CreateStreamInfo(input_events_, new_info);
SendOrBufferEvent(std::unique_ptr<StreamEvent>(event));
} }
auto FatfsAudioInput::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {} auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
OutputStream* output) -> void {
auto FatfsAudioInput::ProcessEndOfStream() -> void { if (!is_file_open_) {
if (is_file_open_) { // TODO(jacqueline): should we clear the stream format?
f_close(&current_file_); // output->prepare({});
is_file_open_ = false; return;
SendOrBufferEvent(std::unique_ptr<StreamEvent>(
StreamEvent::CreateEndOfStream(input_events_)));
} }
}
auto FatfsAudioInput::Process() -> void { StreamInfo::Format format = StreamInfo::Encoded{codecs::STREAM_MP3};
if (is_file_open_) { if (!output->prepare(format)) {
auto dest_block = memory::ArenaRef::Acquire(&arena_);
if (!dest_block) {
return; return;
} }
FRESULT result = f_read(&current_file_, dest_block->ptr.start, std::size_t max_size = output->data().size_bytes();
dest_block->ptr.size, &dest_block->ptr.used_size); std::size_t size = 0;
FRESULT result =
f_read(&current_file_, output->data().data(), max_size, &size);
if (result != FR_OK) { if (result != FR_OK) {
ESP_LOGE(kTag, "file I/O error %d", result); ESP_LOGE(kTag, "file I/O error %d", result);
// TODO(jacqueline): Handle errors. // TODO(jacqueline): Handle errors.
return; return;
} }
if (dest_block->ptr.used_size < dest_block->ptr.size || output->add(size);
f_eof(&current_file_)) {
if (size < max_size || f_eof(&current_file_)) {
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
} }
auto dest_event = std::unique_ptr<StreamEvent>(
StreamEvent::CreateArenaChunk(input_events_, dest_block->Release()));
SendOrBufferEvent(std::move(dest_event));
}
} }
} // namespace audio } // namespace audio

@ -1,6 +1,9 @@
#include "i2s_audio_output.hpp" #include "i2s_audio_output.hpp"
#include <algorithm> #include <algorithm>
#include <cstddef>
#include <memory>
#include <variant>
#include "esp_err.h" #include "esp_err.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
@ -10,68 +13,41 @@
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "result.hpp" #include "result.hpp"
#include "stream_info.hpp"
static const TickType_t kIdleTimeBeforeMute = pdMS_TO_TICKS(1000);
static const char* kTag = "I2SOUT"; static const char* kTag = "I2SOUT";
namespace audio { namespace audio {
static const std::size_t kDmaQueueLength = 8;
auto I2SAudioOutput::create(drivers::GpioExpander* expander)
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error> {
// First, we need to perform initial configuration of the DAC chip.
auto dac_result = drivers::AudioDac::create(expander);
if (dac_result.has_error()) {
ESP_LOGE(kTag, "failed to init dac: %d", dac_result.error());
return cpp::fail(DAC_CONFIG);
}
std::unique_ptr<drivers::AudioDac> dac = std::move(dac_result.value());
// Soft mute immediately, in order to minimise any clicks and pops caused by
// the initial output element and pipeline configuration.
// dac->WriteVolume(255);
dac->WriteVolume(120); // for testing
return std::make_shared<I2SAudioOutput>(expander, std::move(dac));
}
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander, I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
std::unique_ptr<drivers::AudioDac> dac) std::shared_ptr<drivers::AudioDac> dac)
: expander_(expander), : expander_(expander), dac_(std::move(dac)), current_config_() {
dac_(std::move(dac)), dac_->WriteVolume(127); // for testing
chunk_reader_(), dac_->SetSource(buffer());
latest_chunk_() {}
I2SAudioOutput::~I2SAudioOutput() {}
auto I2SAudioOutput::HasUnprocessedInput() -> bool {
return latest_chunk_.size() > 0;
} }
auto I2SAudioOutput::IsOverBuffered() -> bool { I2SAudioOutput::~I2SAudioOutput() {
return false; dac_->SetSource(nullptr);
} }
auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void { auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
// TODO(jacqueline): probs do something with the channel hey if (!std::holds_alternative<StreamInfo::Pcm>(format)) {
ESP_LOGI(kTag, "ignoring non-pcm stream (%d)", format.index());
if (!info.bits_per_sample || !info.sample_rate) { return false;
ESP_LOGE(kTag, "audio stream missing bits or sample rate");
return;
} }
if (!info.chunk_size) { StreamInfo::Pcm pcm = std::get<StreamInfo::Pcm>(format);
ESP_LOGE(kTag, "audio stream missing chunk size");
return; if (current_config_ && pcm == *current_config_) {
ESP_LOGI(kTag, "ignoring unchanged format");
return true;
} }
chunk_reader_.emplace(*info.chunk_size);
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %u Hz", *info.bits_per_sample, ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample,
*info.sample_rate); pcm.sample_rate);
drivers::AudioDac::BitsPerSample bps; drivers::AudioDac::BitsPerSample bps;
switch (*info.bits_per_sample) { switch (pcm.bits_per_sample) {
case 16: case 16:
bps = drivers::AudioDac::BPS_16; bps = drivers::AudioDac::BPS_16;
break; break;
@ -83,11 +59,11 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
break; break;
default: default:
ESP_LOGE(kTag, "dropping stream with unknown bps"); ESP_LOGE(kTag, "dropping stream with unknown bps");
return; return false;
} }
drivers::AudioDac::SampleRate sample_rate; drivers::AudioDac::SampleRate sample_rate;
switch (*info.sample_rate) { switch (pcm.sample_rate) {
case 44100: case 44100:
sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1; sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
break; break;
@ -96,39 +72,25 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
break; break;
default: default:
ESP_LOGE(kTag, "dropping stream with unknown rate"); ESP_LOGE(kTag, "dropping stream with unknown rate");
return; return false;
} }
// TODO(jacqueline): probs do something with the channel hey
dac_->Reconfigure(bps, sample_rate); dac_->Reconfigure(bps, sample_rate);
} current_config_ = pcm;
auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk) -> void { return true;
latest_chunk_ = chunk_reader_->HandleNewData(chunk);
} }
auto I2SAudioOutput::ProcessEndOfStream() -> void { auto I2SAudioOutput::Send(const cpp::span<std::byte>& data) -> void {
dac_->Stop(); dac_->WriteData(data);
SendOrBufferEvent(std::unique_ptr<StreamEvent>(
StreamEvent::CreateEndOfStream(input_events_)));
} }
auto I2SAudioOutput::ProcessLogStatus() -> void { auto I2SAudioOutput::Log() -> void {
dac_->LogStatus(); dac_->LogStatus();
} }
auto I2SAudioOutput::Process() -> void {
// Note: avoid logging here! We need to get bytes from the chunk buffer into
// the I2S DMA buffer as fast as possible, to avoid running out of samples.
std::size_t bytes_written = dac_->WriteData(latest_chunk_);
if (bytes_written == latest_chunk_.size_bytes()) {
latest_chunk_ = cpp::span<std::byte>();
chunk_reader_->HandleBytesLeftOver(0);
} else {
latest_chunk_ = latest_chunk_.subspan(bytes_written);
}
return;
}
auto I2SAudioOutput::SetVolume(uint8_t volume) -> void { auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
dac_->WriteVolume(volume); dac_->WriteVolume(volume);
} }

@ -3,6 +3,7 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector>
#include "chunk.hpp" #include "chunk.hpp"
#include "ff.h" #include "ff.h"
@ -10,6 +11,7 @@
#include "audio_element.hpp" #include "audio_element.hpp"
#include "codec.hpp" #include "codec.hpp"
#include "stream_info.hpp"
namespace audio { namespace audio {
@ -22,28 +24,19 @@ class AudioDecoder : public IAudioElement {
AudioDecoder(); AudioDecoder();
~AudioDecoder(); ~AudioDecoder();
auto StackSizeBytes() const -> std::size_t override { return 10 * 1024; }; auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
-> void override;
auto HasUnprocessedInput() -> bool override;
auto IsOverBuffered() -> bool override;
auto ProcessStreamInfo(const StreamInfo& info) -> void override;
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override;
auto ProcessEndOfStream() -> void override;
auto Process() -> void override;
AudioDecoder(const AudioDecoder&) = delete; AudioDecoder(const AudioDecoder&) = delete;
AudioDecoder& operator=(const AudioDecoder&) = delete; AudioDecoder& operator=(const AudioDecoder&) = delete;
private: private:
memory::Arena arena_;
std::unique_ptr<codecs::ICodec> current_codec_; std::unique_ptr<codecs::ICodec> current_codec_;
std::optional<StreamInfo> stream_info_; std::optional<StreamInfo::Format> current_input_format_;
std::optional<ChunkReader> chunk_reader_; std::optional<StreamInfo::Format> current_output_format_;
bool has_sent_stream_info_;
bool has_samples_to_send_; bool has_samples_to_send_;
bool needs_more_input_;
auto ProcessStreamInfo(const StreamInfo& info) -> bool;
}; };
} // namespace audio } // namespace audio

@ -37,65 +37,11 @@ static const size_t kEventQueueSize = 8;
*/ */
class IAudioElement { class IAudioElement {
public: public:
IAudioElement(); IAudioElement() {}
virtual ~IAudioElement(); virtual ~IAudioElement() {}
/* virtual auto Process(const std::vector<InputStream>& inputs,
* Returns the stack size in bytes that this element requires. This should OutputStream* output) -> void = 0;
* be tuned according to the observed stack size of each element, as different
* elements have fairly different stack requirements (particular decoders).
*/
virtual auto StackSizeBytes() const -> std::size_t { return 4096; };
/* Returns this element's input buffer. */
auto InputEventQueue() const -> QueueHandle_t { return input_events_; }
/* Returns this element's output buffer. */
auto OutputEventQueue() const -> QueueHandle_t { return output_events_; }
auto OutputEventQueue(const QueueHandle_t q) -> void { output_events_ = q; }
virtual auto HasUnprocessedInput() -> bool = 0;
virtual auto IsOverBuffered() -> bool { return false; }
auto HasUnflushedOutput() -> bool { return !buffered_output_.empty(); }
auto FlushBufferedOutput() -> bool;
/*
* Called when a StreamInfo message is received. Used to configure this
* element in preperation for incoming chunks.
*/
virtual auto ProcessStreamInfo(const StreamInfo& info) -> void = 0;
/*
* Called when a ChunkHeader message is received. Includes the data associated
* with this chunk of stream data. This method should return the number of
* bytes in this chunk that were actually used; leftover bytes will be
* prepended to the next call.
*/
virtual auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void = 0;
virtual auto ProcessEndOfStream() -> void = 0;
virtual auto ProcessLogStatus() -> void {}
/*
* Called when there has been no data received over the input buffer for some
* time. This could be used to synthesize output, or to save memory by
* releasing unused resources.
*/
virtual auto Process() -> void = 0;
protected:
auto SendOrBufferEvent(std::unique_ptr<StreamEvent> event) -> bool;
// Queue for events coming into this element. Owned by us.
QueueHandle_t input_events_;
// Queue for events going into the next element. Not owned by us, may be null
// if we're not yet in a pipeline.
// FIXME: it would be nicer if this was non-nullable.
QueueHandle_t output_events_;
// Output events that have been generated, but are yet to be sent downstream.
std::deque<std::unique_ptr<StreamEvent>> buffered_output_;
}; };
} // namespace audio } // namespace audio

@ -5,7 +5,11 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "audio_task.hpp"
#include "driver_cache.hpp"
#include "esp_err.h" #include "esp_err.h"
#include "fatfs_audio_input.hpp"
#include "i2s_audio_output.hpp"
#include "result.hpp" #include "result.hpp"
#include "span.hpp" #include "span.hpp"
@ -22,13 +26,7 @@ namespace audio {
*/ */
class AudioPlayback { class AudioPlayback {
public: public:
enum Error { ERR_INIT_ELEMENT, ERR_MEM }; explicit AudioPlayback(drivers::DriverCache* drivers);
static auto create(drivers::GpioExpander* expander,
std::shared_ptr<drivers::SdStorage> storage)
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>;
// TODO(jacqueline): configure on the fly once we have things to configure.
AudioPlayback();
~AudioPlayback(); ~AudioPlayback();
/* /*
@ -44,9 +42,9 @@ class AudioPlayback {
AudioPlayback& operator=(const AudioPlayback&) = delete; AudioPlayback& operator=(const AudioPlayback&) = delete;
private: private:
auto ConnectElements(IAudioElement* src, IAudioElement* sink) -> void; std::unique_ptr<FatfsAudioInput> file_source_;
std::unique_ptr<I2SAudioOutput> i2s_output_;
QueueHandle_t input_handle_; std::vector<std::unique_ptr<IAudioElement>> elements_;
}; };
} // namespace audio } // namespace audio

@ -0,0 +1,44 @@
#pragma once
#include <stdint.h>
#include "audio_element.hpp"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "stream_info.hpp"
namespace audio {
class IAudioSink {
private:
// TODO: tune. at least about 12KiB seems right for mp3
static const std::size_t kDrainBufferSize = 48 * 1024;
uint8_t* buffer_;
StaticStreamBuffer_t* metadata_;
StreamBufferHandle_t handle_;
public:
IAudioSink()
: buffer_(reinterpret_cast<uint8_t*>(
heap_caps_malloc(kDrainBufferSize,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))),
metadata_(reinterpret_cast<StaticStreamBuffer_t*>(
heap_caps_malloc(sizeof(StaticStreamBuffer_t),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))),
handle_(xStreamBufferCreateStatic(kDrainBufferSize,
1,
buffer_,
metadata_)) {}
virtual ~IAudioSink() {
vStreamBufferDelete(handle_);
free(buffer_);
free(metadata_);
}
virtual auto Configure(const StreamInfo::Format& format) -> bool = 0;
virtual auto Send(const cpp::span<std::byte>& data) -> void = 0;
virtual auto Log() -> void {}
auto buffer() -> StreamBufferHandle_t { return handle_; }
};
} // namespace audio

@ -5,18 +5,33 @@
#include <string> #include <string>
#include "audio_element.hpp" #include "audio_element.hpp"
#include "audio_sink.hpp"
#include "dac.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "pipeline.hpp"
#include "stream_buffer.hpp"
namespace audio { namespace audio {
namespace task {
enum Command { PLAY, PAUSE, QUIT };
struct AudioTaskArgs { struct AudioTaskArgs {
std::shared_ptr<IAudioElement>& element; Pipeline* pipeline;
IAudioSink* sink;
}; };
struct AudioDrainArgs {
IAudioSink* sink;
std::atomic<Command>* command;
};
extern "C" void AudioTaskMain(void* args);
extern "C" void AudioDrainMain(void* args);
auto StartAudioTask(const std::string& name, auto StartPipeline(Pipeline* pipeline, IAudioSink* sink) -> void;
std::optional<BaseType_t> core_id, auto StartDrain(IAudioSink* sink) -> void;
std::shared_ptr<IAudioElement> element) -> void;
void AudioTaskMain(void* args); } // namespace task
} // namespace audio } // namespace audio

@ -3,41 +3,37 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "arena.hpp" #include "arena.hpp"
#include "chunk.hpp" #include "chunk.hpp"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "ff.h"
#include "freertos/message_buffer.h" #include "freertos/message_buffer.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "span.hpp" #include "span.hpp"
#include "audio_element.hpp" #include "audio_element.hpp"
#include "storage.hpp"
#include "stream_buffer.hpp" #include "stream_buffer.hpp"
#include "stream_info.hpp"
namespace audio { namespace audio {
class FatfsAudioInput : public IAudioElement { class FatfsAudioInput : public IAudioElement {
public: public:
explicit FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage); explicit FatfsAudioInput();
~FatfsAudioInput(); ~FatfsAudioInput();
auto HasUnprocessedInput() -> bool override; auto OpenFile(const std::string& path) -> void;
auto IsOverBuffered() -> bool override;
auto ProcessStreamInfo(const StreamInfo& info) -> void override; auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override; -> void override;
auto ProcessEndOfStream() -> void override;
auto Process() -> void override;
FatfsAudioInput(const FatfsAudioInput&) = delete; FatfsAudioInput(const FatfsAudioInput&) = delete;
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
private: private:
memory::Arena arena_;
std::shared_ptr<drivers::SdStorage> storage_;
FIL current_file_; FIL current_file_;
bool is_file_open_; bool is_file_open_;
}; };

@ -2,34 +2,28 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector>
#include "audio_element.hpp" #include "audio_element.hpp"
#include "audio_sink.hpp"
#include "chunk.hpp" #include "chunk.hpp"
#include "result.hpp" #include "result.hpp"
#include "dac.hpp" #include "dac.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "stream_info.hpp"
namespace audio { namespace audio {
class I2SAudioOutput : public IAudioElement { class I2SAudioOutput : public IAudioSink {
public: public:
enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT };
static auto create(drivers::GpioExpander* expander)
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error>;
I2SAudioOutput(drivers::GpioExpander* expander, I2SAudioOutput(drivers::GpioExpander* expander,
std::unique_ptr<drivers::AudioDac> dac); std::shared_ptr<drivers::AudioDac> dac);
~I2SAudioOutput(); ~I2SAudioOutput();
auto HasUnprocessedInput() -> bool override; auto Configure(const StreamInfo::Format& format) -> bool override;
auto IsOverBuffered() -> bool override; auto Send(const cpp::span<std::byte>& data) -> void override;
auto Log() -> void override;
auto ProcessStreamInfo(const StreamInfo& info) -> void override;
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override;
auto ProcessEndOfStream() -> void override;
auto ProcessLogStatus() -> void override;
auto Process() -> void override;
I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
@ -38,10 +32,9 @@ class I2SAudioOutput : public IAudioElement {
auto SetVolume(uint8_t volume) -> void; auto SetVolume(uint8_t volume) -> void;
drivers::GpioExpander* expander_; drivers::GpioExpander* expander_;
std::unique_ptr<drivers::AudioDac> dac_; std::shared_ptr<drivers::AudioDac> dac_;
std::optional<ChunkReader> chunk_reader_; std::optional<StreamInfo::Pcm> current_config_;
cpp::span<std::byte> latest_chunk_;
}; };
} // namespace audio } // namespace audio

@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "freertos/portmacro.h"
#include "audio_element.hpp"
#include "himem.hpp"
#include "stream_info.hpp"
namespace audio {
static const std::size_t kPipelineBufferSize = 64 * 1024;
class Pipeline {
public:
explicit Pipeline(IAudioElement* output);
~Pipeline();
auto AddInput(IAudioElement* input) -> Pipeline*;
auto OutputElement() const -> IAudioElement*;
auto NumInputs() const -> std::size_t;
auto InStreams(std::vector<MappableRegion<kPipelineBufferSize>>*,
std::vector<RawStream>*) -> void;
auto OutStream(MappableRegion<kPipelineBufferSize>*) -> RawStream;
auto GetIterationOrder() -> std::vector<Pipeline*>;
private:
IAudioElement* root_;
std::vector<std::unique_ptr<Pipeline>> subtrees_;
HimemAlloc<kPipelineBufferSize> output_buffer_;
StreamInfo output_info_;
};
} // namespace audio

@ -4,19 +4,96 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include "cbor.h"
#include "result.hpp" #include "result.hpp"
#include "sys/_stdint.h" #include "span.hpp"
#include "types.hpp"
namespace audio { namespace audio {
struct StreamInfo { struct StreamInfo {
std::optional<std::string> path; // The number of bytes that are available for consumption within this
std::optional<uint8_t> channels; // stream's buffer.
std::optional<uint8_t> bits_per_sample; std::size_t bytes_in_stream{0};
std::optional<uint16_t> sample_rate;
std::optional<size_t> chunk_size; // The total length of this stream, in case its source is finite (e.g. a
// file on disk). May be absent for endless streams (internet streams,
// generated audio, etc.)
std::optional<std::size_t> length_bytes{};
struct Encoded {
// The codec that this stream is associated with.
codecs::StreamType type;
bool operator==(const Encoded&) const = default;
};
struct Pcm {
// Number of channels in this stream.
uint8_t channels;
// Number of bits per sample.
uint8_t bits_per_sample;
// The sample rate.
uint32_t sample_rate;
bool operator==(const Pcm&) const = default;
};
typedef std::variant<std::monostate, Encoded, Pcm> Format;
Format format{};
bool operator==(const StreamInfo&) const = default;
};
class RawStream {
public:
StreamInfo* info;
cpp::span<std::byte> data;
bool is_incomplete;
RawStream(StreamInfo* i, cpp::span<std::byte> d)
: info(i), data(d), is_incomplete(false) {}
};
/*
* A byte buffer + associated metadata, which is not allowed to modify any of
* the underlying data.
*/
class InputStream {
public:
explicit InputStream(RawStream* s) : raw_(s) {}
void consume(std::size_t bytes) const;
void mark_incomplete() const;
const StreamInfo& info() const;
cpp::span<const std::byte> data() const;
private:
RawStream* raw_;
};
class OutputStream {
public:
explicit OutputStream(RawStream* s) : raw_(s) {}
void add(std::size_t bytes) const;
bool prepare(const StreamInfo::Format& new_format);
const StreamInfo& info() const;
cpp::span<std::byte> data() const;
bool is_incomplete() const;
private:
RawStream* raw_;
}; };
} // namespace audio } // namespace audio

@ -0,0 +1,57 @@
#include "pipeline.hpp"
#include <memory>
#include "stream_info.hpp"
namespace audio {
Pipeline::Pipeline(IAudioElement* output) : root_(output), subtrees_() {
assert(output != nullptr);
}
Pipeline::~Pipeline() {}
auto Pipeline::AddInput(IAudioElement* input) -> Pipeline* {
subtrees_.push_back(std::make_unique<Pipeline>(input));
return subtrees_.back().get();
}
auto Pipeline::OutputElement() const -> IAudioElement* {
return root_;
}
auto Pipeline::NumInputs() const -> std::size_t {
return subtrees_.size();
}
auto Pipeline::InStreams(
std::vector<MappableRegion<kPipelineBufferSize>>* regions,
std::vector<RawStream>* out) -> void {
for (int i = 0; i < subtrees_.size(); i++) {
RawStream s = subtrees_[i]->OutStream(&regions->at(i));
out->push_back(s);
}
}
auto Pipeline::OutStream(MappableRegion<kPipelineBufferSize>* region)
-> RawStream {
return {&output_info_, region->Map(output_buffer_)};
}
auto Pipeline::GetIterationOrder() -> std::vector<Pipeline*> {
std::vector<Pipeline*> to_search{this};
std::vector<Pipeline*> found;
while (!to_search.empty()) {
Pipeline* current = to_search.back();
to_search.pop_back();
found.push_back(current);
for (const auto& i : current->subtrees_) {
to_search.push_back(i.get());
}
}
return found;
}
} // namespace audio

@ -0,0 +1,70 @@
#include "stream_info.hpp"
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include "result.hpp"
#include "span.hpp"
#include "types.hpp"
namespace audio {
void InputStream::consume(std::size_t bytes) const {
assert(raw_->info->bytes_in_stream >= bytes);
auto new_data = raw_->data.subspan(bytes);
std::move(new_data.begin(), new_data.end(), raw_->data.begin());
raw_->info->bytes_in_stream = new_data.size_bytes();
}
void InputStream::mark_incomplete() const {
raw_->is_incomplete = true;
}
const StreamInfo& InputStream::info() const {
return *raw_->info;
}
cpp::span<const std::byte> InputStream::data() const {
return raw_->data.first(raw_->info->bytes_in_stream);
}
void OutputStream::add(std::size_t bytes) const {
assert(raw_->info->bytes_in_stream + bytes <= raw_->data.size_bytes());
raw_->info->bytes_in_stream += bytes;
}
bool OutputStream::prepare(const StreamInfo::Format& new_format) {
if (std::holds_alternative<std::monostate>(raw_->info->format)) {
raw_->info->format = new_format;
raw_->info->bytes_in_stream = 0;
return true;
}
if (new_format == raw_->info->format) {
return true;
}
if (raw_->is_incomplete) {
raw_->info->format = new_format;
raw_->info->bytes_in_stream = 0;
return true;
}
return false;
}
const StreamInfo& OutputStream::info() const {
return *raw_->info;
}
cpp::span<std::byte> OutputStream::data() const {
return raw_->data.subspan(raw_->info->bytes_in_stream);
}
bool OutputStream::is_incomplete() const {
return raw_->is_incomplete;
}
} // namespace audio

@ -5,7 +5,7 @@
namespace codecs { namespace codecs {
auto CreateCodecForFile(const std::string& file) auto CreateCodecForType(StreamType type)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError> { -> cpp::result<std::unique_ptr<ICodec>, CreateCodecError> {
return std::make_unique<MadMp3Decoder>(); // TODO. return std::make_unique<MadMp3Decoder>(); // TODO.
} }

@ -10,6 +10,7 @@
#include "result.hpp" #include "result.hpp"
#include "span.hpp" #include "span.hpp"
#include "types.hpp"
namespace codecs { namespace codecs {
@ -17,7 +18,7 @@ class ICodec {
public: public:
virtual ~ICodec() {} virtual ~ICodec() {}
virtual auto CanHandleFile(const std::string& path) -> bool = 0; virtual auto CanHandleType(StreamType type) -> bool = 0;
struct OutputFormat { struct OutputFormat {
uint8_t num_channels; uint8_t num_channels;
@ -31,7 +32,7 @@ class ICodec {
virtual auto ResetForNewStream() -> void = 0; virtual auto ResetForNewStream() -> void = 0;
virtual auto SetInput(cpp::span<std::byte> input) -> void = 0; virtual auto SetInput(cpp::span<const std::byte> input) -> void = 0;
/* /*
* Returns the codec's next read position within the input buffer. If the * Returns the codec's next read position within the input buffer. If the
@ -63,7 +64,7 @@ class ICodec {
enum CreateCodecError { UNKNOWN_EXTENSION }; enum CreateCodecError { UNKNOWN_EXTENSION };
auto CreateCodecForFile(const std::string& file) auto CreateCodecForType(StreamType type)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>; -> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>;
} // namespace codecs } // namespace codecs

@ -17,10 +17,10 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder(); MadMp3Decoder();
~MadMp3Decoder(); ~MadMp3Decoder();
auto CanHandleFile(const std::string& path) -> bool override; auto CanHandleType(StreamType type) -> bool override;
auto GetOutputFormat() -> OutputFormat override; auto GetOutputFormat() -> OutputFormat override;
auto ResetForNewStream() -> void override; auto ResetForNewStream() -> void override;
auto SetInput(cpp::span<std::byte> input) -> void override; auto SetInput(cpp::span<const std::byte> input) -> void override;
auto GetInputPosition() -> std::size_t override; auto GetInputPosition() -> std::size_t override;
auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> override; auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> override;
auto WriteOutputSamples(cpp::span<std::byte> output) auto WriteOutputSamples(cpp::span<std::byte> output)

@ -1,16 +1,18 @@
#include "mad.hpp" #include "mad.hpp"
#include <stdint.h>
#include <cstdint> #include <cstdint>
#include "mad.h" #include "mad.h"
#include "codec.hpp" #include "codec.hpp"
#include "types.hpp"
namespace codecs { namespace codecs {
static int scaleTo24Bits(mad_fixed_t sample) { static uint32_t scaleToBits(mad_fixed_t sample, uint8_t bits) {
// Round the bottom bits. // Round the bottom bits.
sample += (1L << (MAD_F_FRACBITS - 16)); sample += (1L << (MAD_F_FRACBITS - bits));
// Clip the leftover bits to within range. // Clip the leftover bits to within range.
if (sample >= MAD_F_ONE) if (sample >= MAD_F_ONE)
@ -18,8 +20,8 @@ static int scaleTo24Bits(mad_fixed_t sample) {
else if (sample < -MAD_F_ONE) else if (sample < -MAD_F_ONE)
sample = -MAD_F_ONE; sample = -MAD_F_ONE;
/* quantize */ // Quantize.
return sample >> (MAD_F_FRACBITS + 1 - 16); return sample >> (MAD_F_FRACBITS + 1 - bits);
} }
MadMp3Decoder::MadMp3Decoder() { MadMp3Decoder::MadMp3Decoder() {
@ -35,8 +37,8 @@ MadMp3Decoder::~MadMp3Decoder() {
mad_header_finish(&header_); mad_header_finish(&header_);
} }
auto MadMp3Decoder::CanHandleFile(const std::string& path) -> bool { auto MadMp3Decoder::CanHandleType(StreamType type) -> bool {
return true; // TODO. return type == STREAM_MP3;
} }
auto MadMp3Decoder::GetOutputFormat() -> OutputFormat { auto MadMp3Decoder::GetOutputFormat() -> OutputFormat {
@ -52,7 +54,7 @@ auto MadMp3Decoder::ResetForNewStream() -> void {
has_decoded_header_ = false; has_decoded_header_ = false;
} }
auto MadMp3Decoder::SetInput(cpp::span<std::byte> input) -> void { auto MadMp3Decoder::SetInput(cpp::span<const std::byte> input) -> void {
mad_stream_buffer(&stream_, mad_stream_buffer(&stream_,
reinterpret_cast<const unsigned char*>(input.data()), reinterpret_cast<const unsigned char*>(input.data()),
input.size()); input.size());
@ -113,15 +115,26 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span<std::byte> output)
} }
while (current_sample_ < synth_.pcm.length) { while (current_sample_ < synth_.pcm.length) {
if (output_byte + (3 * synth_.pcm.channels) >= output.size()) { if (output_byte + (2 * synth_.pcm.channels) >= output.size()) {
return std::make_pair(output_byte, false); return std::make_pair(output_byte, false);
} }
for (int channel = 0; channel < synth_.pcm.channels; channel++) { for (int channel = 0; channel < synth_.pcm.channels; channel++) {
// TODO(jacqueline): output 24 bit samples when (if?) we have a downmix
// step in the pipeline.
/*
uint32_t sample_24 = uint32_t sample_24 =
scaleTo24Bits(synth_.pcm.samples[channel][current_sample_]); scaleToBits(synth_.pcm.samples[channel][current_sample_], 24);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 16) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 8) & 0xFF); output[output_byte++] = static_cast<std::byte>((sample_24 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF); output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF);
// 24 bit samples must still be aligned to 32 bits. The LSB is ignored.
output[output_byte++] = static_cast<std::byte>(0);
*/
uint16_t sample_16 =
scaleToBits(synth_.pcm.samples[channel][current_sample_], 16);
output[output_byte++] = static_cast<std::byte>((sample_16 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_16)&0xFF);
} }
current_sample_++; current_sample_++;
} }

@ -1,6 +1,6 @@
idf_component_register( idf_component_register(
SRCS "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" SRCS "touchwheel.cpp" "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
"spi.cpp" "display.cpp" "display_init.cpp" "spi.cpp" "display.cpp" "display_init.cpp" "driver_cache.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span") REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -13,8 +13,8 @@ static const adc_unit_t kAdcUnit = ADC_UNIT_1;
// Max battery voltage should be a little over 2V due to our divider, so we need // Max battery voltage should be a little over 2V due to our divider, so we need
// the max attenuation to properly handle the full range. // the max attenuation to properly handle the full range.
static const adc_atten_t kAdcAttenuation = ADC_ATTEN_DB_11; static const adc_atten_t kAdcAttenuation = ADC_ATTEN_DB_11;
// Corresponds to GPIO 34. // Corresponds to SENSOR_VP.
static const adc_channel_t kAdcChannel = ADC_CHANNEL_6; static const adc_channel_t kAdcChannel = ADC_CHANNEL_0;
Battery::Battery() { Battery::Battery() {
adc_oneshot_unit_init_cfg_t unit_config = { adc_oneshot_unit_init_cfg_t unit_config = {

@ -7,36 +7,38 @@
#include "driver/i2c.h" #include "driver/i2c.h"
#include "driver/i2s_common.h" #include "driver/i2s_common.h"
#include "driver/i2s_std.h" #include "driver/i2s_std.h"
#include "driver/i2s_types.h"
#include "esp_attr.h" #include "esp_attr.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "hal/gpio_types.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "hal/i2s_types.h" #include "hal/i2s_types.h"
#include "i2c.hpp" #include "i2c.hpp"
#include "soc/clk_tree_defs.h"
#include "sys/_stdint.h" #include "sys/_stdint.h"
namespace drivers { namespace drivers {
static const char* kTag = "AUDIODAC"; static const char* kTag = "AUDIODAC";
static const uint8_t kPcm5122Address = 0x4C; static const uint8_t kPcm5122Address = 0x4C;
static const uint8_t kPcm5122Timeout = pdMS_TO_TICKS(100);
static const i2s_port_t kI2SPort = I2S_NUM_0; static const i2s_port_t kI2SPort = I2S_NUM_0;
static const AudioDac::SampleRate kDefaultSampleRate = auto AudioDac::create(GpioExpander* expander) -> cpp::result<AudioDac*, Error> {
AudioDac::SAMPLE_RATE_44_1;
static const AudioDac::BitsPerSample kDefaultBps = AudioDac::BPS_16;
auto AudioDac::create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error> {
// TODO: tune. // TODO: tune.
i2s_chan_handle_t i2s_handle; i2s_chan_handle_t i2s_handle;
i2s_chan_config_t channel_config = i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER); I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER);
// Use the maximum possible DMA buffer size, since a smaller number of large
// copies is faster than a large number of small copies.
channel_config.dma_frame_num = 1024;
// Triple buffering should be enough to keep samples flowing smoothly.
// TODO(jacqueline): verify this with 192kHz 32bps.
channel_config.dma_desc_num = 4;
// channel_config.auto_clear = true;
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL)); ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL));
// //
@ -50,9 +52,7 @@ auto AudioDac::create(GpioExpander* expander)
i2s_std_config_t i2s_config = { i2s_std_config_t i2s_config = {
.clk_cfg = dac->clock_config_, .clk_cfg = dac->clock_config_,
.slot_cfg = dac->slot_config_, .slot_cfg = dac->slot_config_,
.gpio_cfg = .gpio_cfg = {.mclk = GPIO_NUM_0,
{// TODO: investigate running in three wire mode for less noise
.mclk = GPIO_NUM_0,
.bclk = GPIO_NUM_26, .bclk = GPIO_NUM_26,
.ws = GPIO_NUM_27, .ws = GPIO_NUM_27,
.dout = GPIO_NUM_5, .dout = GPIO_NUM_5,
@ -61,10 +61,13 @@ auto AudioDac::create(GpioExpander* expander)
{ {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_inv = false, .ws_inv = true,
}}, }},
}; };
// gpio_set_direction(GPIO_NUM_0, GPIO_MODE_OUTPUT);
// gpio_set_level(GPIO_NUM_0, 0);
if (esp_err_t err = if (esp_err_t err =
i2s_channel_init_std_mode(i2s_handle, &i2s_config) != ESP_OK) { i2s_channel_init_std_mode(i2s_handle, &i2s_config) != ESP_OK) {
ESP_LOGE(kTag, "failed to initialise i2s channel %x", err); ESP_LOGE(kTag, "failed to initialise i2s channel %x", err);
@ -81,65 +84,64 @@ auto AudioDac::create(GpioExpander* expander)
// The DAC should be booted but in power down mode, but it might not be if we // The DAC should be booted but in power down mode, but it might not be if we
// didn't shut down cleanly. Reset it to ensure it is in a consistent state. // didn't shut down cleanly. Reset it to ensure it is in a consistent state.
dac->WriteRegister(Register::POWER_MODE, 0b10001); dac->WriteRegister(pcm512x::POWER, 1 << 4);
dac->WriteRegister(Register::POWER_MODE, 1 << 4); dac->WriteRegister(pcm512x::RESET, 0b10001);
dac->WriteRegister(Register::RESET, 0b10001);
// Use BCK for the internal PLL.
// dac->WriteRegister(Register::PLL_CLOCK_SOURCE, 1 << 4);
// dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b11 << 5);
// dac->WriteRegister(Register::PLL_ENABLE, 0);
// dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b0110000);
// dac->WriteRegister(Register::CLOCK_ERRORS, 0b01000001);
// dac->WriteRegister(Register::I2S_FORMAT, 0b110000);
// dac->WriteRegister(Register::INTERPOLATION, 1 << 4);
dac->Reconfigure(BPS_16, SAMPLE_RATE_44_1);
// Now configure the DAC for standard auto-clock SCK mode. // Now configure the DAC for standard auto-clock SCK mode.
dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b11 << 5);
// Enable auto clocking, and do your best to carry on despite errors. // Enable auto clocking, and do your best to carry on despite errors.
// dac->WriteRegister(Register::CLOCK_ERRORS, 0b1111101); // dac->WriteRegister(Register::CLOCK_ERRORS, 0b1111101);
i2s_channel_enable(dac->i2s_handle_); // i2s_channel_enable(dac->i2s_handle_);
dac->WaitForPowerState( dac->WaitForPowerState([](bool booted, PowerState state) {
[](bool booted, PowerState state) { return state == STANDBY; }); return state == RUN || state == STANDBY;
});
return dac; return dac.release();
} }
AudioDac::AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle) AudioDac::AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle)
: gpio_(gpio), : gpio_(gpio),
i2s_handle_(i2s_handle), i2s_handle_(i2s_handle),
i2s_active_(false),
active_page_(),
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)), clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)),
slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO)) { I2S_SLOT_MODE_STEREO)) {
gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, true); clock_config_.clk_src = I2S_CLK_SRC_PLL_160M;
gpio_->set_pin(GpioExpander::AMP_EN, true);
gpio_->Write(); gpio_->Write();
} }
AudioDac::~AudioDac() { AudioDac::~AudioDac() {
i2s_channel_disable(i2s_handle_); i2s_channel_disable(i2s_handle_);
i2s_del_channel(i2s_handle_); i2s_del_channel(i2s_handle_);
gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, false); gpio_->set_pin(GpioExpander::AMP_EN, false);
gpio_->Write(); gpio_->Write();
} }
void AudioDac::WriteVolume(uint8_t volume) { void AudioDac::WriteVolume(uint8_t volume) {
WriteRegister(Register::DIGITAL_VOLUME_L, volume); // Left channel.
WriteRegister(Register::DIGITAL_VOLUME_R, volume); WriteRegister(pcm512x::DIGITAL_VOLUME_2, volume);
// Right channel.
WriteRegister(pcm512x::DIGITAL_VOLUME_3, volume);
} }
std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() { std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() {
uint8_t result = 0; uint8_t result = ReadRegister(pcm512x::POWER_STATE);
I2CTransaction transaction;
transaction.start()
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
.write_ack(DSP_BOOT_POWER_STATE)
.start()
.write_addr(kPcm5122Address, I2C_MASTER_READ)
.read(&result, I2C_MASTER_NACK)
.stop();
esp_err_t err = transaction.Execute();
if (err == ESP_ERR_TIMEOUT) {
return std::pair(false, POWERDOWN);
} else {
}
ESP_ERROR_CHECK(err);
bool is_booted = result >> 7; bool is_booted = result >> 7;
PowerState detail = (PowerState)(result & 0b1111); PowerState detail = (PowerState)(result & 0b1111);
return std::pair(is_booted, detail); return std::pair(is_booted, detail);
@ -163,13 +165,29 @@ bool AudioDac::WaitForPowerState(
} }
auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void { auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
// Disable the current output, if it isn't already stopped. if (i2s_active_) {
WriteRegister(Register::POWER_MODE, 1 << 4); WriteRegister(pcm512x::MUTE, 0b10001);
vTaskDelay(1);
WriteRegister(pcm512x::POWER, 1 << 4);
i2s_channel_disable(i2s_handle_); i2s_channel_disable(i2s_handle_);
}
// I2S reconfiguration. // I2S reconfiguration.
uint8_t bps_bits = 0;
slot_config_.slot_bit_width = (i2s_slot_bit_width_t)bps; switch (bps) {
case BPS_16:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
bps_bits = 0;
break;
case BPS_24:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT;
bps_bits = 0b10;
break;
case BPS_32:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
bps_bits = 0b11;
break;
}
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_)); ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_));
clock_config_.sample_rate_hz = rate; clock_config_.sample_rate_hz = rate;
@ -181,30 +199,185 @@ auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_)); ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_));
// DAC reconfiguration. // DAC reconfiguration.
// Inspired heavily by https://github.com/tommag/PCM51xx_Arduino (MIT).
// Check that the bit clock (PLL input) is between 1MHz and 50MHz. It always
// should be.
uint32_t bckFreq = rate * bps * 2;
if (bckFreq < 1000000 || bckFreq > 50000000) {
ESP_LOGE(kTag, "bck freq out of range");
return;
}
// 24 bits is not supported for 44.1kHz and 48kHz.
if ((rate == SAMPLE_RATE_44_1 || rate == SAMPLE_RATE_48) && bps == BPS_24) {
// TODO(jacqueline): I think this *can* be implemented, but requires a bunch
// of maths.
ESP_LOGE(kTag, "sample rate and bps mismatch");
return;
}
// Initialize system clock from the I2S BCK input
// Disable clock autoset and ignore SCK detection
WriteRegister(pcm512x::ERROR_DETECT, 0x1A);
// Set PLL clock source to BCK
WriteRegister(pcm512x::PLL_REF, 0x10);
// Set DAC clock source to PLL output
WriteRegister(pcm512x::DAC_REF, 0x10);
// PLL configuration
int p, j, d, r;
// Clock dividers
int nmac, ndac, ncp, dosr, idac;
if (rate == SAMPLE_RATE_11_025 || rate == SAMPLE_RATE_22_05 ||
rate == SAMPLE_RATE_44_1) {
// 44.1kHz and derivatives.
// P = 1, R = 2, D = 0 for all supported combinations.
// Set J to have PLL clk = 90.3168 MHz
p = 1;
r = 2;
j = 90316800 / bckFreq / r;
d = 0;
// Derive clocks from the 90.3168MHz PLL
nmac = 2;
ndac = 16;
ncp = 4;
dosr = 8;
idac = 1024; // DSP clock / sample rate
} else {
// 8kHz and multiples.
// PLL config for a 98.304 MHz PLL clk
if (bps == BPS_24 && bckFreq > 1536000) {
p = 3;
} else if (bckFreq > 12288000) {
p = 2;
} else {
p = 1;
}
r = 2;
j = 98304000 / (bckFreq / p) / r;
d = 0;
// TODO: base on BPS // Derive clocks from the 98.304MHz PLL
WriteRegister(Register::I2S_FORMAT, 0); switch (rate) {
case SAMPLE_RATE_16:
nmac = 6;
break;
case SAMPLE_RATE_32:
nmac = 3;
break;
default:
nmac = 2;
break;
}
ndac = 16;
ncp = 4;
dosr = 384000 / rate;
idac = 98304000 / nmac / rate; // DSP clock / sample rate
}
// Configure PLL
WriteRegister(pcm512x::PLL_COEFF_0, p - 1);
WriteRegister(pcm512x::PLL_COEFF_1, j);
WriteRegister(pcm512x::PLL_COEFF_2, (d >> 8) & 0x3F);
WriteRegister(pcm512x::PLL_COEFF_3, d & 0xFF);
WriteRegister(pcm512x::PLL_COEFF_4, r - 1);
// Clock dividers
WriteRegister(pcm512x::DSP_CLKDIV, nmac - 1);
WriteRegister(pcm512x::DAC_CLKDIV, ndac - 1);
WriteRegister(pcm512x::NCP_CLKDIV, ncp - 1);
WriteRegister(pcm512x::OSR_CLKDIV, dosr - 1);
// IDAC (nb of DSP clock cycles per sample)
WriteRegister(pcm512x::IDAC_1, (idac >> 8) & 0xFF);
WriteRegister(pcm512x::IDAC_2, idac & 0xFF);
// FS speed mode
int speedMode;
if (rate <= SAMPLE_RATE_48) {
speedMode = 0;
} else if (rate <= SAMPLE_RATE_96) {
speedMode = 1;
} else if (rate <= SAMPLE_RATE_192) {
speedMode = 2;
} else {
speedMode = 3;
}
WriteRegister(pcm512x::FS_SPEED_MODE, speedMode);
WriteRegister(pcm512x::I2S_1, (0b11 << 4) | bps_bits);
WriteRegister(pcm512x::I2S_2, 0);
// Configuration is all done, so we can now bring the DAC and I2S stream back // Configuration is all done, so we can now bring the DAC and I2S stream back
// up. I2S first, since otherwise the DAC will see that there's no clocks and // up. I2S first, since otherwise the DAC will see that there's no clocks and
// shut itself down. // shut itself down.
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_)); ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
WriteRegister(Register::POWER_MODE, 0); WriteRegister(pcm512x::POWER, 0);
if (i2s_active_) {
vTaskDelay(1);
WriteRegister(pcm512x::MUTE, 0);
}
i2s_active_ = true;
} }
auto AudioDac::WriteData(cpp::span<std::byte> data) -> std::size_t { auto AudioDac::WriteData(const cpp::span<const std::byte>& data) -> void {
std::size_t bytes_written = 0; std::size_t bytes_written = 0;
esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(), esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
&bytes_written, 0); &bytes_written, portMAX_DELAY);
if (err != ESP_ERR_TIMEOUT) { if (err != ESP_ERR_TIMEOUT) {
ESP_ERROR_CHECK(err); ESP_ERROR_CHECK(err);
} }
return bytes_written; }
extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle,
i2s_event_data_t* event,
void* user_ctx) -> bool {
if (event == nullptr || user_ctx == nullptr) {
return false;
}
if (event->data == nullptr || event->size == 0) {
return false;
}
uint8_t** buf = reinterpret_cast<uint8_t**>(event->data);
StreamBufferHandle_t src = reinterpret_cast<StreamBufferHandle_t>(user_ctx);
BaseType_t ret = false;
std::size_t bytes_received =
xStreamBufferReceiveFromISR(src, *buf, event->size, &ret);
if (bytes_received < event->size) {
memset(*buf + bytes_received, 0, event->size - bytes_received);
}
return ret;
}
auto AudioDac::SetSource(StreamBufferHandle_t buffer) -> void {
if (i2s_active_) {
ESP_ERROR_CHECK(i2s_channel_disable(i2s_handle_));
}
i2s_event_callbacks_t callbacks{
.on_recv = NULL,
.on_recv_q_ovf = NULL,
.on_sent = NULL,
.on_send_q_ovf = NULL,
};
if (buffer != nullptr) {
callbacks.on_sent = &callback;
}
i2s_channel_register_event_callback(i2s_handle_, &callbacks, buffer);
if (i2s_active_) {
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
}
} }
auto AudioDac::Stop() -> void { auto AudioDac::Stop() -> void {
LogStatus(); LogStatus();
WriteRegister(Register::POWER_MODE, 1 << 4); WriteRegister(pcm512x::POWER, 1 << 4);
i2s_channel_disable(i2s_handle_); i2s_channel_disable(i2s_handle_);
} }
@ -218,35 +391,47 @@ auto AudioDac::Stop() -> void {
auto AudioDac::LogStatus() -> void { auto AudioDac::LogStatus() -> void {
uint8_t res; uint8_t res;
res = ReadRegister(Register::SAMPLE_RATE_DETECTION); res = ReadRegister(pcm512x::RATE_DET_1);
ESP_LOGI(kTag, "detected sample rate (want 3): %u", (res >> 4) && 0b111); ESP_LOGI(kTag, "detected sample rate (want 3): %u", (res & 0b01110000) >> 4);
ESP_LOGI(kTag, "detected SCK ratio (want 6): %u", res && 0b1111); ESP_LOGI(kTag, "detected SCK ratio (want 6): %u", res && 0b1111);
res = ReadRegister(Register::BCK_DETECTION); res = ReadRegister(pcm512x::RATE_DET_3);
ESP_LOGI(kTag, "detected BCK (want... 16? 32?): %u", res); ESP_LOGI(kTag, "detected BCK (want... 16? 32?): %u", res);
res = ReadRegister(Register::CLOCK_ERROR_STATE); res = ReadRegister(pcm512x::RATE_DET_4);
ESP_LOGI(kTag, "clock errors (want zeroes): "); ESP_LOGI(kTag, "clock errors (want zeroes): ");
ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b1111111)); ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b1111111));
res = ReadRegister(Register::CLOCK_STATUS); res = ReadRegister(pcm512x::CLOCK_STATUS);
ESP_LOGI(kTag, "clock status (want zeroes): "); ESP_LOGI(kTag, "clock status (want zeroes): ");
ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b10111)); ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b10111));
res = ReadRegister(Register::AUTO_MUTE_STATE); res = ReadRegister(pcm512x::DIGITAL_MUTE_DET);
ESP_LOGI(kTag, "automute status (want 3): %u", res & 0b11); ESP_LOGI(kTag, "automute status (want 0): %u", res & 0b10001);
res = ReadRegister(Register::SOFT_MUTE_STATE);
ESP_LOGI(kTag, "soft mute pin status (want 3): %u", res & 0b11);
res = ReadRegister(Register::SAMPLE_RATE_STATE);
ESP_LOGI(kTag, "detected sample speed mode (want 0): %u", res & 0b11);
auto power = ReadPowerState(); auto power = ReadPowerState();
ESP_LOGI(kTag, "current power state (want 5): %u", power.second); ESP_LOGI(kTag, "current power state (want 5): %u", power.second);
} }
void AudioDac::WriteRegister(Register reg, uint8_t val) { void AudioDac::WriteRegister(pcm512x::Register r, uint8_t val) {
SelectPage(r.page);
WriteRegisterRaw(r.reg, val);
}
uint8_t AudioDac::ReadRegister(pcm512x::Register r) {
SelectPage(r.page);
return ReadRegisterRaw(r.reg);
}
void AudioDac::SelectPage(uint8_t page) {
if (active_page_ && active_page_ == page) {
return;
}
WriteRegisterRaw(0, page);
active_page_ = page;
}
void AudioDac::WriteRegisterRaw(uint8_t reg, uint8_t val) {
I2CTransaction transaction; I2CTransaction transaction;
transaction.start() transaction.start()
.write_addr(kPcm5122Address, I2C_MASTER_WRITE) .write_addr(kPcm5122Address, I2C_MASTER_WRITE)
@ -256,7 +441,7 @@ void AudioDac::WriteRegister(Register reg, uint8_t val) {
transaction.Execute(); transaction.Execute();
} }
uint8_t AudioDac::ReadRegister(Register reg) { uint8_t AudioDac::ReadRegisterRaw(uint8_t reg) {
uint8_t result = 0; uint8_t result = 0;
I2CTransaction transaction; I2CTransaction transaction;
transaction.start() transaction.start()

@ -20,6 +20,7 @@
#include "display_init.hpp" #include "display_init.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "soc/soc.h"
static const char* kTag = "DISPLAY"; static const char* kTag = "DISPLAY";
@ -28,6 +29,10 @@ static const uint8_t kDisplayWidth = 128 + 2;
static const uint8_t kDisplayHeight = 160 + 1; static const uint8_t kDisplayHeight = 160 + 1;
static const uint8_t kTransactionQueueSize = 10; static const uint8_t kTransactionQueueSize = 10;
static const gpio_num_t kDisplayDr = GPIO_NUM_33;
static const gpio_num_t kDisplayLedEn = GPIO_NUM_32;
static const gpio_num_t kDisplayCs = GPIO_NUM_22;
/* /*
* The size of each of our two display buffers. This is fundamentally a balance * The size of each of our two display buffers. This is fundamentally a balance
* between performance and memory usage. LVGL docs recommend a buffer 1/10th the * between performance and memory usage. LVGL docs recommend a buffer 1/10th the
@ -58,11 +63,28 @@ extern "C" void FlushDataCallback(lv_disp_drv_t* disp_drv,
auto Display::create(GpioExpander* expander, auto Display::create(GpioExpander* expander,
const displays::InitialisationData& init_data) const displays::InitialisationData& init_data)
-> std::unique_ptr<Display> { -> Display* {
// First, turn on the LED backlight. ESP_LOGI(kTag, "Init I/O pins");
expander->set_pin(GpioExpander::DISPLAY_LED, 0); gpio_config_t dr_config{
expander->set_pin(GpioExpander::DISPLAY_POWER_ENABLE, 1); .pin_bit_mask = 1ULL << kDisplayDr,
expander->Write(); .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&dr_config);
gpio_set_level(kDisplayDr, 0);
// TODO: use pwm for the backlight.
gpio_config_t led_config{
.pin_bit_mask = 1ULL << kDisplayLedEn,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&led_config);
gpio_set_level(kDisplayLedEn, 1);
// Next, init the SPI device // Next, init the SPI device
spi_device_interface_config_t spi_cfg = { spi_device_interface_config_t spi_cfg = {
@ -76,7 +98,7 @@ auto Display::create(GpioExpander* expander,
.cs_ena_posttrans = 0, .cs_ena_posttrans = 0,
.clock_speed_hz = SPI_MASTER_FREQ_40M, .clock_speed_hz = SPI_MASTER_FREQ_40M,
.input_delay_ns = 0, .input_delay_ns = 0,
.spics_io_num = GPIO_NUM_22, .spics_io_num = kDisplayCs,
.flags = 0, .flags = 0,
.queue_size = kTransactionQueueSize, .queue_size = kTransactionQueueSize,
.pre_cb = NULL, .pre_cb = NULL,
@ -103,13 +125,16 @@ auto Display::create(GpioExpander* expander,
display->driver_.draw_buf = &display->buffers_; display->driver_.draw_buf = &display->buffers_;
display->driver_.hor_res = kDisplayWidth; display->driver_.hor_res = kDisplayWidth;
display->driver_.ver_res = kDisplayHeight; display->driver_.ver_res = kDisplayHeight;
display->driver_.sw_rotate = 1;
display->driver_.rotated = LV_DISP_ROT_270;
display->driver_.antialiasing = 0;
display->driver_.flush_cb = &FlushDataCallback; display->driver_.flush_cb = &FlushDataCallback;
display->driver_.user_data = display.get(); display->driver_.user_data = display.get();
ESP_LOGI(kTag, "Registering driver"); ESP_LOGI(kTag, "Registering driver");
display->display_ = lv_disp_drv_register(&display->driver_); display->display_ = lv_disp_drv_register(&display->driver_);
return display; return display.release();
} }
Display::Display(GpioExpander* gpio, spi_device_handle_t handle) Display::Display(GpioExpander* gpio, spi_device_handle_t handle)
@ -190,9 +215,7 @@ void Display::SendTransaction(TransactionType type,
transaction.tx_buffer = data; transaction.tx_buffer = data;
} }
// TODO(jacqueline): Move this to an on-board GPIO for speed. gpio_set_level(kDisplayDr, type);
gpio_->set_pin(GpioExpander::DISPLAY_DR, type);
gpio_->Write();
// TODO(jacqueline): Handle these errors. // TODO(jacqueline): Handle these errors.
esp_err_t ret = spi_device_polling_transmit(handle_, &transaction); esp_err_t ret = spi_device_polling_transmit(handle_, &transaction);

@ -0,0 +1,43 @@
#include "driver_cache.hpp"
#include <memory>
#include <mutex>
#include "display.hpp"
#include "display_init.hpp"
#include "storage.hpp"
#include "touchwheel.hpp"
namespace drivers {
DriverCache::DriverCache() : gpios_(std::make_unique<GpioExpander>()) {}
DriverCache::~DriverCache() {}
auto DriverCache::AcquireGpios() -> GpioExpander* {
return gpios_.get();
}
auto DriverCache::AcquireDac() -> std::shared_ptr<AudioDac> {
return Acquire(dac_, [&]() -> AudioDac* {
return AudioDac::create(AcquireGpios()).value_or(nullptr);
});
}
auto DriverCache::AcquireDisplay() -> std::shared_ptr<Display> {
return Acquire(display_, [&]() -> Display* {
return Display::create(AcquireGpios(), displays::kST7735R);
});
}
auto DriverCache::AcquireStorage() -> std::shared_ptr<SdStorage> {
return Acquire(storage_, [&]() -> SdStorage* {
return SdStorage::create(AcquireGpios()).value_or(nullptr);
});
}
auto DriverCache::AcquireTouchWheel() -> std::shared_ptr<TouchWheel> {
return Acquire(touchwheel_,
[&]() -> TouchWheel* { return new TouchWheel(); });
}
} // namespace drivers

@ -9,8 +9,8 @@
namespace drivers { namespace drivers {
static const i2c_port_t kI2CPort = I2C_NUM_0; static const i2c_port_t kI2CPort = I2C_NUM_0;
static const gpio_num_t kI2CSdaPin = GPIO_NUM_2; static const gpio_num_t kI2CSdaPin = GPIO_NUM_4;
static const gpio_num_t kI2CSclPin = GPIO_NUM_4; static const gpio_num_t kI2CSclPin = GPIO_NUM_2;
static const uint32_t kI2CClkSpeed = 400'000; static const uint32_t kI2CClkSpeed = 400'000;
static constexpr int kCmdLinkSize = I2C_LINK_RECOMMENDED_SIZE(12); static constexpr int kCmdLinkSize = I2C_LINK_RECOMMENDED_SIZE(12);
@ -36,6 +36,9 @@ esp_err_t init_i2c(void) {
if (esp_err_t err = i2c_driver_install(kI2CPort, config.mode, 0, 0, 0)) { if (esp_err_t err = i2c_driver_install(kI2CPort, config.mode, 0, 0, 0)) {
return err; return err;
} }
if (esp_err_t err = i2c_set_timeout(kI2CPort, 400000)) {
return err;
}
// TODO: INT line // TODO: INT line
@ -57,8 +60,8 @@ I2CTransaction::~I2CTransaction() {
free(buffer_); free(buffer_);
} }
esp_err_t I2CTransaction::Execute() { esp_err_t I2CTransaction::Execute(uint8_t port) {
return i2c_master_cmd_begin(I2C_NUM_0, handle_, kI2CTimeout); return i2c_master_cmd_begin(port, handle_, kI2CTimeout);
} }
I2CTransaction& I2CTransaction::start() { I2CTransaction& I2CTransaction::start() {

@ -12,6 +12,7 @@
#include "esp_err.h" #include "esp_err.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/stream_buffer.h"
#include "result.hpp" #include "result.hpp"
#include "span.hpp" #include "span.hpp"
@ -20,6 +21,91 @@
namespace drivers { namespace drivers {
namespace pcm512x {
class Register {
public:
uint8_t page;
uint8_t reg;
constexpr Register(uint8_t p, uint8_t r) : page(p), reg(r) {}
};
constexpr Register RESET(0, 1);
constexpr Register POWER(0, 2);
constexpr Register MUTE(0, 3);
constexpr Register PLL_EN(0, 4);
constexpr Register SPI_MISO_FUNCTION(0, 6);
constexpr Register DSP(0, 7);
constexpr Register GPIO_EN(0, 8);
constexpr Register BCLK_LRCLK_CFG(0, 9);
constexpr Register DSP_GPIO_INPUT(0, 10);
constexpr Register MASTER_MODE(0, 12);
constexpr Register PLL_REF(0, 13);
constexpr Register DAC_REF(0, 14);
constexpr Register GPIO_DACIN(0, 16);
constexpr Register GPIO_PLLIN(0, 18);
constexpr Register SYNCHRONIZE(0, 19);
constexpr Register PLL_COEFF_0(0, 20);
constexpr Register PLL_COEFF_1(0, 21);
constexpr Register PLL_COEFF_2(0, 22);
constexpr Register PLL_COEFF_3(0, 23);
constexpr Register PLL_COEFF_4(0, 24);
constexpr Register DSP_CLKDIV(0, 27);
constexpr Register DAC_CLKDIV(0, 28);
constexpr Register NCP_CLKDIV(0, 29);
constexpr Register OSR_CLKDIV(0, 30);
constexpr Register MASTER_CLKDIV_1(0, 32);
constexpr Register MASTER_CLKDIV_2(0, 33);
constexpr Register FS_SPEED_MODE(0, 34);
constexpr Register IDAC_1(0, 35);
constexpr Register IDAC_2(0, 36);
constexpr Register ERROR_DETECT(0, 37);
constexpr Register I2S_1(0, 40);
constexpr Register I2S_2(0, 41);
constexpr Register DAC_ROUTING(0, 42);
constexpr Register DSP_PROGRAM(0, 43);
constexpr Register CLKDET(0, 44);
constexpr Register AUTO_MUTE(0, 59);
constexpr Register DIGITAL_VOLUME_1(0, 60);
constexpr Register DIGITAL_VOLUME_2(0, 61);
constexpr Register DIGITAL_VOLUME_3(0, 62);
constexpr Register DIGITAL_MUTE_1(0, 63);
constexpr Register DIGITAL_MUTE_2(0, 64);
constexpr Register DIGITAL_MUTE_3(0, 65);
constexpr Register GPIO_OUTPUT_1(0, 80);
constexpr Register GPIO_OUTPUT_2(0, 81);
constexpr Register GPIO_OUTPUT_3(0, 82);
constexpr Register GPIO_OUTPUT_4(0, 83);
constexpr Register GPIO_OUTPUT_5(0, 84);
constexpr Register GPIO_OUTPUT_6(0, 85);
constexpr Register GPIO_CONTROL_1(0, 86);
constexpr Register GPIO_CONTROL_2(0, 87);
constexpr Register OVERFLOW(0, 90);
constexpr Register RATE_DET_1(0, 91);
constexpr Register RATE_DET_2(0, 92);
constexpr Register RATE_DET_3(0, 93);
constexpr Register RATE_DET_4(0, 94);
constexpr Register CLOCK_STATUS(0, 95);
constexpr Register ANALOG_MUTE_DET(0, 108);
constexpr Register POWER_STATE(0, 118);
constexpr Register GPIN(0, 119);
constexpr Register DIGITAL_MUTE_DET(0, 120);
constexpr Register OUTPUT_AMPLITUDE(1, 1);
constexpr Register ANALOG_GAIN_CTRL(1, 2);
constexpr Register UNDERVOLTAGE_PROT(1, 5);
constexpr Register ANALOG_MUTE_CTRL(1, 6);
constexpr Register ANALOG_GAIN_BOOST(1, 7);
constexpr Register VCOM_CTRL_1(1, 8);
constexpr Register VCOM_CTRL_2(1, 9);
constexpr Register CRAM_CTRL(44, 1);
constexpr Register FLEX_A(253, 63);
constexpr Register FLEX_B(253, 64);
} // namespace pcm512x
/** /**
* Interface for a PCM5122PWR DAC, configured over I2C. * Interface for a PCM5122PWR DAC, configured over I2C.
*/ */
@ -31,8 +117,7 @@ class AudioDac {
FAILED_TO_INSTALL_I2S, FAILED_TO_INSTALL_I2S,
}; };
static auto create(GpioExpander* expander) static auto create(GpioExpander* expander) -> cpp::result<AudioDac*, Error>;
-> cpp::result<std::unique_ptr<AudioDac>, Error>;
AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle); AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle);
~AudioDac(); ~AudioDac();
@ -64,14 +149,21 @@ class AudioDac {
BPS_32 = I2S_DATA_BIT_WIDTH_32BIT, BPS_32 = I2S_DATA_BIT_WIDTH_32BIT,
}; };
enum SampleRate { enum SampleRate {
SAMPLE_RATE_11_025 = 11025,
SAMPLE_RATE_16 = 16000,
SAMPLE_RATE_22_05 = 22050,
SAMPLE_RATE_32 = 32000,
SAMPLE_RATE_44_1 = 44100, SAMPLE_RATE_44_1 = 44100,
SAMPLE_RATE_48 = 48000, SAMPLE_RATE_48 = 48000,
SAMPLE_RATE_96 = 96000,
SAMPLE_RATE_192 = 192000,
}; };
// TODO(jacqueline): worth supporting channels here as well? // TODO(jacqueline): worth supporting channels here as well?
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void; auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
auto WriteData(cpp::span<std::byte> data) -> std::size_t; auto WriteData(const cpp::span<const std::byte>& data) -> void;
auto SetSource(StreamBufferHandle_t buffer) -> void;
auto Stop() -> void; auto Stop() -> void;
auto LogStatus() -> void; auto LogStatus() -> void;
@ -83,6 +175,8 @@ class AudioDac {
private: private:
GpioExpander* gpio_; GpioExpander* gpio_;
i2s_chan_handle_t i2s_handle_; i2s_chan_handle_t i2s_handle_;
bool i2s_active_;
std::optional<uint8_t> active_page_;
i2s_std_clk_config_t clock_config_; i2s_std_clk_config_t clock_config_;
i2s_std_slot_config_t slot_config_; i2s_std_slot_config_t slot_config_;
@ -93,29 +187,12 @@ class AudioDac {
*/ */
bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate); bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate);
enum Register { void WriteRegister(pcm512x::Register r, uint8_t val);
PAGE_SELECT = 0, uint8_t ReadRegister(pcm512x::Register r);
RESET = 1,
POWER_MODE = 2,
DE_EMPHASIS = 7,
DAC_CLOCK_SOURCE = 14,
CLOCK_ERRORS = 37,
I2S_FORMAT = 40,
DIGITAL_VOLUME_L = 61,
DIGITAL_VOLUME_R = 62,
SAMPLE_RATE_DETECTION = 91,
BCK_DETECTION = 93,
CLOCK_ERROR_STATE = 94,
CLOCK_STATUS = 95,
AUTO_MUTE_STATE = 108,
SOFT_MUTE_STATE = 114,
SAMPLE_RATE_STATE = 115,
DSP_BOOT_POWER_STATE = 118,
};
void WriteRegister(Register reg, uint8_t val); void SelectPage(uint8_t page);
uint8_t ReadRegister(Register reg); void WriteRegisterRaw(uint8_t reg, uint8_t val);
uint8_t ReadRegisterRaw(uint8_t reg);
}; };
} // namespace drivers } // namespace drivers

@ -23,8 +23,7 @@ class Display {
* us back any kind of signal to tell us we're actually using them correctly. * us back any kind of signal to tell us we're actually using them correctly.
*/ */
static auto create(GpioExpander* expander, static auto create(GpioExpander* expander,
const displays::InitialisationData& init_data) const displays::InitialisationData& init_data) -> Display*;
-> std::unique_ptr<Display>;
Display(GpioExpander* gpio, spi_device_handle_t handle); Display(GpioExpander* gpio, spi_device_handle_t handle);
~Display(); ~Display();

@ -0,0 +1,54 @@
#pragma once
#include <memory>
#include <mutex>
#include "dac.hpp"
#include "display.hpp"
#include "gpio_expander.hpp"
#include "storage.hpp"
#include "touchwheel.hpp"
namespace drivers {
class DriverCache {
private:
std::unique_ptr<GpioExpander> gpios_;
std::weak_ptr<AudioDac> dac_;
std::weak_ptr<Display> display_;
std::weak_ptr<SdStorage> storage_;
std::weak_ptr<TouchWheel> touchwheel_;
// TODO(jacqueline): Haptics, samd
std::mutex mutex_;
template <typename T, typename F>
auto Acquire(std::weak_ptr<T> ptr, F factory) -> std::shared_ptr<T> {
std::shared_ptr<T> acquired = ptr.lock();
if (acquired) {
return acquired;
}
std::lock_guard<std::mutex> lock(mutex_);
acquired = ptr.lock();
if (acquired) {
return acquired;
}
acquired.reset(factory());
ptr = acquired;
return acquired;
}
public:
DriverCache();
~DriverCache();
auto AcquireGpios() -> GpioExpander*;
auto AcquireDac() -> std::shared_ptr<AudioDac>;
auto AcquireDisplay() -> std::shared_ptr<Display>;
auto AcquireStorage() -> std::shared_ptr<SdStorage>;
auto AcquireTouchWheel() -> std::shared_ptr<TouchWheel>;
};
} // namespace drivers

@ -35,28 +35,28 @@ class GpioExpander {
static const uint8_t kPca8575Timeout = pdMS_TO_TICKS(100); static const uint8_t kPca8575Timeout = pdMS_TO_TICKS(100);
// Port A: // Port A:
// 0 - audio power enable // 0 - sd card mux switch
// 1 - usb interface power enable (active low) // 1 - sd card mux enable (active low)
// 2 - display power enable // 2 - key up
// 3 - touchpad power enable // 3 - key down
// 4 - sd card power enable // 4 - key lock
// 5 - sd mux switch // 5 - display reset
// 6 - LDO enable // 6 - NC
// 7 - charge power ok (active low) // 7 - sd card power (active low)
// All power switches low, sd mux pointing away from us, inputs high. // Default to SD card off, inputs high.
static const uint8_t kPortADefault = 0b10000010; static const uint8_t kPortADefault = 0b10111110;
// Port B: // Port B:
// 0 - 3.5mm jack detect (active low) // 0 - trs output enable
// 1 - unused // 1 - 3.5mm jack detect (active low)
// 2 - volume up // 2 - NC
// 3 - volume down // 3 - NC
// 4 - lock switch // 4 - NC
// 5 - touchpad interupt // 5 - NC
// 6 - display DR // 6 - NC
// 7 - display LED // 7 - NC
// Inputs all high, all others low. // Default input high, trs output low
static const uint8_t kPortBDefault = 0b00111101; static const uint8_t kPortBDefault = 0b00000010;
/* /*
* Convenience mehod for packing the port a and b bytes into a single 16 bit * Convenience mehod for packing the port a and b bytes into a single 16 bit
@ -99,30 +99,30 @@ class GpioExpander {
/* Maps each pin of the expander to its number in a `pack`ed uint16. */ /* Maps each pin of the expander to its number in a `pack`ed uint16. */
enum Pin { enum Pin {
// Port A // Port A
AUDIO_POWER_ENABLE = 0, SD_MUX_SWITCH = 0,
USB_INTERFACE_POWER_ENABLE = 1, SD_MUX_EN_ACTIVE_LOW = 1,
DISPLAY_POWER_ENABLE = 2, KEY_UP = 2,
TOUCHPAD_POWER_ENABLE = 3, KEY_DOWN = 3,
SD_CARD_POWER_ENABLE = 4, KEY_LOCK = 4,
SD_MUX_SWITCH = 5, DISPLAY_RESET = 5,
LDO_ENABLE = 6, // UNUSED = 6,
CHARGE_POWER_OK = 7, // Active-low input SD_CARD_POWER_ENABLE = 7,
// Port B // Port B
PHONE_DETECT = 8, // Active-high input AMP_EN = 8,
// UNUSED = 9, PHONE_DETECT = 9,
VOL_UP = 10, // UNUSED = 10,
VOL_DOWN = 11, // UNUSED = 11,
LOCK = 12, // UNUSED = 12,
TOUCHPAD_INT = 13, // UNUSED = 13,
DISPLAY_DR = 14, // UNUSED = 14,
DISPLAY_LED = 15, // UNUSED = 15,
}; };
/* Nicer value names for use with the SD_MUX_SWITCH pin. */ /* Nicer value names for use with the SD_MUX_SWITCH pin. */
enum SdController { enum SdController {
SD_MUX_ESP = 1, SD_MUX_ESP = 0,
SD_MUX_USB = 0, SD_MUX_SAMD = 1,
}; };
/** /**

@ -35,7 +35,7 @@ class I2CTransaction {
* ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode. * ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode.
* ESP_ERR_TIMEOUT Operation timeout because the bus is busy. * ESP_ERR_TIMEOUT Operation timeout because the bus is busy.
*/ */
esp_err_t Execute(); esp_err_t Execute(uint8_t port = I2C_NUM_0);
/* /*
* Enqueues a start condition. May also be used for repeated start * Enqueues a start condition. May also be used for repeated start

@ -25,14 +25,13 @@ class SdStorage {
FAILED_TO_MOUNT, FAILED_TO_MOUNT,
}; };
static auto create(GpioExpander* gpio) static auto create(GpioExpander* gpio) -> cpp::result<SdStorage*, Error>;
-> cpp::result<std::shared_ptr<SdStorage>, Error>;
SdStorage(GpioExpander* gpio, SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*),
sdspi_dev_handle_t handle_, sdspi_dev_handle_t handle_,
std::unique_ptr<sdmmc_host_t>& host_, std::unique_ptr<sdmmc_host_t> host_,
std::unique_ptr<sdmmc_card_t>& card_, std::unique_ptr<sdmmc_card_t> card_,
FATFS* fs_); FATFS* fs_);
~SdStorage(); ~SdStorage();

@ -0,0 +1,49 @@
#pragma once
#include <stdint.h>
#include <functional>
#include "esp_err.h"
#include "result.hpp"
#include "gpio_expander.hpp"
namespace drivers {
struct TouchWheelData {
bool is_touched = false;
uint8_t wheel_position = -1;
};
class TouchWheel {
public:
TouchWheel();
~TouchWheel();
// Not copyable or movable.
TouchWheel(const TouchWheel&) = delete;
TouchWheel& operator=(const TouchWheel&) = delete;
auto Update() -> void;
auto GetTouchWheelData() const -> TouchWheelData;
private:
TouchWheelData data_;
enum Register {
FIRMWARE_VERSION = 0x1,
DETECTION_STATUS = 0x2,
KEY_STATUS_A = 0x3,
KEY_STATUS_B = 0x4,
SLIDER_POSITION = 0x5,
CALIBRATE = 0x6,
RESET = 0x7,
LOW_POWER = 0x8,
SLIDER_OPTIONS = 0x14,
};
void WriteRegister(uint8_t reg, uint8_t val);
uint8_t ReadRegister(uint8_t reg);
};
} // namespace drivers

@ -49,8 +49,9 @@ static esp_err_t do_transaction(sdspi_dev_handle_t handle,
} }
} // namespace callback } // namespace callback
auto SdStorage::create(GpioExpander* gpio) auto SdStorage::create(GpioExpander* gpio) -> cpp::result<SdStorage*, Error> {
-> cpp::result<std::shared_ptr<SdStorage>, Error> { gpio->set_pin(GpioExpander::SD_CARD_POWER_ENABLE, 0);
gpio->set_pin(GpioExpander::SD_MUX_EN_ACTIVE_LOW, 0);
gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP);
gpio->Write(); gpio->Write();
@ -103,16 +104,16 @@ auto SdStorage::create(GpioExpander* gpio)
return cpp::fail(Error::FAILED_TO_MOUNT); return cpp::fail(Error::FAILED_TO_MOUNT);
} }
return std::make_unique<SdStorage>(gpio, do_transaction, handle, host, card, return new SdStorage(gpio, do_transaction, handle, std::move(host),
fs); std::move(card), fs);
} }
SdStorage::SdStorage(GpioExpander* gpio, SdStorage::SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t, esp_err_t (*do_transaction)(sdspi_dev_handle_t,
sdmmc_command_t*), sdmmc_command_t*),
sdspi_dev_handle_t handle, sdspi_dev_handle_t handle,
std::unique_ptr<sdmmc_host_t>& host, std::unique_ptr<sdmmc_host_t> host,
std::unique_ptr<sdmmc_card_t>& card, std::unique_ptr<sdmmc_card_t> card,
FATFS* fs) FATFS* fs)
: gpio_(gpio), : gpio_(gpio),
do_transaction_(do_transaction), do_transaction_(do_transaction),
@ -136,6 +137,10 @@ SdStorage::~SdStorage() {
// Uninstall the SPI driver // Uninstall the SPI driver
sdspi_host_remove_device(this->handle_); sdspi_host_remove_device(this->handle_);
sdspi_host_deinit(); sdspi_host_deinit();
gpio_->set_pin(GpioExpander::SD_CARD_POWER_ENABLE, 0);
gpio_->set_pin(GpioExpander::SD_MUX_EN_ACTIVE_LOW, 1);
gpio_->Write();
} }
auto SdStorage::HandleTransaction(sdspi_dev_handle_t handle, auto SdStorage::HandleTransaction(sdspi_dev_handle_t handle,

@ -0,0 +1,95 @@
#include "touchwheel.hpp"
#include <stdint.h>
#include <cstdint>
#include "assert.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/projdefs.h"
#include "hal/gpio_types.h"
#include "hal/i2c_types.h"
#include "i2c.hpp"
namespace drivers {
static const char* kTag = "TOUCHWHEEL";
static const uint8_t kTouchWheelAddress = 0x1C;
static const gpio_num_t kIntPin = GPIO_NUM_25;
TouchWheel::TouchWheel() {
gpio_config_t int_config{
.pin_bit_mask = 1ULL << kIntPin,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&int_config);
WriteRegister(Register::RESET, 1);
// TODO(daniel): do we need this? how long does reset take?
vTaskDelay(pdMS_TO_TICKS(1));
WriteRegister(Register::SLIDER_OPTIONS, 0b11000000);
WriteRegister(Register::CALIBRATE, 1);
}
TouchWheel::~TouchWheel() {}
void TouchWheel::WriteRegister(uint8_t reg, uint8_t val) {
// uint8_t maskedReg = reg | kWriteMask;
uint8_t maskedReg = reg;
I2CTransaction transaction;
transaction.start()
.write_addr(kTouchWheelAddress, I2C_MASTER_WRITE)
.write_ack(maskedReg, val)
.stop();
transaction.Execute();
// TODO(jacqueline): check for errors again when i find where all the ffc
// cables went q.q
// ESP_ERROR_CHECK(transaction.Execute());
}
uint8_t TouchWheel::ReadRegister(uint8_t reg) {
uint8_t res;
I2CTransaction transaction;
transaction.start()
.write_addr(kTouchWheelAddress, I2C_MASTER_WRITE)
.write_ack(reg)
.start()
.write_addr(kTouchWheelAddress, I2C_MASTER_READ)
.read(&res, I2C_MASTER_NACK)
.stop();
ESP_ERROR_CHECK(transaction.Execute());
return res;
}
void TouchWheel::Update() {
// Read data from device into member struct
bool has_data = !gpio_get_level(GPIO_NUM_25);
if (!has_data) {
return;
}
uint8_t status = ReadRegister(Register::DETECTION_STATUS);
if (status & 0b10000000) {
// Still calibrating.
return;
}
if (status & 0b10) {
// Slider detect.
data_.wheel_position = ReadRegister(Register::SLIDER_POSITION);
}
if (status & 0b1) {
// Key detect.
// TODO(daniel): implement me
}
}
TouchWheelData TouchWheel::GetTouchWheelData() const {
return data_;
}
} // namespace drivers

@ -1,5 +1,5 @@
idf_component_register( idf_component_register(
SRCS "main.cpp" "app_console.cpp" SRCS "main.cpp" "app_console.cpp"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES "audio" "drivers" "dev_console" "drivers" "database") REQUIRES "audio" "drivers" "dev_console" "drivers" "database" "ui")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -1,30 +1,25 @@
#include <dirent.h> #include <dirent.h>
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include "core/lv_disp.h"
#include "core/lv_obj_pos.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "driver/i2c.h" #include "driver/i2c.h"
#include "driver/sdspi_host.h" #include "driver/sdspi_host.h"
#include "driver/spi_common.h" #include "driver/spi_common.h"
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "driver_cache.hpp"
#include "esp_freertos_hooks.h" #include "esp_freertos_hooks.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_intr_alloc.h" #include "esp_intr_alloc.h"
#include "esp_log.h" #include "esp_log.h"
#include "font/lv_font.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "hal/gpio_types.h" #include "hal/gpio_types.h"
#include "hal/spi_types.h" #include "hal/spi_types.h"
#include "lvgl/lvgl.h"
#include "misc/lv_color.h"
#include "misc/lv_style.h"
#include "misc/lv_timer.h"
#include "widgets/lv_label.h"
#include "app_console.hpp" #include "app_console.hpp"
#include "audio_playback.hpp" #include "audio_playback.hpp"
@ -35,64 +30,14 @@
#include "display_init.hpp" #include "display_init.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "i2c.hpp" #include "i2c.hpp"
#include "lvgl_task.hpp"
#include "spi.hpp" #include "spi.hpp"
#include "storage.hpp" #include "storage.hpp"
#include "touchwheel.hpp"
static const char* TAG = "MAIN"; static const char* TAG = "MAIN";
void IRAM_ATTR tick_hook(void) { void db_main(void *whatever) {
lv_tick_inc(1);
}
static const size_t kLvglStackSize = 8 * 1024;
static StaticTask_t sLvglTaskBuffer = {};
static StackType_t sLvglStack[kLvglStackSize] = {0};
struct LvglArgs {
drivers::GpioExpander* gpio_expander;
};
extern "C" void lvgl_main(void* voidArgs) {
ESP_LOGI(TAG, "starting LVGL task");
LvglArgs* args = (LvglArgs*)voidArgs;
drivers::GpioExpander* gpio_expander = args->gpio_expander;
// Dispose of the args now that we've gotten everything out of them.
delete args;
ESP_LOGI(TAG, "init lvgl");
lv_init();
// LVGL has been initialised, so we can now start reporting ticks to it.
esp_register_freertos_tick_hook(&tick_hook);
ESP_LOGI(TAG, "init display");
std::unique_ptr<drivers::Display> display =
drivers::Display::create(gpio_expander, drivers::displays::kST7735R);
lv_style_t style;
lv_style_init(&style);
lv_style_set_text_color(&style, LV_COLOR_MAKE(0xFF, 0, 0));
// TODO: find a nice bitmap font for this display size and density.
// lv_style_set_text_font(&style, &lv_font_montserrat_24);
auto label = lv_label_create(NULL);
lv_label_set_text(label, "COLOURS!!");
lv_obj_add_style(label, &style, 0);
lv_obj_center(label);
lv_scr_load(label);
while (1) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(10));
}
// TODO: break from the loop to kill this task, so that we can do our RAII
// cleanup, unregister our tick callback and so on.
}
extern "C" void db_main(void *whatever) {
ESP_LOGI(TAG, "Init database"); ESP_LOGI(TAG, "Init database");
auto db_res = database::Database::Open(); auto db_res = database::Database::Open();
if (db_res.has_error()) { if (db_res.has_error()) {
@ -111,30 +56,24 @@ extern "C" void app_main(void) {
ESP_ERROR_CHECK(drivers::init_i2c()); ESP_ERROR_CHECK(drivers::init_i2c());
ESP_ERROR_CHECK(drivers::init_spi()); ESP_ERROR_CHECK(drivers::init_spi());
std::unique_ptr<drivers::DriverCache> drivers =
std::make_unique<drivers::DriverCache>();
ESP_LOGI(TAG, "Init GPIOs"); ESP_LOGI(TAG, "Init GPIOs");
drivers::GpioExpander* expander = new drivers::GpioExpander(); drivers::GpioExpander* expander = drivers->AcquireGpios();
ESP_LOGI(TAG, "Enable power rails for development"); ESP_LOGI(TAG, "Enable power rails for development");
expander->with([&](auto& gpio) { expander->with(
gpio.set_pin(drivers::GpioExpander::AUDIO_POWER_ENABLE, 1); [&](auto& gpio) { gpio.set_pin(drivers::GpioExpander::AMP_EN, 1); });
gpio.set_pin(drivers::GpioExpander::USB_INTERFACE_POWER_ENABLE, 0);
gpio.set_pin(drivers::GpioExpander::SD_CARD_POWER_ENABLE, 1);
gpio.set_pin(drivers::GpioExpander::SD_MUX_SWITCH,
drivers::GpioExpander::SD_MUX_ESP);
});
ESP_LOGI(TAG, "Init battery measurement"); ESP_LOGI(TAG, "Init battery measurement");
drivers::Battery* battery = new drivers::Battery(); drivers::Battery* battery = new drivers::Battery();
ESP_LOGI(TAG, "it's reading %d mV!", (int)battery->Millivolts()); ESP_LOGI(TAG, "it's reading %d mV!", (int)battery->Millivolts());
ESP_LOGI(TAG, "Init SD card"); ESP_LOGI(TAG, "Init SD card");
auto storage_res = drivers::SdStorage::create(expander); auto storage = drivers->AcquireStorage();
std::shared_ptr<drivers::SdStorage> storage; if (!storage) {
if (storage_res.has_error()) {
ESP_LOGE(TAG, "Failed! Do you have an SD card?"); ESP_LOGE(TAG, "Failed! Do you have an SD card?");
} else {
storage = std::move(storage_res.value());
} }
ESP_LOGI(TAG, "Launch database task"); ESP_LOGI(TAG, "Launch database task");
@ -144,22 +83,18 @@ extern "C" void app_main(void) {
reinterpret_cast<StackType_t*>(heap_caps_malloc(db_stack_size, MALLOC_CAP_SPIRAM)); reinterpret_cast<StackType_t*>(heap_caps_malloc(db_stack_size, MALLOC_CAP_SPIRAM));
xTaskCreateStatic(&db_main, "LEVELDB", db_stack_size, NULL, 1, database_stack, &database_task_buffer); xTaskCreateStatic(&db_main, "LEVELDB", db_stack_size, NULL, 1, database_stack, &database_task_buffer);
ESP_LOGI(TAG, "Launch LVGL task"); ESP_LOGI(TAG, "Init touch wheel");
LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs)); std::shared_ptr<drivers::TouchWheel> touchwheel =
lvglArgs->gpio_expander = expander; drivers->AcquireTouchWheel();
xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize,
(void*)lvglArgs, 1, sLvglStack, std::atomic<bool> lvgl_quit;
&sLvglTaskBuffer, 1); TaskHandle_t lvgl_task_handle;
ui::StartLvgl(drivers.get(), &lvgl_quit, &lvgl_task_handle);
std::shared_ptr<audio::AudioPlayback> playback; std::unique_ptr<audio::AudioPlayback> playback;
if (storage) { if (storage) {
ESP_LOGI(TAG, "Init audio pipeline"); ESP_LOGI(TAG, "Init audio pipeline");
auto playback_res = audio::AudioPlayback::create(expander, storage); playback = std::make_unique<audio::AudioPlayback>(drivers.get());
if (playback_res.has_error()) {
ESP_LOGE(TAG, "Failed! Playback will not work.");
} else {
playback = std::move(playback_res.value());
}
} }
ESP_LOGI(TAG, "Waiting for background tasks before launching console..."); ESP_LOGI(TAG, "Waiting for background tasks before launching console...");
@ -169,7 +104,15 @@ extern "C" void app_main(void) {
console::AppConsole console(playback.get()); console::AppConsole console(playback.get());
console.Launch(); console.Launch();
uint8_t prev_position = 0;
while (1) { while (1) {
touchwheel->Update();
auto wheel_data = touchwheel->GetTouchWheelData();
if (wheel_data.wheel_position != prev_position) {
prev_position = wheel_data.wheel_position;
ESP_LOGI(TAG, "Touch wheel pos: %u", prev_position);
}
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
} }

@ -1,2 +1,2 @@
idf_component_register(SRCS "arena.cpp" INCLUDE_DIRS "include" REQUIRES "span") idf_component_register(SRCS "arena.cpp" INCLUDE_DIRS "include" REQUIRES "span" "esp_psram")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -0,0 +1,83 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esp32/himem.h"
#include "span.hpp"
/*
* Wrapper around an ESP-IDF himem allocation, which uses RAII to clean up after
* itself.
*/
template <std::size_t size>
class HimemAlloc {
public:
esp_himem_handle_t handle;
const bool is_valid;
HimemAlloc() : is_valid(esp_himem_alloc(size, &handle) == ESP_OK) {}
~HimemAlloc() {
if (is_valid) {
esp_himem_free(handle);
}
}
// Not copyable or movable.
HimemAlloc(const HimemAlloc&) = delete;
HimemAlloc& operator=(const HimemAlloc&) = delete;
};
/*
* Wrapper around an ESP-IDF himem allocation, which maps a HimemAlloc into the
* usable address space. Instances always contain the last memory region that
* was mapped within them.
*/
template <std::size_t size>
class MappableRegion {
private:
std::byte* bytes_;
public:
esp_himem_rangehandle_t range_handle;
const bool is_valid;
MappableRegion()
: bytes_(nullptr),
is_valid(esp_himem_alloc_map_range(size, &range_handle) == ESP_OK) {}
~MappableRegion() {
if (bytes_ != nullptr) {
esp_himem_unmap(range_handle, bytes_, size);
}
if (is_valid) {
esp_himem_free_map_range(range_handle);
}
}
auto Get() -> cpp::span<std::byte> {
if (bytes_ == nullptr) {
return {};
}
return {bytes_, size};
}
auto Map(const HimemAlloc<size>& alloc) -> cpp::span<std::byte> {
assert(bytes_ == nullptr);
ESP_ERROR_CHECK(esp_himem_map(alloc.handle, range_handle, 0, 0, size, 0,
reinterpret_cast<void**>(&bytes_)));
return Get();
}
auto Unmap() -> void {
if (bytes_ != nullptr) {
ESP_ERROR_CHECK(esp_himem_unmap(range_handle, bytes_, size));
bytes_ = nullptr;
}
}
// Not copyable or movable.
MappableRegion(const MappableRegion&) = delete;
MappableRegion& operator=(const MappableRegion&) = delete;
};

@ -1,4 +1,5 @@
#include "tasks.hpp" #include "tasks.hpp"
const UBaseType_t kTaskPriorityLvgl = 4; const UBaseType_t kTaskPriorityLvgl = 4;
const UBaseType_t kTaskPriorityAudio = 5; const UBaseType_t kTaskPriorityAudioPipeline = 5;
const UBaseType_t kTaskPriorityAudioDrain = 6;

@ -3,4 +3,5 @@
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
extern const UBaseType_t kTaskPriorityLvgl; extern const UBaseType_t kTaskPriorityLvgl;
extern const UBaseType_t kTaskPriorityAudio; extern const UBaseType_t kTaskPriorityAudioPipeline;
extern const UBaseType_t kTaskPriorityAudioDrain;

@ -0,0 +1,5 @@
idf_component_register(
SRCS "lvgl_task.cpp"
INCLUDE_DIRS "include"
REQUIRES "drivers")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -0,0 +1,17 @@
#pragma once
#include <atomic>
#include <cstdbool>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver_cache.hpp"
namespace ui {
auto StartLvgl(drivers::DriverCache* drivers,
std::atomic<bool>* quit,
TaskHandle_t* handle) -> bool;
} // namespace ui

@ -0,0 +1,108 @@
#include "lvgl_task.hpp"
#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include "core/lv_disp.h"
#include "core/lv_obj.h"
#include "core/lv_obj_pos.h"
#include "core/lv_obj_tree.h"
#include "esp_log.h"
#include "font/lv_font.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "freertos/timers.h"
#include "hal/gpio_types.h"
#include "hal/spi_types.h"
#include "lvgl/lvgl.h"
#include "misc/lv_color.h"
#include "misc/lv_style.h"
#include "misc/lv_timer.h"
#include "widgets/lv_label.h"
#include "display.hpp"
#include "driver_cache.hpp"
#include "gpio_expander.hpp"
namespace ui {
static const char* kTag = "lv_task";
auto tick_hook(TimerHandle_t xTimer) -> void {
lv_tick_inc(1);
}
struct LvglArgs {
drivers::DriverCache* drivers;
std::atomic<bool>* quit;
};
void LvglMain(void* voidArgs) {
LvglArgs* args = reinterpret_cast<LvglArgs*>(voidArgs);
drivers::DriverCache* drivers = args->drivers;
std::atomic<bool>* quit = args->quit;
delete args;
{
ESP_LOGI(kTag, "init lvgl");
lv_init();
// LVGL has been initialised, so we can now start reporting ticks to it.
TimerHandle_t tick_timer =
xTimerCreate("lv_tick", pdMS_TO_TICKS(1), pdTRUE, NULL, &tick_hook);
ESP_LOGI(kTag, "init display");
std::shared_ptr<drivers::Display> display = drivers->AcquireDisplay();
lv_style_t style;
lv_style_init(&style);
lv_style_set_text_color(&style, LV_COLOR_MAKE(0xFF, 0, 0));
// TODO: find a nice bitmap font for this display size and density.
// lv_style_set_text_font(&style, &lv_font_montserrat_24);
auto label = lv_label_create(NULL);
lv_label_set_text(label, "COLOURS!!");
lv_obj_add_style(label, &style, 0);
lv_obj_center(label);
lv_scr_load(label);
while (!quit->load()) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(10));
}
// TODO(robin? daniel?): De-init the UI stack here.
lv_obj_del(label);
lv_style_reset(&style);
xTimerDelete(tick_timer, portMAX_DELAY);
lv_deinit();
}
vTaskDelete(NULL);
}
static const size_t kLvglStackSize = 8 * 1024;
static StaticTask_t sLvglTaskBuffer = {};
static StackType_t sLvglStack[kLvglStackSize] = {0};
auto StartLvgl(drivers::DriverCache* drivers,
std::atomic<bool>* quit,
TaskHandle_t* handle) -> bool {
LvglArgs* args = new LvglArgs();
args->drivers = drivers;
args->quit = quit;
return xTaskCreateStaticPinnedToCore(&LvglMain, "LVGL", kLvglStackSize,
reinterpret_cast<void*>(args), 1,
sLvglStack, &sLvglTaskBuffer, 1);
}
} // namespace ui
Loading…
Cancel
Save