|
|
|
@ -5,11 +5,14 @@ |
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
#include "tts/player.hpp" |
|
|
|
|
#include <mutex> |
|
|
|
|
|
|
|
|
|
#include "audio/audio_events.hpp" |
|
|
|
|
#include "audio/processor.hpp" |
|
|
|
|
#include "audio/resample.hpp" |
|
|
|
|
#include "codec.hpp" |
|
|
|
|
#include "esp_log.h" |
|
|
|
|
#include "events/event_queue.hpp" |
|
|
|
|
#include "freertos/projdefs.h" |
|
|
|
|
#include "portmacro.h" |
|
|
|
|
#include "sample.hpp" |
|
|
|
@ -22,47 +25,70 @@ namespace tts { |
|
|
|
|
Player::Player(tasks::WorkerPool& worker, |
|
|
|
|
drivers::PcmBuffer& output, |
|
|
|
|
audio::FatfsStreamFactory& factory) |
|
|
|
|
: bg_(worker), stream_factory_(factory), output_(output), play_count_(0) {} |
|
|
|
|
: bg_(worker), |
|
|
|
|
stream_factory_(factory), |
|
|
|
|
output_(output), |
|
|
|
|
stream_playing_(false), |
|
|
|
|
stream_cancelled_(false) {} |
|
|
|
|
|
|
|
|
|
auto Player::playFile(const std::string& path) -> void { |
|
|
|
|
ESP_LOGI(kTag, "playing '%s'", path.c_str()); |
|
|
|
|
int this_play = ++play_count_; |
|
|
|
|
|
|
|
|
|
bg_.Dispatch<void>([=, this]() { |
|
|
|
|
auto stream = stream_factory_.create(path); |
|
|
|
|
if (!stream) { |
|
|
|
|
ESP_LOGE(kTag, "creating stream failed"); |
|
|
|
|
return; |
|
|
|
|
// Interrupt current playback
|
|
|
|
|
{ |
|
|
|
|
std::scoped_lock<std::mutex> lock{new_stream_mutex_}; |
|
|
|
|
if (stream_playing_) { |
|
|
|
|
stream_cancelled_ = true; |
|
|
|
|
stream_playing_.wait(true); |
|
|
|
|
} |
|
|
|
|
stream_cancelled_ = false; |
|
|
|
|
stream_playing_ = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// FIXME: Rather than hardcoding WAV support only, we should work out a
|
|
|
|
|
// proper subset of 'low memory' decoders that can all be used for TTS
|
|
|
|
|
// playback.
|
|
|
|
|
if (stream->type() != codecs::StreamType::kWav) { |
|
|
|
|
ESP_LOGE(kTag, "stream was unsupported type"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
openAndDecode(path); |
|
|
|
|
|
|
|
|
|
auto decoder = codecs::CreateCodecForType(stream->type()); |
|
|
|
|
if (!decoder) { |
|
|
|
|
ESP_LOGE(kTag, "creating decoder failed"); |
|
|
|
|
return; |
|
|
|
|
if (!stream_cancelled_) { |
|
|
|
|
events::Audio().Dispatch(audio::TtsPlaybackChanged{.is_playing = false}); |
|
|
|
|
} |
|
|
|
|
stream_playing_ = false; |
|
|
|
|
stream_playing_.notify_all(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::unique_ptr<codecs::ICodec> codec{*decoder}; |
|
|
|
|
auto open_res = codec->OpenStream(stream, 0); |
|
|
|
|
if (open_res.has_error()) { |
|
|
|
|
ESP_LOGE(kTag, "opening stream failed"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
auto Player::openAndDecode(const std::string& path) -> void { |
|
|
|
|
auto stream = stream_factory_.create(path); |
|
|
|
|
if (!stream) { |
|
|
|
|
ESP_LOGE(kTag, "creating stream failed"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
decodeToSink(*open_res, std::move(codec), this_play); |
|
|
|
|
}); |
|
|
|
|
// FIXME: Rather than hardcoding WAV support only, we should work out a
|
|
|
|
|
// proper subset of 'low memory' decoders that can all be used for TTS
|
|
|
|
|
// playback.
|
|
|
|
|
if (stream->type() != codecs::StreamType::kWav) { |
|
|
|
|
ESP_LOGE(kTag, "stream was unsupported type"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
auto decoder = codecs::CreateCodecForType(stream->type()); |
|
|
|
|
if (!decoder) { |
|
|
|
|
ESP_LOGE(kTag, "creating decoder failed"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::unique_ptr<codecs::ICodec> codec{*decoder}; |
|
|
|
|
auto open_res = codec->OpenStream(stream, 0); |
|
|
|
|
if (open_res.has_error()) { |
|
|
|
|
ESP_LOGE(kTag, "opening stream failed"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
decodeToSink(*open_res, std::move(codec)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
auto Player::decodeToSink(const codecs::ICodec::OutputFormat& format, |
|
|
|
|
std::unique_ptr<codecs::ICodec> codec, |
|
|
|
|
int play_count) -> void { |
|
|
|
|
std::unique_ptr<codecs::ICodec> codec) -> void { |
|
|
|
|
// Set up buffers to hold samples between the intermediary parts of
|
|
|
|
|
// processing. We can just use the stack for these, since this method is
|
|
|
|
|
// called only from background workers, which have enormous stacks.
|
|
|
|
@ -83,20 +109,18 @@ auto Player::decodeToSink(const codecs::ICodec::OutputFormat& format, |
|
|
|
|
} |
|
|
|
|
bool double_samples = format.num_channels == 1; |
|
|
|
|
|
|
|
|
|
// Start our playback (wait for previous to end?)
|
|
|
|
|
events::Audio().Dispatch(audio::TtsPlaybackChanged{.is_playing = true}); |
|
|
|
|
|
|
|
|
|
// FIXME: This decode-and-process loop is substantially the same as the audio
|
|
|
|
|
// processor's filter loop. Ideally we should refactor both of these loops to
|
|
|
|
|
// reuse code, however I'm holding off on doing this until we've implemented
|
|
|
|
|
// more advanced audio processing features in the audio processor (EQ, tempo
|
|
|
|
|
// shifting, etc.) as it's not clear to me yet how much the two codepaths will
|
|
|
|
|
// be diverging later anyway.
|
|
|
|
|
while (codec || !decode_buf.isEmpty() || !resample_buf.isEmpty() || |
|
|
|
|
!stereo_buf.isEmpty()) { |
|
|
|
|
if (play_count != play_count_) { |
|
|
|
|
// FIXME: This is a little unsafe and could maybe take out the first few
|
|
|
|
|
// samples of the next file.
|
|
|
|
|
output_.clear(); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
while ((codec || !decode_buf.isEmpty() || !resample_buf.isEmpty() || |
|
|
|
|
!stereo_buf.isEmpty()) && |
|
|
|
|
!stream_cancelled_) { |
|
|
|
|
if (codec) { |
|
|
|
|
auto decode_res = codec->DecodeTo(decode_buf.writeAcquire()); |
|
|
|
|
if (decode_res.has_error()) { |
|
|
|
@ -156,6 +180,14 @@ auto Player::decodeToSink(const codecs::ICodec::OutputFormat& format, |
|
|
|
|
stereo_buf.readCommit(sent); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
while (!output_.isEmpty()) { |
|
|
|
|
if (stream_cancelled_) { |
|
|
|
|
output_.clear(); |
|
|
|
|
} else { |
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(100)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} // namespace tts
|
|
|
|
|