summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-04-26 08:49:02 +1000
committerjacqueline <me@jacqueline.id.au>2023-04-26 08:49:02 +1000
commit7972bd4567a99179338259e9e6ce19168c2c0db3 (patch)
treef46642afd36011d3d064e022232e77744b82c6ae /src
parent4887f3789817f87bf1272af0b52684e3364270c2 (diff)
parent5575378c1c8171cd716b79d3ab89df1e56ceb9d3 (diff)
downloadtangara-fw-7972bd4567a99179338259e9e6ce19168c2c0db3.tar.gz
Merge branch 'main' into leveldb
Diffstat (limited to 'src')
-rw-r--r--src/audio/CMakeLists.txt2
-rw-r--r--src/audio/audio_decoder.cpp141
-rw-r--r--src/audio/audio_element.cpp56
-rw-r--r--src/audio/audio_playback.cpp61
-rw-r--r--src/audio/audio_task.cpp249
-rw-r--r--src/audio/fatfs_audio_input.cpp94
-rw-r--r--src/audio/i2s_audio_output.cpp102
-rw-r--r--src/audio/include/audio_decoder.hpp23
-rw-r--r--src/audio/include/audio_element.hpp62
-rw-r--r--src/audio/include/audio_playback.hpp18
-rw-r--r--src/audio/include/audio_sink.hpp44
-rw-r--r--src/audio/include/audio_task.hpp25
-rw-r--r--src/audio/include/fatfs_audio_input.hpp18
-rw-r--r--src/audio/include/i2s_audio_output.hpp27
-rw-r--r--src/audio/include/pipeline.hpp43
-rw-r--r--src/audio/include/stream_info.hpp91
-rw-r--r--src/audio/pipeline.cpp57
-rw-r--r--src/audio/stream_info.cpp70
-rw-r--r--src/codecs/codec.cpp2
-rw-r--r--src/codecs/include/codec.hpp7
-rw-r--r--src/codecs/include/mad.hpp4
-rw-r--r--src/codecs/mad.cpp31
-rw-r--r--src/drivers/CMakeLists.txt4
-rw-r--r--src/drivers/battery.cpp4
-rw-r--r--src/drivers/dac.cpp341
-rw-r--r--src/drivers/display.cpp43
-rw-r--r--src/drivers/driver_cache.cpp43
-rw-r--r--src/drivers/i2c.cpp11
-rw-r--r--src/drivers/include/dac.hpp127
-rw-r--r--src/drivers/include/display.hpp3
-rw-r--r--src/drivers/include/driver_cache.hpp54
-rw-r--r--src/drivers/include/gpio_expander.hpp76
-rw-r--r--src/drivers/include/i2c.hpp2
-rw-r--r--src/drivers/include/storage.hpp7
-rw-r--r--src/drivers/include/touchwheel.hpp49
-rw-r--r--src/drivers/storage.cpp17
-rw-r--r--src/drivers/touchwheel.cpp95
-rw-r--r--src/main/CMakeLists.txt2
-rw-r--r--src/main/main.cpp117
-rw-r--r--src/memory/CMakeLists.txt2
-rw-r--r--src/memory/include/himem.hpp83
-rw-r--r--src/tasks/tasks.cpp3
-rw-r--r--src/tasks/tasks.hpp3
-rw-r--r--src/ui/CMakeLists.txt5
-rw-r--r--src/ui/include/lvgl_task.hpp17
-rw-r--r--src/ui/lvgl_task.cpp108
46 files changed, 1621 insertions, 822 deletions
diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index d4c89214..6361f827 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.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"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory")
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp
index 9879b042..4b9826a9 100644
--- a/src/audio/audio_decoder.cpp
+++ b/src/audio/audio_decoder.cpp
@@ -2,13 +2,17 @@
#include <string.h>
+#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
+#include <variant>
+#include "cbor/tinycbor/src/cborinternal_p.h"
#include "freertos/FreeRTOS.h"
#include "esp_heap_caps.h"
+#include "esp_log.h"
#include "freertos/message_buffer.h"
#include "freertos/portmacro.h"
@@ -21,128 +25,113 @@ namespace audio {
static const char* kTag = "DEC";
-static const std::size_t kChunkSize = 1024;
-static const std::size_t kReadahead = 8;
-
AudioDecoder::AudioDecoder()
: IAudioElement(),
- arena_(kChunkSize, kReadahead, MALLOC_CAP_SPIRAM),
- stream_info_({}),
- has_samples_to_send_(false),
- needs_more_input_(true) {}
+ current_codec_(),
+ current_input_format_(),
+ current_output_format_(),
+ has_samples_to_send_(false) {}
AudioDecoder::~AudioDecoder() {}
-auto AudioDecoder::HasUnprocessedInput() -> bool {
- return !needs_more_input_ || has_samples_to_send_;
-}
-
-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;
+auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
+ if (!std::holds_alternative<StreamInfo::Encoded>(info.format)) {
+ return false;
}
+ 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,
// since we can potentially just continue to decode as we were before,
// without any setup overhead.
if (current_codec_ != nullptr &&
- current_codec_->CanHandleFile(info.path.value_or(""))) {
+ current_codec_->CanHandleType(encoded.type)) {
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()) {
+ ESP_LOGI(kTag, "creating new decoder");
current_codec_ = std::move(result.value());
} else {
ESP_LOGE(kTag, "no codec for this file");
- return;
+ return false;
}
- stream_info_ = info;
- has_sent_stream_info_ = false;
+ return true;
}
-auto AudioDecoder::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {
- if (current_codec_ == nullptr || !chunk_reader_) {
- // Should never happen, but fail explicitly anyway.
- ESP_LOGW(kTag, "received chunk without chunk size or codec");
+auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
+ OutputStream* output) -> void {
+ auto input = inputs.begin();
+ 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;
}
- ESP_LOGI(kTag, "received new chunk (size %u)", chunk.size());
- current_codec_->SetInput(chunk_reader_->HandleNewData(chunk));
- needs_more_input_ = false;
-}
+ if (!current_input_format_ || *current_input_format_ != info.format) {
+ // The input stream has changed! Immediately throw everything away and
+ // start from scratch.
+ current_input_format_ = info.format;
+ has_samples_to_send_ = false;
-auto AudioDecoder::ProcessEndOfStream() -> void {
- has_samples_to_send_ = false;
- needs_more_input_ = true;
- current_codec_.reset();
+ ProcessStreamInfo(info);
+ }
- SendOrBufferEvent(std::unique_ptr<StreamEvent>(
- StreamEvent::CreateEndOfStream(input_events_)));
-}
+ current_codec_->SetInput(input->data());
-auto AudioDecoder::Process() -> void {
- if (has_samples_to_send_) {
- // Writing samples is relatively quick (it's just a bunch of memcopy's), so
- // do them all at once.
- while (has_samples_to_send_ && !IsOverBuffered()) {
- if (!has_sent_stream_info_) {
- has_sent_stream_info_ = true;
+ while (true) {
+ if (has_samples_to_send_) {
+ if (!current_output_format_) {
auto format = current_codec_->GetOutputFormat();
- stream_info_->bits_per_sample = format.bits_per_sample;
- stream_info_->sample_rate = format.sample_rate_hz;
- stream_info_->channels = format.num_channels;
- stream_info_->chunk_size = kChunkSize;
-
- auto event =
- StreamEvent::CreateStreamInfo(input_events_, *stream_info_);
- SendOrBufferEvent(std::unique_ptr<StreamEvent>(event));
+ current_output_format_ = StreamInfo::Pcm{
+ .channels = format.num_channels,
+ .bits_per_sample = format.bits_per_sample,
+ .sample_rate = format.sample_rate_hz,
+ };
}
- auto block = arena_.Acquire();
- if (!block) {
- return;
+ if (!output->prepare(*current_output_format_)) {
+ break;
}
- auto write_res =
- current_codec_->WriteOutputSamples({block->start, block->size});
- block->used_size = write_res.first;
+ auto write_res = current_codec_->WriteOutputSamples(output->data());
+ output->add(write_res.first);
has_samples_to_send_ = !write_res.second;
- auto chunk = std::unique_ptr<StreamEvent>(
- StreamEvent::CreateArenaChunk(input_events_, *block));
- if (!SendOrBufferEvent(std::move(chunk))) {
- return;
+ if (has_samples_to_send_) {
+ // We weren't able to fit all the generated samples into the output
+ // buffer. Stop trying; we'll finish up during the next pass.
+ break;
}
}
- // We will process the next frame during the next call to this method.
- }
- if (!needs_more_input_) {
auto res = current_codec_->ProcessNextFrame();
if (res.has_error()) {
// TODO(jacqueline): Handle errors.
return;
}
- needs_more_input_ = res.value();
- has_samples_to_send_ = true;
- if (needs_more_input_) {
- chunk_reader_->HandleBytesUsed(current_codec_->GetInputPosition());
+ if (res.value()) {
+ // 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);
+ }
}
} // namespace audio
diff --git a/src/audio/audio_element.cpp b/src/audio/audio_element.cpp
deleted file mode 100644
index cef54631..00000000
--- a/src/audio/audio_element.cpp
+++ /dev/null
@@ -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
diff --git a/src/audio/audio_playback.cpp b/src/audio/audio_playback.cpp
index c95a5d63..c51e41fb 100644
--- a/src/audio/audio_playback.cpp
+++ b/src/audio/audio_playback.cpp
@@ -5,71 +5,46 @@
#include <memory>
#include <string_view>
+#include "driver_cache.hpp"
#include "freertos/portmacro.h"
#include "audio_decoder.hpp"
+#include "audio_element.hpp"
#include "audio_task.hpp"
#include "chunk.hpp"
#include "fatfs_audio_input.hpp"
#include "gpio_expander.hpp"
#include "i2s_audio_output.hpp"
+#include "pipeline.hpp"
#include "storage.hpp"
#include "stream_buffer.hpp"
#include "stream_info.hpp"
#include "stream_message.hpp"
namespace audio {
-
-auto AudioPlayback::create(drivers::GpioExpander* expander,
- std::shared_ptr<drivers::SdStorage> storage)
- -> cpp::result<std::unique_ptr<AudioPlayback>, Error> {
- // Create everything
- auto source = std::make_shared<FatfsAudioInput>(storage);
- auto codec = std::make_shared<AudioDecoder>();
-
- auto sink_res = I2SAudioOutput::create(expander);
- if (sink_res.has_error()) {
- return cpp::fail(ERR_INIT_ELEMENT);
- }
- 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(drivers::DriverCache* drivers)
+ : file_source_(std::make_unique<FatfsAudioInput>()),
+ i2s_output_(std::make_unique<I2SAudioOutput>(drivers->AcquireGpios(),
+ drivers->AcquireDac())) {
+ AudioDecoder* codec = new AudioDecoder();
+ elements_.emplace_back(codec);
+
+ Pipeline* pipeline = new Pipeline(elements_.front().get());
+ pipeline->AddInput(file_source_.get());
+
+ task::StartPipeline(pipeline, i2s_output_.get());
+ // task::StartDrain(i2s_output_.get());
}
-AudioPlayback::AudioPlayback() {}
-
AudioPlayback::~AudioPlayback() {}
auto AudioPlayback::Play(const std::string& filename) -> void {
- StreamInfo info;
- info.path = 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);
+ // TODO: concurrency, yo!
+ file_source_->OpenFile(filename);
}
auto AudioPlayback::LogStatus() -> void {
- auto event = StreamEvent::CreateLogStatus();
- xQueueSendToFront(input_handle_, &event, portMAX_DELAY);
-}
-
-auto AudioPlayback::ConnectElements(IAudioElement* src, IAudioElement* sink)
- -> void {
- src->OutputEventQueue(sink->InputEventQueue());
+ i2s_output_->Log();
}
} // namespace audio
diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp
index ce6d724e..b2a8062e 100644
--- a/src/audio/audio_task.cpp
+++ b/src/audio/audio_task.cpp
@@ -2,16 +2,23 @@
#include <stdlib.h>
+#include <algorithm>
+#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
+#include <variant>
+#include "audio_sink.hpp"
#include "cbor.h"
+#include "dac.hpp"
+#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "freertos/queue.h"
+#include "pipeline.hpp"
#include "span.hpp"
#include "arena.hpp"
@@ -25,25 +32,32 @@
namespace audio {
+namespace 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,
- std::optional<BaseType_t> core_id,
- std::shared_ptr<IAudioElement> element) -> void {
- auto task_handle = std::make_unique<TaskHandle_t>();
+ ESP_LOGI(kTag, "starting audio pipeline task");
+ xTaskCreatePinnedToCore(&AudioTaskMain, "pipeline", kStackSize, args,
+ kTaskPriorityAudioPipeline, NULL, 1);
+}
+auto StartDrain(IAudioSink* sink) -> void {
+ auto command = new std::atomic<Command>(PLAY);
// Newly created task will free this.
- AudioTaskArgs* args = new AudioTaskArgs{.element = element};
-
- ESP_LOGI(kTag, "starting audio task %s", name.c_str());
- if (core_id) {
- xTaskCreatePinnedToCore(&AudioTaskMain, name.c_str(),
- element->StackSizeBytes(), args, kTaskPriorityAudio,
- task_handle.get(), *core_id);
- } else {
- xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args,
- kTaskPriorityAudio, task_handle.get());
- }
+ AudioDrainArgs* drain_args = new AudioDrainArgs{
+ .sink = sink,
+ .command = command,
+ };
+
+ ESP_LOGI(kTag, "starting audio drain task");
+ xTaskCreate(&AudioDrainMain, "drain", kDrainStackSize, drain_args,
+ kTaskPriorityAudioDrain, NULL);
}
void AudioTaskMain(void* args) {
@@ -51,118 +65,129 @@ void AudioTaskMain(void* args) {
// called before the task quits.
{
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;
- // Queue of events that we have received on our input queue, but not yet
- // processed.
- std::deque<std::unique_ptr<StreamEvent>> pending_events;
-
- // TODO(jacqueline): quit event
- while (true) {
- // First, we pull events from our input queue into pending_events. This
- // keeps us responsive to any events that need to be handled immediately.
- // Then we check if there's any events to flush downstream.
- // Then we pass anything requiring processing to the element.
-
- bool has_work_to_do =
- (!pending_events.empty() || element->HasUnflushedOutput() ||
- element->HasUnprocessedInput()) &&
- !element->IsOverBuffered();
-
- if (has_work_to_do) {
- ESP_LOGD(kTag, "checking for events");
- } else {
- ESP_LOGD(kTag, "waiting for events");
- }
-
- // If we have no new events to process and the element has nothing left to
- // do, then just delay forever waiting for a new event.
- TickType_t ticks_to_wait = has_work_to_do ? 0 : portMAX_DELAY;
-
- StreamEvent* new_event = nullptr;
- bool has_event =
- xQueueReceive(element->InputEventQueue(), &new_event, ticks_to_wait);
-
- if (has_event) {
- if (new_event->tag == StreamEvent::UNINITIALISED) {
- ESP_LOGE(kTag, "discarding invalid event!!");
- } else if (new_event->tag == StreamEvent::CHUNK_NOTIFICATION) {
- delete new_event;
- } else if (new_event->tag == StreamEvent::LOG_STATUS) {
- element->ProcessLogStatus();
- if (element->OutputEventQueue() != nullptr) {
- xQueueSendToFront(element->OutputEventQueue(), &new_event, 0);
- } else {
- delete new_event;
- }
- } else {
- // This isn't an event that needs to be actioned immediately. Add it
- // to our work queue.
- pending_events.emplace_back(new_event);
- ESP_LOGD(kTag, "deferring event");
+ std::optional<StreamInfo::Format> output_format;
+
+ std::vector<Pipeline*> elements = pipeline->GetIterationOrder();
+ std::size_t max_inputs =
+ (*std::max_element(elements.begin(), elements.end(),
+ [](Pipeline const* first, Pipeline const* second) {
+ return first->NumInputs() < second->NumInputs();
+ }))
+ ->NumInputs();
+
+ // We need to be able to simultaneously map all of an element's inputs, plus
+ // its output. So preallocate that many ranges.
+ std::vector<MappableRegion<kPipelineBufferSize>> in_regions(max_inputs);
+ MappableRegion<kPipelineBufferSize> out_region;
+ std::for_each(in_regions.begin(), in_regions.end(),
+ [](const auto& region) { assert(region.is_valid); });
+ assert(out_region.is_valid);
+
+ // Each element has exactly one output buffer.
+ std::vector<HimemAlloc<kPipelineBufferSize>> buffers(elements.size());
+ std::vector<StreamInfo> buffer_infos(buffers.size());
+ std::for_each(buffers.begin(), buffers.end(),
+ [](const HimemAlloc<kPipelineBufferSize>& alloc) {
+ assert(alloc.is_valid);
+ });
+
+ bool playing = true;
+ bool quit = false;
+ while (!quit) {
+ if (playing) {
+ for (int i = 0; i < elements.size(); i++) {
+ std::vector<RawStream> raw_in_streams;
+ elements.at(i)->InStreams(&in_regions, &raw_in_streams);
+ RawStream raw_out_stream = elements.at(i)->OutStream(&out_region);
+
+ // Crop the input and output streams to the ranges that are safe to
+ // touch. For the input streams, this is the region that contains
+ // data. For the output stream, this is the region that does *not*
+ // already contain data.
+ std::vector<InputStream> in_streams;
+ std::for_each(raw_in_streams.begin(), raw_in_streams.end(),
+ [&](RawStream& s) { in_streams.emplace_back(&s); });
+ OutputStream out_stream(&raw_out_stream);
+
+ elements.at(i)->OutputElement()->Process(in_streams, &out_stream);
+
+ std::for_each(in_regions.begin(), in_regions.end(),
+ [](auto&& r) { r.Unmap(); });
+ out_region.Unmap();
}
- // Loop again, so that we service all incoming events before doing our
- // possibly expensive processing.
- continue;
- }
-
- if (element->HasUnflushedOutput()) {
- ESP_LOGD(kTag, "flushing output");
- }
- // We have no new events. Next, see if there's anything that needs to be
- // flushed.
- 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));
- continue;
- }
+ RawStream raw_sink_stream = elements.front()->OutStream(&out_region);
+ InputStream sink_stream(&raw_sink_stream);
- if (element->HasUnprocessedInput()) {
- ESP_LOGD(kTag, "processing input events");
- element->Process();
- continue;
- }
-
- // The element ran out of data, so now it's time to let it process more
- // input.
- while (!pending_events.empty()) {
- std::unique_ptr<StreamEvent> event;
- pending_events.front().swap(event);
- pending_events.pop_front();
- ESP_LOGD(kTag, "processing event, tag %i", event->tag);
+ if (sink_stream.info().bytes_in_stream == 0) {
+ out_region.Unmap();
+ vTaskDelay(pdMS_TO_TICKS(100));
+ continue;
+ }
- if (event->tag == StreamEvent::STREAM_INFO) {
- ESP_LOGD(kTag, "processing stream info");
+ if (!output_format || output_format != sink_stream.info().format) {
+ // The format of the stream within the sink stream has changed. We
+ // need to reconfigure the sink, but shouldn't do so until we've fully
+ // drained the current buffer.
+ if (xStreamBufferIsEmpty(sink->buffer())) {
+ ESP_LOGI(kTag, "reconfiguring dac");
+ output_format = sink_stream.info().format;
+ sink->Configure(*output_format);
+ }
+ }
- element->ProcessStreamInfo(*event->stream_info);
+ // We've reconfigured the sink, or it was already configured correctly.
+ // Send through some data.
+ if (output_format == sink_stream.info().format &&
+ !std::holds_alternative<std::monostate>(*output_format)) {
+ // TODO: tune the delay on this, as it's currently the only way to
+ // throttle this task's CPU time. Maybe also hold off on the pipeline
+ // if the buffer is already close to full?
+ std::size_t sent = xStreamBufferSend(
+ sink->buffer(), sink_stream.data().data(),
+ sink_stream.data().size_bytes(), pdMS_TO_TICKS(10));
+ if (sent > 0) {
+ ESP_LOGI(kTag, "sunk %u bytes out of %u (%d %%)", sent,
+ sink_stream.info().bytes_in_stream,
+ (int)(((float)sent /
+ (float)sink_stream.info().bytes_in_stream) *
+ 100));
+ }
+ sink_stream.consume(sent);
+ }
- } else if (event->tag == StreamEvent::ARENA_CHUNK) {
- ESP_LOGD(kTag, "processing arena data");
+ out_region.Unmap();
+ }
+ }
+ }
+ vTaskDelete(NULL);
+}
- memory::ArenaRef ref(event->arena_chunk);
- auto callback =
- StreamEvent::CreateChunkNotification(element->InputEventQueue());
- if (!xQueueSend(event->source, &callback, 0)) {
- ESP_LOGW(kTag, "failed to send chunk notif");
- continue;
- }
+static std::byte sDrainBuf[8 * 1024];
- // TODO(jacqueline): Consider giving the element a full ArenaRef here,
- // so that it can hang on to it and potentially save an alloc+copy.
- element->ProcessChunk({ref.ptr.start, ref.ptr.used_size});
+void AudioDrainMain(void* args) {
+ {
+ AudioDrainArgs* real_args = reinterpret_cast<AudioDrainArgs*>(args);
+ IAudioSink* sink = real_args->sink;
+ std::atomic<Command>* command = real_args->command;
+ delete real_args;
- // TODO: think about whether to do the whole queue
- break;
- }
+ // 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);
}
+} // namespace task
+
} // namespace audio
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 5354c5fd..22d707d6 100644
--- a/src/audio/fatfs_audio_input.cpp
+++ b/src/audio/fatfs_audio_input.cpp
@@ -4,9 +4,12 @@
#include <cstdint>
#include <memory>
#include <string>
+#include <variant>
#include "arena.hpp"
#include "esp_heap_caps.h"
+#include "esp_log.h"
+#include "ff.h"
#include "freertos/portmacro.h"
#include "audio_element.hpp"
@@ -15,43 +18,23 @@
#include "stream_event.hpp"
#include "stream_info.hpp"
#include "stream_message.hpp"
+#include "types.hpp"
static const char* kTag = "SRC";
namespace audio {
-static const std::size_t kChunkSize = 24 * 1024;
-static const std::size_t kChunkReadahead = 2;
-
-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()
+ : IAudioElement(), current_file_(), is_file_open_(false) {}
FatfsAudioInput::~FatfsAudioInput() {}
-auto FatfsAudioInput::HasUnprocessedInput() -> bool {
- return is_file_open_;
-}
-
-auto FatfsAudioInput::IsOverBuffered() -> bool {
- return arena_.BlocksFree() == 0;
-}
-
-auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
+auto FatfsAudioInput::OpenFile(const std::string& path) -> void {
if (is_file_open_) {
f_close(&current_file_);
is_file_open_ = false;
}
-
- if (!info.path) {
- // TODO(jacqueline): Handle errors.
- return;
- }
- ESP_LOGI(kTag, "opening file %s", info.path->c_str());
- std::string path = *info.path;
+ ESP_LOGI(kTag, "opening file %s", path.c_str());
FRESULT res = f_open(&current_file_, path.c_str(), FA_READ);
if (res != FR_OK) {
ESP_LOGE(kTag, "failed to open file! res: %i", res);
@@ -60,51 +43,36 @@ auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
}
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::ProcessEndOfStream() -> void {
- if (is_file_open_) {
- f_close(&current_file_);
- is_file_open_ = false;
- SendOrBufferEvent(std::unique_ptr<StreamEvent>(
- StreamEvent::CreateEndOfStream(input_events_)));
+auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
+ OutputStream* output) -> void {
+ if (!is_file_open_) {
+ // TODO(jacqueline): should we clear the stream format?
+ // output->prepare({});
+ return;
}
-}
-auto FatfsAudioInput::Process() -> void {
- if (is_file_open_) {
- auto dest_block = memory::ArenaRef::Acquire(&arena_);
- if (!dest_block) {
- return;
- }
-
- FRESULT result = f_read(&current_file_, dest_block->ptr.start,
- dest_block->ptr.size, &dest_block->ptr.used_size);
- if (result != FR_OK) {
- ESP_LOGE(kTag, "file I/O error %d", result);
- // TODO(jacqueline): Handle errors.
- return;
- }
+ StreamInfo::Format format = StreamInfo::Encoded{codecs::STREAM_MP3};
+ if (!output->prepare(format)) {
+ return;
+ }
- if (dest_block->ptr.used_size < dest_block->ptr.size ||
- f_eof(&current_file_)) {
- f_close(&current_file_);
- is_file_open_ = false;
- }
+ std::size_t max_size = output->data().size_bytes();
+ std::size_t size = 0;
+ FRESULT result =
+ f_read(&current_file_, output->data().data(), max_size, &size);
+ if (result != FR_OK) {
+ ESP_LOGE(kTag, "file I/O error %d", result);
+ // TODO(jacqueline): Handle errors.
+ return;
+ }
- auto dest_event = std::unique_ptr<StreamEvent>(
- StreamEvent::CreateArenaChunk(input_events_, dest_block->Release()));
+ output->add(size);
- SendOrBufferEvent(std::move(dest_event));
+ if (size < max_size || f_eof(&current_file_)) {
+ f_close(&current_file_);
+ is_file_open_ = false;
}
}
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
index 110227cf..8c374351 100644
--- a/src/audio/i2s_audio_output.cpp
+++ b/src/audio/i2s_audio_output.cpp
@@ -1,6 +1,9 @@
#include "i2s_audio_output.hpp"
#include <algorithm>
+#include <cstddef>
+#include <memory>
+#include <variant>
#include "esp_err.h"
#include "freertos/portmacro.h"
@@ -10,68 +13,41 @@
#include "freertos/projdefs.h"
#include "gpio_expander.hpp"
#include "result.hpp"
+#include "stream_info.hpp"
-static const TickType_t kIdleTimeBeforeMute = pdMS_TO_TICKS(1000);
static const char* kTag = "I2SOUT";
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,
- std::unique_ptr<drivers::AudioDac> dac)
- : expander_(expander),
- dac_(std::move(dac)),
- chunk_reader_(),
- latest_chunk_() {}
-
-I2SAudioOutput::~I2SAudioOutput() {}
-
-auto I2SAudioOutput::HasUnprocessedInput() -> bool {
- return latest_chunk_.size() > 0;
+ std::shared_ptr<drivers::AudioDac> dac)
+ : expander_(expander), dac_(std::move(dac)), current_config_() {
+ dac_->WriteVolume(127); // for testing
+ dac_->SetSource(buffer());
}
-auto I2SAudioOutput::IsOverBuffered() -> bool {
- return false;
+I2SAudioOutput::~I2SAudioOutput() {
+ dac_->SetSource(nullptr);
}
-auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
- // TODO(jacqueline): probs do something with the channel hey
-
- if (!info.bits_per_sample || !info.sample_rate) {
- ESP_LOGE(kTag, "audio stream missing bits or sample rate");
- return;
+auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
+ if (!std::holds_alternative<StreamInfo::Pcm>(format)) {
+ ESP_LOGI(kTag, "ignoring non-pcm stream (%d)", format.index());
+ return false;
}
- if (!info.chunk_size) {
- ESP_LOGE(kTag, "audio stream missing chunk size");
- return;
+ StreamInfo::Pcm pcm = std::get<StreamInfo::Pcm>(format);
+
+ 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,
- *info.sample_rate);
+ ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample,
+ pcm.sample_rate);
drivers::AudioDac::BitsPerSample bps;
- switch (*info.bits_per_sample) {
+ switch (pcm.bits_per_sample) {
case 16:
bps = drivers::AudioDac::BPS_16;
break;
@@ -83,11 +59,11 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
break;
default:
ESP_LOGE(kTag, "dropping stream with unknown bps");
- return;
+ return false;
}
drivers::AudioDac::SampleRate sample_rate;
- switch (*info.sample_rate) {
+ switch (pcm.sample_rate) {
case 44100:
sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
break;
@@ -96,39 +72,25 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
break;
default:
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);
-}
+ current_config_ = pcm;
-auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {
- latest_chunk_ = chunk_reader_->HandleNewData(chunk);
+ return true;
}
-auto I2SAudioOutput::ProcessEndOfStream() -> void {
- dac_->Stop();
- SendOrBufferEvent(std::unique_ptr<StreamEvent>(
- StreamEvent::CreateEndOfStream(input_events_)));
+auto I2SAudioOutput::Send(const cpp::span<std::byte>& data) -> void {
+ dac_->WriteData(data);
}
-auto I2SAudioOutput::ProcessLogStatus() -> void {
+auto I2SAudioOutput::Log() -> void {
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 {
dac_->WriteVolume(volume);
}
diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp
index 47642469..6a1b5177 100644
--- a/src/audio/include/audio_decoder.hpp
+++ b/src/audio/include/audio_decoder.hpp
@@ -3,6 +3,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
+#include <vector>
#include "chunk.hpp"
#include "ff.h"
@@ -10,6 +11,7 @@
#include "audio_element.hpp"
#include "codec.hpp"
+#include "stream_info.hpp"
namespace audio {
@@ -22,28 +24,19 @@ class AudioDecoder : public IAudioElement {
AudioDecoder();
~AudioDecoder();
- auto StackSizeBytes() const -> std::size_t override { return 10 * 1024; };
-
- 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;
+ auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
+ -> void override;
AudioDecoder(const AudioDecoder&) = delete;
AudioDecoder& operator=(const AudioDecoder&) = delete;
private:
- memory::Arena arena_;
std::unique_ptr<codecs::ICodec> current_codec_;
- std::optional<StreamInfo> stream_info_;
- std::optional<ChunkReader> chunk_reader_;
-
- bool has_sent_stream_info_;
+ std::optional<StreamInfo::Format> current_input_format_;
+ std::optional<StreamInfo::Format> current_output_format_;
bool has_samples_to_send_;
- bool needs_more_input_;
+
+ auto ProcessStreamInfo(const StreamInfo& info) -> bool;
};
} // namespace audio
diff --git a/src/audio/include/audio_element.hpp b/src/audio/include/audio_element.hpp
index 91036348..5884f7b2 100644
--- a/src/audio/include/audio_element.hpp
+++ b/src/audio/include/audio_element.hpp
@@ -37,65 +37,11 @@ static const size_t kEventQueueSize = 8;
*/
class IAudioElement {
public:
- IAudioElement();
- virtual ~IAudioElement();
+ IAudioElement() {}
+ virtual ~IAudioElement() {}
- /*
- * Returns the stack size in bytes that this element requires. This should
- * 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_;
+ virtual auto Process(const std::vector<InputStream>& inputs,
+ OutputStream* output) -> void = 0;
};
} // namespace audio
diff --git a/src/audio/include/audio_playback.hpp b/src/audio/include/audio_playback.hpp
index a1bea64f..cd4be3e7 100644
--- a/src/audio/include/audio_playback.hpp
+++ b/src/audio/include/audio_playback.hpp
@@ -5,7 +5,11 @@
#include <string>
#include <vector>
+#include "audio_task.hpp"
+#include "driver_cache.hpp"
#include "esp_err.h"
+#include "fatfs_audio_input.hpp"
+#include "i2s_audio_output.hpp"
#include "result.hpp"
#include "span.hpp"
@@ -22,13 +26,7 @@ namespace audio {
*/
class AudioPlayback {
public:
- enum Error { ERR_INIT_ELEMENT, ERR_MEM };
- 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();
+ explicit AudioPlayback(drivers::DriverCache* drivers);
~AudioPlayback();
/*
@@ -44,9 +42,9 @@ class AudioPlayback {
AudioPlayback& operator=(const AudioPlayback&) = delete;
private:
- auto ConnectElements(IAudioElement* src, IAudioElement* sink) -> void;
-
- QueueHandle_t input_handle_;
+ std::unique_ptr<FatfsAudioInput> file_source_;
+ std::unique_ptr<I2SAudioOutput> i2s_output_;
+ std::vector<std::unique_ptr<IAudioElement>> elements_;
};
} // namespace audio
diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp
new file mode 100644
index 00000000..7a535c35
--- /dev/null
+++ b/src/audio/include/audio_sink.hpp
@@ -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
diff --git a/src/audio/include/audio_task.hpp b/src/audio/include/audio_task.hpp
index df70ebaa..a7b7a0fa 100644
--- a/src/audio/include/audio_task.hpp
+++ b/src/audio/include/audio_task.hpp
@@ -5,18 +5,33 @@
#include <string>
#include "audio_element.hpp"
+#include "audio_sink.hpp"
+#include "dac.hpp"
#include "freertos/portmacro.h"
+#include "pipeline.hpp"
+#include "stream_buffer.hpp"
namespace audio {
+namespace task {
+
+enum Command { PLAY, PAUSE, QUIT };
+
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,
- std::optional<BaseType_t> core_id,
- std::shared_ptr<IAudioElement> element) -> void;
+auto StartPipeline(Pipeline* pipeline, IAudioSink* sink) -> void;
+auto StartDrain(IAudioSink* sink) -> void;
-void AudioTaskMain(void* args);
+} // namespace task
} // namespace audio
diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp
index 9f2d676c..24f62e3c 100644
--- a/src/audio/include/fatfs_audio_input.hpp
+++ b/src/audio/include/fatfs_audio_input.hpp
@@ -3,41 +3,37 @@
#include <cstdint>
#include <memory>
#include <string>
+#include <vector>
#include "arena.hpp"
#include "chunk.hpp"
#include "freertos/FreeRTOS.h"
+#include "ff.h"
#include "freertos/message_buffer.h"
#include "freertos/queue.h"
#include "span.hpp"
#include "audio_element.hpp"
-#include "storage.hpp"
#include "stream_buffer.hpp"
+#include "stream_info.hpp"
namespace audio {
class FatfsAudioInput : public IAudioElement {
public:
- explicit FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage);
+ explicit FatfsAudioInput();
~FatfsAudioInput();
- auto HasUnprocessedInput() -> bool override;
- auto IsOverBuffered() -> bool override;
+ auto OpenFile(const std::string& path) -> void;
- 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;
+ auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
+ -> void override;
FatfsAudioInput(const FatfsAudioInput&) = delete;
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
private:
- memory::Arena arena_;
- std::shared_ptr<drivers::SdStorage> storage_;
-
FIL current_file_;
bool is_file_open_;
};
diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp
index 2bea091b..07430777 100644
--- a/src/audio/include/i2s_audio_output.hpp
+++ b/src/audio/include/i2s_audio_output.hpp
@@ -2,34 +2,28 @@
#include <cstdint>
#include <memory>
+#include <vector>
#include "audio_element.hpp"
+#include "audio_sink.hpp"
#include "chunk.hpp"
#include "result.hpp"
#include "dac.hpp"
#include "gpio_expander.hpp"
+#include "stream_info.hpp"
namespace audio {
-class I2SAudioOutput : public IAudioElement {
+class I2SAudioOutput : public IAudioSink {
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,
- std::unique_ptr<drivers::AudioDac> dac);
+ std::shared_ptr<drivers::AudioDac> dac);
~I2SAudioOutput();
- 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 ProcessLogStatus() -> void override;
- auto Process() -> void override;
+ auto Configure(const StreamInfo::Format& format) -> bool override;
+ auto Send(const cpp::span<std::byte>& data) -> void override;
+ auto Log() -> void override;
I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
@@ -38,10 +32,9 @@ class I2SAudioOutput : public IAudioElement {
auto SetVolume(uint8_t volume) -> void;
drivers::GpioExpander* expander_;
- std::unique_ptr<drivers::AudioDac> dac_;
+ std::shared_ptr<drivers::AudioDac> dac_;
- std::optional<ChunkReader> chunk_reader_;
- cpp::span<std::byte> latest_chunk_;
+ std::optional<StreamInfo::Pcm> current_config_;
};
} // namespace audio
diff --git a/src/audio/include/pipeline.hpp b/src/audio/include/pipeline.hpp
new file mode 100644
index 00000000..7a658a38
--- /dev/null
+++ b/src/audio/include/pipeline.hpp
@@ -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
diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp
index bf67364f..28095935 100644
--- a/src/audio/include/stream_info.hpp
+++ b/src/audio/include/stream_info.hpp
@@ -4,19 +4,96 @@
#include <optional>
#include <string>
#include <string_view>
+#include <type_traits>
+#include <utility>
+#include <variant>
-#include "cbor.h"
#include "result.hpp"
-#include "sys/_stdint.h"
+#include "span.hpp"
+#include "types.hpp"
namespace audio {
struct StreamInfo {
- std::optional<std::string> path;
- std::optional<uint8_t> channels;
- std::optional<uint8_t> bits_per_sample;
- std::optional<uint16_t> sample_rate;
- std::optional<size_t> chunk_size;
+ // The number of bytes that are available for consumption within this
+ // stream's buffer.
+ std::size_t bytes_in_stream{0};
+
+ // 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
diff --git a/src/audio/pipeline.cpp b/src/audio/pipeline.cpp
new file mode 100644
index 00000000..bab2f3ff
--- /dev/null
+++ b/src/audio/pipeline.cpp
@@ -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
diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp
new file mode 100644
index 00000000..7d833d25
--- /dev/null
+++ b/src/audio/stream_info.cpp
@@ -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
diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp
index 4e9a6a47..bdc8f51e 100644
--- a/src/codecs/codec.cpp
+++ b/src/codecs/codec.cpp
@@ -5,7 +5,7 @@
namespace codecs {
-auto CreateCodecForFile(const std::string& file)
+auto CreateCodecForType(StreamType type)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError> {
return std::make_unique<MadMp3Decoder>(); // TODO.
}
diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp
index 6897acf2..4595f877 100644
--- a/src/codecs/include/codec.hpp
+++ b/src/codecs/include/codec.hpp
@@ -10,6 +10,7 @@
#include "result.hpp"
#include "span.hpp"
+#include "types.hpp"
namespace codecs {
@@ -17,7 +18,7 @@ class ICodec {
public:
virtual ~ICodec() {}
- virtual auto CanHandleFile(const std::string& path) -> bool = 0;
+ virtual auto CanHandleType(StreamType type) -> bool = 0;
struct OutputFormat {
uint8_t num_channels;
@@ -31,7 +32,7 @@ class ICodec {
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
@@ -63,7 +64,7 @@ class ICodec {
enum CreateCodecError { UNKNOWN_EXTENSION };
-auto CreateCodecForFile(const std::string& file)
+auto CreateCodecForType(StreamType type)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>;
} // namespace codecs
diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp
index 1f5791b9..074784fb 100644
--- a/src/codecs/include/mad.hpp
+++ b/src/codecs/include/mad.hpp
@@ -17,10 +17,10 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder();
~MadMp3Decoder();
- auto CanHandleFile(const std::string& path) -> bool override;
+ auto CanHandleType(StreamType type) -> bool override;
auto GetOutputFormat() -> OutputFormat 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 ProcessNextFrame() -> cpp::result<bool, ProcessingError> override;
auto WriteOutputSamples(cpp::span<std::byte> output)
diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp
index 1112bd62..dd839537 100644
--- a/src/codecs/mad.cpp
+++ b/src/codecs/mad.cpp
@@ -1,16 +1,18 @@
#include "mad.hpp"
+#include <stdint.h>
#include <cstdint>
#include "mad.h"
#include "codec.hpp"
+#include "types.hpp"
namespace codecs {
-static int scaleTo24Bits(mad_fixed_t sample) {
+static uint32_t scaleToBits(mad_fixed_t sample, uint8_t bits) {
// Round the bottom bits.
- sample += (1L << (MAD_F_FRACBITS - 16));
+ sample += (1L << (MAD_F_FRACBITS - bits));
// Clip the leftover bits to within range.
if (sample >= MAD_F_ONE)
@@ -18,8 +20,8 @@ static int scaleTo24Bits(mad_fixed_t sample) {
else if (sample < -MAD_F_ONE)
sample = -MAD_F_ONE;
- /* quantize */
- return sample >> (MAD_F_FRACBITS + 1 - 16);
+ // Quantize.
+ return sample >> (MAD_F_FRACBITS + 1 - bits);
}
MadMp3Decoder::MadMp3Decoder() {
@@ -35,8 +37,8 @@ MadMp3Decoder::~MadMp3Decoder() {
mad_header_finish(&header_);
}
-auto MadMp3Decoder::CanHandleFile(const std::string& path) -> bool {
- return true; // TODO.
+auto MadMp3Decoder::CanHandleType(StreamType type) -> bool {
+ return type == STREAM_MP3;
}
auto MadMp3Decoder::GetOutputFormat() -> OutputFormat {
@@ -52,7 +54,7 @@ auto MadMp3Decoder::ResetForNewStream() -> void {
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_,
reinterpret_cast<const unsigned char*>(input.data()),
input.size());
@@ -113,15 +115,26 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span<std::byte> output)
}
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);
}
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 =
- 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)&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_++;
}
diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt
index c4e4c172..072a8b68 100644
--- a/src/drivers/CMakeLists.txt
+++ b/src/drivers/CMakeLists.txt
@@ -1,6 +1,6 @@
idf_component_register(
- SRCS "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
- "spi.cpp" "display.cpp" "display_init.cpp"
+ SRCS "touchwheel.cpp" "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
+ "spi.cpp" "display.cpp" "display_init.cpp" "driver_cache.cpp"
INCLUDE_DIRS "include"
REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/drivers/battery.cpp b/src/drivers/battery.cpp
index 00e7796a..8d747c07 100644
--- a/src/drivers/battery.cpp
+++ b/src/drivers/battery.cpp
@@ -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
// the max attenuation to properly handle the full range.
static const adc_atten_t kAdcAttenuation = ADC_ATTEN_DB_11;
-// Corresponds to GPIO 34.
-static const adc_channel_t kAdcChannel = ADC_CHANNEL_6;
+// Corresponds to SENSOR_VP.
+static const adc_channel_t kAdcChannel = ADC_CHANNEL_0;
Battery::Battery() {
adc_oneshot_unit_init_cfg_t unit_config = {
diff --git a/src/drivers/dac.cpp b/src/drivers/dac.cpp
index 1ab562f9..e82f0d27 100644
--- a/src/drivers/dac.cpp
+++ b/src/drivers/dac.cpp
@@ -7,36 +7,38 @@
#include "driver/i2c.h"
#include "driver/i2s_common.h"
#include "driver/i2s_std.h"
-#include "driver/i2s_types.h"
#include "esp_attr.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
+#include "hal/gpio_types.h"
#include "hal/i2c_types.h"
#include "gpio_expander.hpp"
#include "hal/i2s_types.h"
#include "i2c.hpp"
+#include "soc/clk_tree_defs.h"
#include "sys/_stdint.h"
namespace drivers {
static const char* kTag = "AUDIODAC";
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 AudioDac::SampleRate kDefaultSampleRate =
- 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> {
+auto AudioDac::create(GpioExpander* expander) -> cpp::result<AudioDac*, Error> {
// TODO: tune.
i2s_chan_handle_t i2s_handle;
i2s_chan_config_t channel_config =
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));
//
@@ -50,21 +52,22 @@ auto AudioDac::create(GpioExpander* expander)
i2s_std_config_t i2s_config = {
.clk_cfg = dac->clock_config_,
.slot_cfg = dac->slot_config_,
- .gpio_cfg =
- {// TODO: investigate running in three wire mode for less noise
- .mclk = GPIO_NUM_0,
- .bclk = GPIO_NUM_26,
- .ws = GPIO_NUM_27,
- .dout = GPIO_NUM_5,
- .din = I2S_GPIO_UNUSED,
- .invert_flags =
- {
- .mclk_inv = false,
- .bclk_inv = false,
- .ws_inv = false,
- }},
+ .gpio_cfg = {.mclk = GPIO_NUM_0,
+ .bclk = GPIO_NUM_26,
+ .ws = GPIO_NUM_27,
+ .dout = GPIO_NUM_5,
+ .din = I2S_GPIO_UNUSED,
+ .invert_flags =
+ {
+ .mclk_inv = false,
+ .bclk_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 =
i2s_channel_init_std_mode(i2s_handle, &i2s_config) != ESP_OK) {
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
// didn't shut down cleanly. Reset it to ensure it is in a consistent state.
- dac->WriteRegister(Register::POWER_MODE, 0b10001);
- dac->WriteRegister(Register::POWER_MODE, 1 << 4);
- dac->WriteRegister(Register::RESET, 0b10001);
+ dac->WriteRegister(pcm512x::POWER, 1 << 4);
+ dac->WriteRegister(pcm512x::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.
- dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b11 << 5);
// Enable auto clocking, and do your best to carry on despite errors.
// dac->WriteRegister(Register::CLOCK_ERRORS, 0b1111101);
- i2s_channel_enable(dac->i2s_handle_);
+ // i2s_channel_enable(dac->i2s_handle_);
- dac->WaitForPowerState(
- [](bool booted, PowerState state) { return state == STANDBY; });
+ dac->WaitForPowerState([](bool booted, PowerState state) {
+ return state == RUN || state == STANDBY;
+ });
- return dac;
+ return dac.release();
}
AudioDac::AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle)
: gpio_(gpio),
i2s_handle_(i2s_handle),
+ i2s_active_(false),
+ active_page_(),
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)),
slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
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();
}
AudioDac::~AudioDac() {
i2s_channel_disable(i2s_handle_);
i2s_del_channel(i2s_handle_);
- gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, false);
+ gpio_->set_pin(GpioExpander::AMP_EN, false);
gpio_->Write();
}
void AudioDac::WriteVolume(uint8_t volume) {
- WriteRegister(Register::DIGITAL_VOLUME_L, volume);
- WriteRegister(Register::DIGITAL_VOLUME_R, volume);
+ // Left channel.
+ WriteRegister(pcm512x::DIGITAL_VOLUME_2, volume);
+ // Right channel.
+ WriteRegister(pcm512x::DIGITAL_VOLUME_3, volume);
}
std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() {
- uint8_t result = 0;
-
- 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);
-
+ uint8_t result = ReadRegister(pcm512x::POWER_STATE);
bool is_booted = result >> 7;
PowerState detail = (PowerState)(result & 0b1111);
return std::pair(is_booted, detail);
@@ -163,13 +165,29 @@ bool AudioDac::WaitForPowerState(
}
auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
- // Disable the current output, if it isn't already stopped.
- WriteRegister(Register::POWER_MODE, 1 << 4);
- i2s_channel_disable(i2s_handle_);
+ if (i2s_active_) {
+ WriteRegister(pcm512x::MUTE, 0b10001);
+ vTaskDelay(1);
+ WriteRegister(pcm512x::POWER, 1 << 4);
+ i2s_channel_disable(i2s_handle_);
+ }
// I2S reconfiguration.
-
- slot_config_.slot_bit_width = (i2s_slot_bit_width_t)bps;
+ uint8_t bps_bits = 0;
+ 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_));
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_));
// 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;
+ }
- // TODO: base on BPS
- WriteRegister(Register::I2S_FORMAT, 0);
+ // 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;
+
+ // Derive clocks from the 98.304MHz PLL
+ 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
// up. I2S first, since otherwise the DAC will see that there's no clocks and
// shut itself down.
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;
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) {
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 {
LogStatus();
- WriteRegister(Register::POWER_MODE, 1 << 4);
+ WriteRegister(pcm512x::POWER, 1 << 4);
i2s_channel_disable(i2s_handle_);
}
@@ -218,35 +391,47 @@ auto AudioDac::Stop() -> void {
auto AudioDac::LogStatus() -> void {
uint8_t res;
- res = ReadRegister(Register::SAMPLE_RATE_DETECTION);
- ESP_LOGI(kTag, "detected sample rate (want 3): %u", (res >> 4) && 0b111);
+ res = ReadRegister(pcm512x::RATE_DET_1);
+ ESP_LOGI(kTag, "detected sample rate (want 3): %u", (res & 0b01110000) >> 4);
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);
- res = ReadRegister(Register::CLOCK_ERROR_STATE);
+ res = ReadRegister(pcm512x::RATE_DET_4);
ESP_LOGI(kTag, "clock errors (want zeroes): ");
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, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b10111));
- res = ReadRegister(Register::AUTO_MUTE_STATE);
- ESP_LOGI(kTag, "automute status (want 3): %u", res & 0b11);
-
- 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);
+ res = ReadRegister(pcm512x::DIGITAL_MUTE_DET);
+ ESP_LOGI(kTag, "automute status (want 0): %u", res & 0b10001);
auto power = ReadPowerState();
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;
transaction.start()
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
@@ -256,7 +441,7 @@ void AudioDac::WriteRegister(Register reg, uint8_t val) {
transaction.Execute();
}
-uint8_t AudioDac::ReadRegister(Register reg) {
+uint8_t AudioDac::ReadRegisterRaw(uint8_t reg) {
uint8_t result = 0;
I2CTransaction transaction;
transaction.start()
diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp
index 6ec82787..f8594a5a 100644
--- a/src/drivers/display.cpp
+++ b/src/drivers/display.cpp
@@ -20,6 +20,7 @@
#include "display_init.hpp"
#include "gpio_expander.hpp"
+#include "soc/soc.h"
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 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
* 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,
const displays::InitialisationData& init_data)
- -> std::unique_ptr<Display> {
- // First, turn on the LED backlight.
- expander->set_pin(GpioExpander::DISPLAY_LED, 0);
- expander->set_pin(GpioExpander::DISPLAY_POWER_ENABLE, 1);
- expander->Write();
+ -> Display* {
+ ESP_LOGI(kTag, "Init I/O pins");
+ gpio_config_t dr_config{
+ .pin_bit_mask = 1ULL << kDisplayDr,
+ .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
spi_device_interface_config_t spi_cfg = {
@@ -76,7 +98,7 @@ auto Display::create(GpioExpander* expander,
.cs_ena_posttrans = 0,
.clock_speed_hz = SPI_MASTER_FREQ_40M,
.input_delay_ns = 0,
- .spics_io_num = GPIO_NUM_22,
+ .spics_io_num = kDisplayCs,
.flags = 0,
.queue_size = kTransactionQueueSize,
.pre_cb = NULL,
@@ -103,13 +125,16 @@ auto Display::create(GpioExpander* expander,
display->driver_.draw_buf = &display->buffers_;
display->driver_.hor_res = kDisplayWidth;
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_.user_data = display.get();
ESP_LOGI(kTag, "Registering driver");
display->display_ = lv_disp_drv_register(&display->driver_);
- return display;
+ return display.release();
}
Display::Display(GpioExpander* gpio, spi_device_handle_t handle)
@@ -190,9 +215,7 @@ void Display::SendTransaction(TransactionType type,
transaction.tx_buffer = data;
}
- // TODO(jacqueline): Move this to an on-board GPIO for speed.
- gpio_->set_pin(GpioExpander::DISPLAY_DR, type);
- gpio_->Write();
+ gpio_set_level(kDisplayDr, type);
// TODO(jacqueline): Handle these errors.
esp_err_t ret = spi_device_polling_transmit(handle_, &transaction);
diff --git a/src/drivers/driver_cache.cpp b/src/drivers/driver_cache.cpp
new file mode 100644
index 00000000..650e6f16
--- /dev/null
+++ b/src/drivers/driver_cache.cpp
@@ -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
diff --git a/src/drivers/i2c.cpp b/src/drivers/i2c.cpp
index 04a6d7d1..6c6fc407 100644
--- a/src/drivers/i2c.cpp
+++ b/src/drivers/i2c.cpp
@@ -9,8 +9,8 @@
namespace drivers {
static const i2c_port_t kI2CPort = I2C_NUM_0;
-static const gpio_num_t kI2CSdaPin = GPIO_NUM_2;
-static const gpio_num_t kI2CSclPin = GPIO_NUM_4;
+static const gpio_num_t kI2CSdaPin = GPIO_NUM_4;
+static const gpio_num_t kI2CSclPin = GPIO_NUM_2;
static const uint32_t kI2CClkSpeed = 400'000;
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)) {
return err;
}
+ if (esp_err_t err = i2c_set_timeout(kI2CPort, 400000)) {
+ return err;
+ }
// TODO: INT line
@@ -57,8 +60,8 @@ I2CTransaction::~I2CTransaction() {
free(buffer_);
}
-esp_err_t I2CTransaction::Execute() {
- return i2c_master_cmd_begin(I2C_NUM_0, handle_, kI2CTimeout);
+esp_err_t I2CTransaction::Execute(uint8_t port) {
+ return i2c_master_cmd_begin(port, handle_, kI2CTimeout);
}
I2CTransaction& I2CTransaction::start() {
diff --git a/src/drivers/include/dac.hpp b/src/drivers/include/dac.hpp
index 06808a78..4952c992 100644
--- a/src/drivers/include/dac.hpp
+++ b/src/drivers/include/dac.hpp
@@ -12,6 +12,7 @@
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
+#include "freertos/stream_buffer.h"
#include "result.hpp"
#include "span.hpp"
@@ -20,6 +21,91 @@
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.
*/
@@ -31,8 +117,7 @@ class AudioDac {
FAILED_TO_INSTALL_I2S,
};
- static auto create(GpioExpander* expander)
- -> cpp::result<std::unique_ptr<AudioDac>, Error>;
+ static auto create(GpioExpander* expander) -> cpp::result<AudioDac*, Error>;
AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle);
~AudioDac();
@@ -64,14 +149,21 @@ class AudioDac {
BPS_32 = I2S_DATA_BIT_WIDTH_32BIT,
};
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_48 = 48000,
+ SAMPLE_RATE_96 = 96000,
+ SAMPLE_RATE_192 = 192000,
};
// TODO(jacqueline): worth supporting channels here as well?
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 LogStatus() -> void;
@@ -83,6 +175,8 @@ class AudioDac {
private:
GpioExpander* gpio_;
i2s_chan_handle_t i2s_handle_;
+ bool i2s_active_;
+ std::optional<uint8_t> active_page_;
i2s_std_clk_config_t clock_config_;
i2s_std_slot_config_t slot_config_;
@@ -93,29 +187,12 @@ class AudioDac {
*/
bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate);
- enum Register {
- PAGE_SELECT = 0,
- 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(pcm512x::Register r, uint8_t val);
+ uint8_t ReadRegister(pcm512x::Register r);
- void WriteRegister(Register reg, uint8_t val);
- uint8_t ReadRegister(Register reg);
+ void SelectPage(uint8_t page);
+ void WriteRegisterRaw(uint8_t reg, uint8_t val);
+ uint8_t ReadRegisterRaw(uint8_t reg);
};
} // namespace drivers
diff --git a/src/drivers/include/display.hpp b/src/drivers/include/display.hpp
index 8157c3a5..9e4a0224 100644
--- a/src/drivers/include/display.hpp
+++ b/src/drivers/include/display.hpp
@@ -23,8 +23,7 @@ class Display {
* us back any kind of signal to tell us we're actually using them correctly.
*/
static auto create(GpioExpander* expander,
- const displays::InitialisationData& init_data)
- -> std::unique_ptr<Display>;
+ const displays::InitialisationData& init_data) -> Display*;
Display(GpioExpander* gpio, spi_device_handle_t handle);
~Display();
diff --git a/src/drivers/include/driver_cache.hpp b/src/drivers/include/driver_cache.hpp
new file mode 100644
index 00000000..c56ebc3f
--- /dev/null
+++ b/src/drivers/include/driver_cache.hpp
@@ -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
diff --git a/src/drivers/include/gpio_expander.hpp b/src/drivers/include/gpio_expander.hpp
index a6e96d87..cd3719a0 100644
--- a/src/drivers/include/gpio_expander.hpp
+++ b/src/drivers/include/gpio_expander.hpp
@@ -35,28 +35,28 @@ class GpioExpander {
static const uint8_t kPca8575Timeout = pdMS_TO_TICKS(100);
// Port A:
- // 0 - audio power enable
- // 1 - usb interface power enable (active low)
- // 2 - display power enable
- // 3 - touchpad power enable
- // 4 - sd card power enable
- // 5 - sd mux switch
- // 6 - LDO enable
- // 7 - charge power ok (active low)
- // All power switches low, sd mux pointing away from us, inputs high.
- static const uint8_t kPortADefault = 0b10000010;
+ // 0 - sd card mux switch
+ // 1 - sd card mux enable (active low)
+ // 2 - key up
+ // 3 - key down
+ // 4 - key lock
+ // 5 - display reset
+ // 6 - NC
+ // 7 - sd card power (active low)
+ // Default to SD card off, inputs high.
+ static const uint8_t kPortADefault = 0b10111110;
// Port B:
- // 0 - 3.5mm jack detect (active low)
- // 1 - unused
- // 2 - volume up
- // 3 - volume down
- // 4 - lock switch
- // 5 - touchpad interupt
- // 6 - display DR
- // 7 - display LED
- // Inputs all high, all others low.
- static const uint8_t kPortBDefault = 0b00111101;
+ // 0 - trs output enable
+ // 1 - 3.5mm jack detect (active low)
+ // 2 - NC
+ // 3 - NC
+ // 4 - NC
+ // 5 - NC
+ // 6 - NC
+ // 7 - NC
+ // Default input high, trs output low
+ static const uint8_t kPortBDefault = 0b00000010;
/*
* 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. */
enum Pin {
// Port A
- AUDIO_POWER_ENABLE = 0,
- USB_INTERFACE_POWER_ENABLE = 1,
- DISPLAY_POWER_ENABLE = 2,
- TOUCHPAD_POWER_ENABLE = 3,
- SD_CARD_POWER_ENABLE = 4,
- SD_MUX_SWITCH = 5,
- LDO_ENABLE = 6,
- CHARGE_POWER_OK = 7, // Active-low input
+ SD_MUX_SWITCH = 0,
+ SD_MUX_EN_ACTIVE_LOW = 1,
+ KEY_UP = 2,
+ KEY_DOWN = 3,
+ KEY_LOCK = 4,
+ DISPLAY_RESET = 5,
+ // UNUSED = 6,
+ SD_CARD_POWER_ENABLE = 7,
// Port B
- PHONE_DETECT = 8, // Active-high input
- // UNUSED = 9,
- VOL_UP = 10,
- VOL_DOWN = 11,
- LOCK = 12,
- TOUCHPAD_INT = 13,
- DISPLAY_DR = 14,
- DISPLAY_LED = 15,
+ AMP_EN = 8,
+ PHONE_DETECT = 9,
+ // UNUSED = 10,
+ // UNUSED = 11,
+ // UNUSED = 12,
+ // UNUSED = 13,
+ // UNUSED = 14,
+ // UNUSED = 15,
};
/* Nicer value names for use with the SD_MUX_SWITCH pin. */
enum SdController {
- SD_MUX_ESP = 1,
- SD_MUX_USB = 0,
+ SD_MUX_ESP = 0,
+ SD_MUX_SAMD = 1,
};
/**
diff --git a/src/drivers/include/i2c.hpp b/src/drivers/include/i2c.hpp
index dbdd8a11..811c9333 100644
--- a/src/drivers/include/i2c.hpp
+++ b/src/drivers/include/i2c.hpp
@@ -35,7 +35,7 @@ class I2CTransaction {
* 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_t Execute();
+ esp_err_t Execute(uint8_t port = I2C_NUM_0);
/*
* Enqueues a start condition. May also be used for repeated start
diff --git a/src/drivers/include/storage.hpp b/src/drivers/include/storage.hpp
index 64ce4782..c19ec935 100644
--- a/src/drivers/include/storage.hpp
+++ b/src/drivers/include/storage.hpp
@@ -25,14 +25,13 @@ class SdStorage {
FAILED_TO_MOUNT,
};
- static auto create(GpioExpander* gpio)
- -> cpp::result<std::shared_ptr<SdStorage>, Error>;
+ static auto create(GpioExpander* gpio) -> cpp::result<SdStorage*, Error>;
SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*),
sdspi_dev_handle_t handle_,
- std::unique_ptr<sdmmc_host_t>& host_,
- std::unique_ptr<sdmmc_card_t>& card_,
+ std::unique_ptr<sdmmc_host_t> host_,
+ std::unique_ptr<sdmmc_card_t> card_,
FATFS* fs_);
~SdStorage();
diff --git a/src/drivers/include/touchwheel.hpp b/src/drivers/include/touchwheel.hpp
new file mode 100644
index 00000000..3dfa182b
--- /dev/null
+++ b/src/drivers/include/touchwheel.hpp
@@ -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
diff --git a/src/drivers/storage.cpp b/src/drivers/storage.cpp
index 88159744..d90bd811 100644
--- a/src/drivers/storage.cpp
+++ b/src/drivers/storage.cpp
@@ -49,8 +49,9 @@ static esp_err_t do_transaction(sdspi_dev_handle_t handle,
}
} // namespace callback
-auto SdStorage::create(GpioExpander* gpio)
- -> cpp::result<std::shared_ptr<SdStorage>, Error> {
+auto SdStorage::create(GpioExpander* gpio) -> cpp::result<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->Write();
@@ -103,16 +104,16 @@ auto SdStorage::create(GpioExpander* gpio)
return cpp::fail(Error::FAILED_TO_MOUNT);
}
- return std::make_unique<SdStorage>(gpio, do_transaction, handle, host, card,
- fs);
+ return new SdStorage(gpio, do_transaction, handle, std::move(host),
+ std::move(card), fs);
}
SdStorage::SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t,
sdmmc_command_t*),
sdspi_dev_handle_t handle,
- std::unique_ptr<sdmmc_host_t>& host,
- std::unique_ptr<sdmmc_card_t>& card,
+ std::unique_ptr<sdmmc_host_t> host,
+ std::unique_ptr<sdmmc_card_t> card,
FATFS* fs)
: gpio_(gpio),
do_transaction_(do_transaction),
@@ -136,6 +137,10 @@ SdStorage::~SdStorage() {
// Uninstall the SPI driver
sdspi_host_remove_device(this->handle_);
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,
diff --git a/src/drivers/touchwheel.cpp b/src/drivers/touchwheel.cpp
new file mode 100644
index 00000000..51a67187
--- /dev/null
+++ b/src/drivers/touchwheel.cpp
@@ -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
diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt
index d5ab0429..524a7d30 100644
--- a/src/main/CMakeLists.txt
+++ b/src/main/CMakeLists.txt
@@ -1,5 +1,5 @@
idf_component_register(
SRCS "main.cpp" "app_console.cpp"
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})
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 88943f21..4fb355e2 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -1,30 +1,25 @@
#include <dirent.h>
+#include <stdint.h>
#include <stdio.h>
#include <cstddef>
#include <cstdint>
#include <memory>
-#include "core/lv_disp.h"
-#include "core/lv_obj_pos.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
#include "driver/spi_master.h"
+#include "driver_cache.hpp"
#include "esp_freertos_hooks.h"
#include "esp_heap_caps.h"
#include "esp_intr_alloc.h"
#include "esp_log.h"
-#include "font/lv_font.h"
#include "freertos/portmacro.h"
+#include "freertos/projdefs.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 "app_console.hpp"
#include "audio_playback.hpp"
@@ -35,64 +30,14 @@
#include "display_init.hpp"
#include "gpio_expander.hpp"
#include "i2c.hpp"
+#include "lvgl_task.hpp"
#include "spi.hpp"
#include "storage.hpp"
+#include "touchwheel.hpp"
static const char* TAG = "MAIN";
-void IRAM_ATTR tick_hook(void) {
- 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) {
+void db_main(void *whatever) {
ESP_LOGI(TAG, "Init database");
auto db_res = database::Database::Open();
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_spi());
+ std::unique_ptr<drivers::DriverCache> drivers =
+ std::make_unique<drivers::DriverCache>();
ESP_LOGI(TAG, "Init GPIOs");
- drivers::GpioExpander* expander = new drivers::GpioExpander();
+ drivers::GpioExpander* expander = drivers->AcquireGpios();
ESP_LOGI(TAG, "Enable power rails for development");
- expander->with([&](auto& gpio) {
- gpio.set_pin(drivers::GpioExpander::AUDIO_POWER_ENABLE, 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);
- });
+ expander->with(
+ [&](auto& gpio) { gpio.set_pin(drivers::GpioExpander::AMP_EN, 1); });
ESP_LOGI(TAG, "Init battery measurement");
drivers::Battery* battery = new drivers::Battery();
ESP_LOGI(TAG, "it's reading %d mV!", (int)battery->Millivolts());
ESP_LOGI(TAG, "Init SD card");
- auto storage_res = drivers::SdStorage::create(expander);
- std::shared_ptr<drivers::SdStorage> storage;
- if (storage_res.has_error()) {
+ auto storage = drivers->AcquireStorage();
+ if (!storage) {
ESP_LOGE(TAG, "Failed! Do you have an SD card?");
- } else {
- storage = std::move(storage_res.value());
}
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));
xTaskCreateStatic(&db_main, "LEVELDB", db_stack_size, NULL, 1, database_stack, &database_task_buffer);
- ESP_LOGI(TAG, "Launch LVGL task");
- LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs));
- lvglArgs->gpio_expander = expander;
- xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize,
- (void*)lvglArgs, 1, sLvglStack,
- &sLvglTaskBuffer, 1);
+ ESP_LOGI(TAG, "Init touch wheel");
+ std::shared_ptr<drivers::TouchWheel> touchwheel =
+ drivers->AcquireTouchWheel();
+
+ std::atomic<bool> lvgl_quit;
+ 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) {
ESP_LOGI(TAG, "Init audio pipeline");
- auto playback_res = audio::AudioPlayback::create(expander, storage);
- if (playback_res.has_error()) {
- ESP_LOGE(TAG, "Failed! Playback will not work.");
- } else {
- playback = std::move(playback_res.value());
- }
+ playback = std::make_unique<audio::AudioPlayback>(drivers.get());
}
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.Launch();
+ uint8_t prev_position = 0;
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));
}
}
+
diff --git a/src/memory/CMakeLists.txt b/src/memory/CMakeLists.txt
index 67e64267..69afc454 100644
--- a/src/memory/CMakeLists.txt
+++ b/src/memory/CMakeLists.txt
@@ -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})
diff --git a/src/memory/include/himem.hpp b/src/memory/include/himem.hpp
new file mode 100644
index 00000000..517ebfdf
--- /dev/null
+++ b/src/memory/include/himem.hpp
@@ -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;
+};
diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp
index 32de431a..b9fce7ec 100644
--- a/src/tasks/tasks.cpp
+++ b/src/tasks/tasks.cpp
@@ -1,4 +1,5 @@
#include "tasks.hpp"
const UBaseType_t kTaskPriorityLvgl = 4;
-const UBaseType_t kTaskPriorityAudio = 5;
+const UBaseType_t kTaskPriorityAudioPipeline = 5;
+const UBaseType_t kTaskPriorityAudioDrain = 6;
diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp
index 24f8509a..47668aea 100644
--- a/src/tasks/tasks.hpp
+++ b/src/tasks/tasks.hpp
@@ -3,4 +3,5 @@
#include "freertos/portmacro.h"
extern const UBaseType_t kTaskPriorityLvgl;
-extern const UBaseType_t kTaskPriorityAudio;
+extern const UBaseType_t kTaskPriorityAudioPipeline;
+extern const UBaseType_t kTaskPriorityAudioDrain;
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
new file mode 100644
index 00000000..bdccbbdb
--- /dev/null
+++ b/src/ui/CMakeLists.txt
@@ -0,0 +1,5 @@
+idf_component_register(
+ SRCS "lvgl_task.cpp"
+ INCLUDE_DIRS "include"
+ REQUIRES "drivers")
+target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/ui/include/lvgl_task.hpp b/src/ui/include/lvgl_task.hpp
new file mode 100644
index 00000000..ca3fc771
--- /dev/null
+++ b/src/ui/include/lvgl_task.hpp
@@ -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
diff --git a/src/ui/lvgl_task.cpp b/src/ui/lvgl_task.cpp
new file mode 100644
index 00000000..12dfd34e
--- /dev/null
+++ b/src/ui/lvgl_task.cpp
@@ -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