|
|
@ -21,6 +21,7 @@ |
|
|
|
#include "audio_events.hpp" |
|
|
|
#include "audio_events.hpp" |
|
|
|
#include "audio_fsm.hpp" |
|
|
|
#include "audio_fsm.hpp" |
|
|
|
#include "audio_sink.hpp" |
|
|
|
#include "audio_sink.hpp" |
|
|
|
|
|
|
|
#include "audio_source.hpp" |
|
|
|
#include "cbor.h" |
|
|
|
#include "cbor.h" |
|
|
|
#include "codec.hpp" |
|
|
|
#include "codec.hpp" |
|
|
|
#include "esp_err.h" |
|
|
|
#include "esp_err.h" |
|
|
@ -43,6 +44,7 @@ |
|
|
|
#include "stream_message.hpp" |
|
|
|
#include "stream_message.hpp" |
|
|
|
#include "sys/_stdint.h" |
|
|
|
#include "sys/_stdint.h" |
|
|
|
#include "tasks.hpp" |
|
|
|
#include "tasks.hpp" |
|
|
|
|
|
|
|
#include "types.hpp" |
|
|
|
#include "ui_fsm.hpp" |
|
|
|
#include "ui_fsm.hpp" |
|
|
|
|
|
|
|
|
|
|
|
namespace audio { |
|
|
|
namespace audio { |
|
|
@ -62,7 +64,7 @@ auto Timer::SetLengthSeconds(uint32_t len) -> void { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Timer::SetLengthBytes(uint32_t len) -> void { |
|
|
|
auto Timer::SetLengthBytes(uint32_t len) -> void { |
|
|
|
total_duration_seconds_ = 0; |
|
|
|
total_duration_seconds_ = bytes_to_samples(len) / format_.sample_rate; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Timer::AddBytes(std::size_t bytes) -> void { |
|
|
|
auto Timer::AddBytes(std::size_t bytes) -> void { |
|
|
@ -84,14 +86,29 @@ auto Timer::AddBytes(std::size_t bytes) -> void { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (incremented) { |
|
|
|
if (incremented) { |
|
|
|
// ESP_LOGI("timer", "new time %lu", current_seconds_);
|
|
|
|
if (total_duration_seconds_ < current_seconds_) { |
|
|
|
|
|
|
|
total_duration_seconds_ = current_seconds_; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
events::Audio().Dispatch(PlaybackUpdate{ |
|
|
|
events::Audio().Dispatch(PlaybackUpdate{ |
|
|
|
.seconds_elapsed = current_seconds_, |
|
|
|
.seconds_elapsed = current_seconds_, |
|
|
|
.seconds_total = 0, |
|
|
|
.seconds_total = total_duration_seconds_, |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto Timer::bytes_to_samples(uint32_t bytes) -> uint32_t { |
|
|
|
|
|
|
|
uint32_t samples = bytes; |
|
|
|
|
|
|
|
samples /= format_.channels; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Samples must be aligned to 16 bits. The number of actual bytes per
|
|
|
|
|
|
|
|
// sample is therefore the bps divided by 16, rounded up (align to word),
|
|
|
|
|
|
|
|
// times two (convert to bytes).
|
|
|
|
|
|
|
|
uint8_t bytes_per_sample = ((format_.bits_per_sample + 16 - 1) / 16) * 2; |
|
|
|
|
|
|
|
samples /= bytes_per_sample; |
|
|
|
|
|
|
|
return samples; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto AudioTask::Start(IAudioSource* source, IAudioSink* sink) -> AudioTask* { |
|
|
|
auto AudioTask::Start(IAudioSource* source, IAudioSink* sink) -> AudioTask* { |
|
|
|
AudioTask* task = new AudioTask(source, sink); |
|
|
|
AudioTask* task = new AudioTask(source, sink); |
|
|
|
tasks::StartPersistent<tasks::Type::kAudio>([=]() { task->Main(); }); |
|
|
|
tasks::StartPersistent<tasks::Type::kAudio>([=]() { task->Main(); }); |
|
|
@ -103,7 +120,7 @@ AudioTask::AudioTask(IAudioSource* source, IAudioSink* sink) |
|
|
|
sink_(sink), |
|
|
|
sink_(sink), |
|
|
|
codec_(), |
|
|
|
codec_(), |
|
|
|
timer_(), |
|
|
|
timer_(), |
|
|
|
is_new_stream_(false), |
|
|
|
has_begun_decoding_(false), |
|
|
|
current_input_format_(), |
|
|
|
current_input_format_(), |
|
|
|
current_output_format_(), |
|
|
|
current_output_format_(), |
|
|
|
sample_buffer_(reinterpret_cast<std::byte*>( |
|
|
|
sample_buffer_(reinterpret_cast<std::byte*>( |
|
|
@ -114,38 +131,72 @@ AudioTask::AudioTask(IAudioSource* source, IAudioSink* sink) |
|
|
|
void AudioTask::Main() { |
|
|
|
void AudioTask::Main() { |
|
|
|
for (;;) { |
|
|
|
for (;;) { |
|
|
|
source_->Read( |
|
|
|
source_->Read( |
|
|
|
[this](StreamInfo::Format format) -> bool { |
|
|
|
[this](IAudioSource::Flags flags, InputStream& stream) -> void { |
|
|
|
if (current_input_format_ && format == *current_input_format_) { |
|
|
|
if (flags.is_start()) { |
|
|
|
// This is the continuation of previous data. We can handle it if
|
|
|
|
has_begun_decoding_ = false; |
|
|
|
// we are able to decode it, or if it doesn't need decoding.
|
|
|
|
if (!HandleNewStream(stream)) { |
|
|
|
return current_output_format_ == format || codec_ != nullptr; |
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto pcm = stream.info().format_as<StreamInfo::Pcm>(); |
|
|
|
|
|
|
|
if (pcm) { |
|
|
|
|
|
|
|
if (ForwardPcmStream(*pcm, stream.data())) { |
|
|
|
|
|
|
|
stream.consume(stream.data().size_bytes()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
timer_->SetLengthBytes( |
|
|
|
|
|
|
|
stream.info().total_length_bytes().value_or(0)); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!stream.info().format_as<StreamInfo::Encoded>() || !codec_) { |
|
|
|
|
|
|
|
// Either unknown stream format, or it's encoded but we don't have
|
|
|
|
|
|
|
|
// a decoder that supports it. Either way, bail out.
|
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!has_begun_decoding_) { |
|
|
|
|
|
|
|
if (BeginDecoding(stream)) { |
|
|
|
|
|
|
|
has_begun_decoding_ = true; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At this point the decoder has been initialised, and the sink has
|
|
|
|
|
|
|
|
// been correctly configured. All that remains is to throw samples
|
|
|
|
|
|
|
|
// into the sink as fast as possible.
|
|
|
|
|
|
|
|
if (!ContinueDecoding(stream)) { |
|
|
|
|
|
|
|
codec_.reset(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (flags.is_end()) { |
|
|
|
|
|
|
|
FinishDecoding(stream); |
|
|
|
|
|
|
|
events::Audio().Dispatch(internal::InputFileFinished{}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
portMAX_DELAY); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto AudioTask::HandleNewStream(const InputStream& stream) -> bool { |
|
|
|
// This must be a new stream of data. Reset everything to prepare to
|
|
|
|
// This must be a new stream of data. Reset everything to prepare to
|
|
|
|
// handle it.
|
|
|
|
// handle it.
|
|
|
|
current_input_format_ = format; |
|
|
|
current_input_format_ = stream.info().format(); |
|
|
|
is_new_stream_ = true; |
|
|
|
|
|
|
|
codec_.reset(); |
|
|
|
codec_.reset(); |
|
|
|
timer_.reset(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// What kind of data does this new stream contain?
|
|
|
|
// What kind of data does this new stream contain?
|
|
|
|
if (std::holds_alternative<StreamInfo::Pcm>(format)) { |
|
|
|
auto pcm = stream.info().format_as<StreamInfo::Pcm>(); |
|
|
|
// It's already decoded! We can handle this immediately if it
|
|
|
|
auto encoded = stream.info().format_as<StreamInfo::Encoded>(); |
|
|
|
// matches what we're currently sending to the sink. Otherwise, we
|
|
|
|
if (pcm) { |
|
|
|
// will need to wait for the sink to drain before we can reconfigure
|
|
|
|
// It's already decoded! We can always handle this.
|
|
|
|
// it.
|
|
|
|
|
|
|
|
if (current_output_format_ && format == *current_output_format_) { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} else if (xStreamBufferIsEmpty(sink_->stream())) { |
|
|
|
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} else { |
|
|
|
} else if (encoded) { |
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (std::holds_alternative<StreamInfo::Encoded>(format)) { |
|
|
|
|
|
|
|
// The stream has some kind of encoding. Whether or not we can
|
|
|
|
// The stream has some kind of encoding. Whether or not we can
|
|
|
|
// handle it is entirely down to whether or not we have a codec for
|
|
|
|
// handle it is entirely down to whether or not we have a codec for
|
|
|
|
// it.
|
|
|
|
// it.
|
|
|
|
auto encoding = std::get<StreamInfo::Encoded>(format); |
|
|
|
has_begun_decoding_ = false; |
|
|
|
auto codec = codecs::CreateCodecForType(encoding.type); |
|
|
|
auto codec = codecs::CreateCodecForType(encoded->type); |
|
|
|
if (codec) { |
|
|
|
if (codec) { |
|
|
|
ESP_LOGI(kTag, "successfully created codec for stream"); |
|
|
|
ESP_LOGI(kTag, "successfully created codec for stream"); |
|
|
|
codec_.reset(*codec); |
|
|
|
codec_.reset(*codec); |
|
|
@ -157,104 +208,136 @@ void AudioTask::Main() { |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// programmer error / skill issue :(
|
|
|
|
// programmer error / skill issue :(
|
|
|
|
ESP_LOGE(kTag, "stream has unknown format"); |
|
|
|
ESP_LOGE(kTag, "stream has unknown format"); |
|
|
|
current_input_format_ = format; |
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
[this](cpp::span<const std::byte> bytes) -> size_t { |
|
|
|
|
|
|
|
// PCM streams are simple, so handle them first.
|
|
|
|
|
|
|
|
if (std::holds_alternative<StreamInfo::Pcm>(*current_input_format_)) { |
|
|
|
|
|
|
|
// First we need to reconfigure the sink for this sample format.
|
|
|
|
|
|
|
|
// TODO(jacqueline): We should verify whether or not the sink can
|
|
|
|
|
|
|
|
// actually deal with this format first.
|
|
|
|
|
|
|
|
if (current_input_format_ != current_output_format_) { |
|
|
|
|
|
|
|
current_output_format_ = current_input_format_; |
|
|
|
|
|
|
|
sink_->Configure(*current_output_format_); |
|
|
|
|
|
|
|
timer_.reset(new Timer( |
|
|
|
|
|
|
|
std::get<StreamInfo::Pcm>(*current_output_format_))); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Stream the raw samples directly to the sink.
|
|
|
|
|
|
|
|
xStreamBufferSend(sink_->stream(), bytes.data(), bytes.size_bytes(), |
|
|
|
|
|
|
|
portMAX_DELAY); |
|
|
|
|
|
|
|
timer_->AddBytes(bytes.size_bytes()); |
|
|
|
|
|
|
|
return bytes.size_bytes(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Else, assume it's an encoded stream.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
size_t bytes_used = 0; |
|
|
|
auto AudioTask::BeginDecoding(InputStream& stream) -> bool { |
|
|
|
if (is_new_stream_) { |
|
|
|
auto res = codec_->BeginStream(stream.data()); |
|
|
|
// This is a new stream! First order of business is verifying that
|
|
|
|
stream.consume(res.first); |
|
|
|
// we can indeed decode it.
|
|
|
|
|
|
|
|
auto res = codec_->BeginStream(bytes); |
|
|
|
|
|
|
|
bytes_used += res.first; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (res.second.has_error()) { |
|
|
|
if (res.second.has_error()) { |
|
|
|
if (res.second.error() != codecs::ICodec::Error::kOutOfInput) { |
|
|
|
if (res.second.error() == codecs::ICodec::Error::kOutOfInput) { |
|
|
|
// Decoding the header failed, so we can't actually deal with
|
|
|
|
// Running out of input is fine; just return and we will try beginning the
|
|
|
|
// this stream after all. It could be malformed.
|
|
|
|
// stream again when we have more data.
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Decoding the header failed, so we can't actually deal with this stream
|
|
|
|
|
|
|
|
// after all. It could be malformed.
|
|
|
|
ESP_LOGE(kTag, "error beginning stream"); |
|
|
|
ESP_LOGE(kTag, "error beginning stream"); |
|
|
|
codec_.reset(); |
|
|
|
codec_.reset(); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
return bytes_used; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
is_new_stream_ = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
codecs::ICodec::OutputFormat format = res.second.value(); |
|
|
|
codecs::ICodec::OutputFormat format = res.second.value(); |
|
|
|
StreamInfo::Pcm pcm{ |
|
|
|
StreamInfo::Pcm new_format{ |
|
|
|
.channels = format.num_channels, |
|
|
|
.channels = format.num_channels, |
|
|
|
.bits_per_sample = format.bits_per_sample, |
|
|
|
.bits_per_sample = format.bits_per_sample, |
|
|
|
.sample_rate = format.sample_rate_hz, |
|
|
|
.sample_rate = format.sample_rate_hz, |
|
|
|
}; |
|
|
|
}; |
|
|
|
StreamInfo::Format new_format{pcm}; |
|
|
|
|
|
|
|
timer_.reset(new Timer{pcm}); |
|
|
|
if (!ConfigureSink(new_format)) { |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (format.duration_seconds) { |
|
|
|
if (format.duration_seconds) { |
|
|
|
timer_->SetLengthSeconds(*format.duration_seconds); |
|
|
|
timer_->SetLengthSeconds(*format.duration_seconds); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
timer_->SetLengthBytes(stream.info().total_length_bytes().value_or(0)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Now that we have the output format for decoded samples from this
|
|
|
|
return true; |
|
|
|
// stream, we need to see if they are compatible with what's already
|
|
|
|
} |
|
|
|
// in the sink stream.
|
|
|
|
|
|
|
|
if (new_format != current_output_format_) { |
|
|
|
auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { |
|
|
|
// The new format is different to the old one. Wait for the sink
|
|
|
|
while (!stream.data().empty()) { |
|
|
|
// to drain before continuing.
|
|
|
|
auto res = codec_->ContinueStream(stream.data(), |
|
|
|
while (!xStreamBufferIsEmpty(sink_->stream())) { |
|
|
|
{sample_buffer_, sample_buffer_len_}); |
|
|
|
ESP_LOGI(kTag, "waiting for sink stream to drain..."); |
|
|
|
|
|
|
|
// TODO(jacqueline): Get the sink drain ISR to notify us of this
|
|
|
|
stream.consume(res.first); |
|
|
|
// via semaphore instead of busy-ish waiting.
|
|
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(100)); |
|
|
|
if (res.second.has_error()) { |
|
|
|
|
|
|
|
if (res.second.error() == codecs::ICodec::Error::kOutOfInput) { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
xStreamBufferSend(sink_->stream(), sample_buffer_, |
|
|
|
|
|
|
|
res.second->bytes_written, portMAX_DELAY); |
|
|
|
|
|
|
|
timer_->AddBytes(res.second->bytes_written); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ESP_LOGI(kTag, "configuring sink"); |
|
|
|
|
|
|
|
current_output_format_ = new_format; |
|
|
|
|
|
|
|
sink_->Configure(new_format); |
|
|
|
|
|
|
|
timer_.reset( |
|
|
|
|
|
|
|
new Timer(std::get<StreamInfo::Pcm>(*current_output_format_))); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// At this point the decoder has been initialised, and the sink has
|
|
|
|
auto AudioTask::FinishDecoding(InputStream& stream) -> void { |
|
|
|
// been correctly configured. All that remains is to throw samples
|
|
|
|
// HACK: libmad requires each frame passed to it to have an additional
|
|
|
|
// into the sink as fast as possible.
|
|
|
|
// MAD_HEADER_GUARD (8) bytes after the end of the frame. Without these extra
|
|
|
|
while (bytes_used < bytes.size_bytes()) { |
|
|
|
// bytes, it will not decode the frame.
|
|
|
|
auto res = |
|
|
|
// The is fine for most of the stream, but at the end of the stream we don't
|
|
|
|
codec_->ContinueStream(bytes.subspan(bytes_used), |
|
|
|
// get a trailing 8 bytes for free.
|
|
|
|
{sample_buffer_, sample_buffer_len_}); |
|
|
|
if (stream.info().format_as<StreamInfo::Encoded>()->type == |
|
|
|
|
|
|
|
codecs::StreamType::kMp3) { |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "applying MAD_HEADER_GUARD fix"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<RawStream> mad_buffer; |
|
|
|
|
|
|
|
mad_buffer.reset(new RawStream(stream.data().size_bytes() + 8)); |
|
|
|
|
|
|
|
|
|
|
|
bytes_used += res.first; |
|
|
|
OutputStream writer{mad_buffer.get()}; |
|
|
|
|
|
|
|
std::copy(stream.data().begin(), stream.data().end(), |
|
|
|
|
|
|
|
writer.data().begin()); |
|
|
|
|
|
|
|
std::fill(writer.data().begin(), writer.data().end(), std::byte{0}); |
|
|
|
|
|
|
|
InputStream padded_stream{mad_buffer.get()}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto res = codec_->ContinueStream(stream.data(), |
|
|
|
|
|
|
|
{sample_buffer_, sample_buffer_len_}); |
|
|
|
if (res.second.has_error()) { |
|
|
|
if (res.second.has_error()) { |
|
|
|
return bytes_used; |
|
|
|
return; |
|
|
|
} else { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
xStreamBufferSend(sink_->stream(), sample_buffer_, |
|
|
|
xStreamBufferSend(sink_->stream(), sample_buffer_, |
|
|
|
res.second->bytes_written, portMAX_DELAY); |
|
|
|
res.second->bytes_written, portMAX_DELAY); |
|
|
|
timer_->AddBytes(res.second->bytes_written); |
|
|
|
timer_->AddBytes(res.second->bytes_written); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto AudioTask::ForwardPcmStream(StreamInfo::Pcm& format, |
|
|
|
|
|
|
|
cpp::span<const std::byte> samples) -> bool { |
|
|
|
|
|
|
|
// First we need to reconfigure the sink for this sample format.
|
|
|
|
|
|
|
|
if (format != current_output_format_) { |
|
|
|
|
|
|
|
if (!ConfigureSink(format)) { |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return bytes_used; |
|
|
|
// Stream the raw samples directly to the sink.
|
|
|
|
}, |
|
|
|
xStreamBufferSend(sink_->stream(), samples.data(), samples.size_bytes(), |
|
|
|
portMAX_DELAY); |
|
|
|
portMAX_DELAY); |
|
|
|
|
|
|
|
timer_->AddBytes(samples.size_bytes()); |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format) -> bool { |
|
|
|
|
|
|
|
if (format != current_output_format_) { |
|
|
|
|
|
|
|
// The new format is different to the old one. Wait for the sink to drain
|
|
|
|
|
|
|
|
// before continuing.
|
|
|
|
|
|
|
|
while (!xStreamBufferIsEmpty(sink_->stream())) { |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "waiting for sink stream to drain..."); |
|
|
|
|
|
|
|
// TODO(jacqueline): Get the sink drain ISR to notify us of this
|
|
|
|
|
|
|
|
// via semaphore instead of busy-ish waiting.
|
|
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(100)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ESP_LOGI(kTag, "configuring sink"); |
|
|
|
|
|
|
|
if (!sink_->Configure(format)) { |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_output_format_ = format; |
|
|
|
|
|
|
|
timer_.reset(new Timer(format)); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} // namespace audio
|
|
|
|
} // namespace audio
|
|
|
|