/* * Copyright 2023 jacqueline * * SPDX-License-Identifier: GPL-3.0-only */ #include "audio_fsm.hpp" #include #include #include #include "audio_decoder.hpp" #include "audio_events.hpp" #include "audio_task.hpp" #include "esp_log.h" #include "event_queue.hpp" #include "fatfs_audio_input.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" #include "pipeline.hpp" #include "system_events.hpp" #include "track.hpp" namespace audio { static const char kTag[] = "audio_fsm"; drivers::IGpios* AudioState::sIGpios; std::shared_ptr AudioState::sDac; std::weak_ptr AudioState::sDatabase; std::unique_ptr AudioState::sFileSource; std::unique_ptr AudioState::sI2SOutput; std::vector> AudioState::sPipeline; std::deque AudioState::sTrackQueue; auto AudioState::Init(drivers::IGpios* gpio_expander, std::weak_ptr database) -> bool { sIGpios = gpio_expander; auto dac = drivers::I2SDac::create(gpio_expander); if (!dac) { return false; } sDac.reset(dac.value()); sDatabase = database; sFileSource.reset(new FatfsAudioInput()); sI2SOutput.reset(new I2SAudioOutput(sIGpios, sDac)); // Perform initial pipeline configuration. // TODO(jacqueline): Factor this out once we have any kind of dynamic // reconfiguration. AudioDecoder* codec = new AudioDecoder(); sPipeline.emplace_back(codec); Pipeline* pipeline = new Pipeline(sPipeline.front().get()); pipeline->AddInput(sFileSource.get()); task::StartPipeline(pipeline, sI2SOutput.get()); return true; } void AudioState::react(const system_fsm::StorageMounted& ev) { sDatabase = ev.db; } void AudioState::react(const system_fsm::KeyUpChanged& ev) { if (ev.falling && sI2SOutput->AdjustVolumeUp()) { ESP_LOGI(kTag, "volume up!"); events::Dispatch({}); } } void AudioState::react(const system_fsm::KeyDownChanged& ev) { if (ev.falling && sI2SOutput->AdjustVolumeDown()) { ESP_LOGI(kTag, "volume down!"); events::Dispatch({}); } } void AudioState::react(const system_fsm::HasPhonesChanged& ev) { if (ev.falling) { ESP_LOGI(kTag, "headphones in!"); } else { ESP_LOGI(kTag, "headphones out!"); } } namespace states { void Uninitialised::react(const system_fsm::BootComplete&) { transit(); } void Standby::react(const InputFileOpened& ev) { transit(); } void Standby::react(const PlayTrack& ev) { auto db = sDatabase.lock(); if (!db) { ESP_LOGW(kTag, "database not open; ignoring play request"); return; } if (ev.data) { sFileSource->OpenFile(ev.data->filepath()); } else { sFileSource->OpenFile(db->GetTrackPath(ev.id)); } } void Standby::react(const PlayFile& ev) { sFileSource->OpenFile(ev.filename); } void Playback::entry() { ESP_LOGI(kTag, "beginning playback"); sI2SOutput->SetInUse(true); } void Playback::exit() { ESP_LOGI(kTag, "finishing playback"); sI2SOutput->SetInUse(false); } void Playback::react(const PlayTrack& ev) { sTrackQueue.push_back(EnqueuedItem(ev.id)); } void Playback::react(const PlayFile& ev) { sTrackQueue.push_back(EnqueuedItem(ev.filename)); } void Playback::react(const PlaybackUpdate& ev) { ESP_LOGI(kTag, "elapsed: %lu", ev.seconds_elapsed); } void Playback::react(const InputFileOpened& ev) {} void Playback::react(const InputFileFinished& ev) { ESP_LOGI(kTag, "finished file"); if (sTrackQueue.empty()) { return; } EnqueuedItem next_item = sTrackQueue.front(); sTrackQueue.pop_front(); if (std::holds_alternative(next_item)) { sFileSource->OpenFile(std::get(next_item)); } else if (std::holds_alternative(next_item)) { auto db = sDatabase.lock(); if (!db) { ESP_LOGW(kTag, "database not open; ignoring play request"); return; } sFileSource->OpenFile( db->GetTrackPath(std::get(next_item))); } } void Playback::react(const AudioPipelineIdle& ev) { transit(); } } // namespace states } // namespace audio FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised)