1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "audio_fsm.hpp"
#include <future>
#include <memory>
#include <variant>
#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 "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "future_fetcher.hpp"
#include "i2s_audio_output.hpp"
#include "i2s_dac.hpp"
#include "pipeline.hpp"
#include "system_events.hpp"
#include "track.hpp"
#include "track_queue.hpp"
namespace audio {
static const char kTag[] = "audio_fsm";
drivers::IGpios* AudioState::sIGpios;
std::shared_ptr<drivers::I2SDac> AudioState::sDac;
std::weak_ptr<database::Database> AudioState::sDatabase;
std::unique_ptr<AudioTask> AudioState::sTask;
std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
TrackQueue* AudioState::sTrackQueue;
std::optional<database::TrackId> AudioState::sCurrentTrack;
auto AudioState::Init(drivers::IGpios* gpio_expander,
std::weak_ptr<database::Database> database,
std::shared_ptr<database::ITagParser> tag_parser,
TrackQueue* queue) -> bool {
sIGpios = gpio_expander;
sTrackQueue = queue;
auto dac = drivers::I2SDac::create(gpio_expander);
if (!dac) {
return false;
}
sDac.reset(dac.value());
sDatabase = database;
sFileSource.reset(new FatfsAudioInput(tag_parser));
sI2SOutput.reset(new I2SAudioOutput(sIGpios, sDac));
AudioTask::Start(sFileSource.get(), 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::Ui().Dispatch(VolumeChanged{});
}
}
void AudioState::react(const system_fsm::KeyDownChanged& ev) {
if (ev.falling && sI2SOutput->AdjustVolumeDown()) {
ESP_LOGI(kTag, "volume down!");
events::Ui().Dispatch(VolumeChanged{});
}
}
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<Standby>();
}
void Standby::react(const PlayFile& ev) {
sFileSource->SetPath(ev.filename);
}
void Playback::react(const PlayFile& ev) {
sFileSource->SetPath(ev.filename);
}
void Standby::react(const internal::InputFileOpened& ev) {
transit<Playback>();
}
void Standby::react(const QueueUpdate& ev) {
auto current_track = sTrackQueue->GetCurrent();
if (!current_track || (sCurrentTrack && *sCurrentTrack == *current_track)) {
return;
}
sCurrentTrack = current_track;
auto db = sDatabase.lock();
if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request");
return;
}
sFileSource->SetPath(db->GetTrackPath(*current_track));
}
void Playback::entry() {
ESP_LOGI(kTag, "beginning playback");
sI2SOutput->SetInUse(true);
}
void Playback::exit() {
ESP_LOGI(kTag, "finishing playback");
// TODO(jacqueline): Second case where it's useful to wait for the i2s buffer
// to drain.
vTaskDelay(pdMS_TO_TICKS(250));
sI2SOutput->SetInUse(false);
}
void Playback::react(const QueueUpdate& ev) {
if (!ev.current_changed) {
return;
}
auto current_track = sTrackQueue->GetCurrent();
if (!current_track) {
sFileSource->SetPath();
sCurrentTrack.reset();
transit<Standby>();
return;
}
sCurrentTrack = current_track;
auto db = sDatabase.lock();
if (!db) {
return;
}
sFileSource->SetPath(db->GetTrackPath(*current_track));
}
void Playback::react(const PlaybackUpdate& ev) {
ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
ev.seconds_total);
}
void Playback::react(const internal::InputFileOpened& ev) {}
void Playback::react(const internal::InputFileClosed& ev) {}
void Playback::react(const internal::InputFileFinished& ev) {
ESP_LOGI(kTag, "finished playing file");
sTrackQueue->Next();
if (!sTrackQueue->GetCurrent()) {
transit<Standby>();
}
}
void Playback::react(const internal::AudioPipelineIdle& ev) {
transit<Standby>();
}
} // namespace states
} // namespace audio
FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised)
|