summaryrefslogtreecommitdiff
path: root/src/codecs
diff options
context:
space:
mode:
Diffstat (limited to 'src/codecs')
-rw-r--r--src/codecs/CMakeLists.txt1
-rw-r--r--src/codecs/codec.cpp4
-rw-r--r--src/codecs/foxenflac.cpp77
-rw-r--r--src/codecs/include/codec.hpp46
-rw-r--r--src/codecs/include/foxenflac.hpp19
-rw-r--r--src/codecs/include/mad.hpp31
-rw-r--r--src/codecs/include/opus.hpp33
-rw-r--r--src/codecs/include/source_buffer.hpp37
-rw-r--r--src/codecs/include/vorbis.hpp32
-rw-r--r--src/codecs/mad.cpp245
-rw-r--r--src/codecs/opus.cpp135
-rw-r--r--src/codecs/source_buffer.cpp75
-rw-r--r--src/codecs/vorbis.cpp103
13 files changed, 484 insertions, 354 deletions
diff --git a/src/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt
index 91d3f319..2d98198b 100644
--- a/src/codecs/CMakeLists.txt
+++ b/src/codecs/CMakeLists.txt
@@ -4,6 +4,7 @@
idf_component_register(
SRCS "codec.cpp" "mad.cpp" "foxenflac.cpp" "opus.cpp" "vorbis.cpp"
+ "source_buffer.cpp"
INCLUDE_DIRS "include"
REQUIRES "result" "span" "libmad" "libfoxenflac" "tremor" "opusfile")
diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp
index 9ac20097..a4c1a5cf 100644
--- a/src/codecs/codec.cpp
+++ b/src/codecs/codec.cpp
@@ -10,10 +10,10 @@
#include <optional>
#include "foxenflac.hpp"
-#include "opus.hpp"
#include "mad.hpp"
-#include "vorbis.hpp"
+#include "opus.hpp"
#include "types.hpp"
+#include "vorbis.hpp"
namespace codecs {
diff --git a/src/codecs/foxenflac.cpp b/src/codecs/foxenflac.cpp
index b676f82a..cc110920 100644
--- a/src/codecs/foxenflac.cpp
+++ b/src/codecs/foxenflac.cpp
@@ -19,23 +19,34 @@ namespace codecs {
static const char kTag[] = "flac";
FoxenFlacDecoder::FoxenFlacDecoder()
- : flac_(FX_FLAC_ALLOC(FLAC_MAX_BLOCK_SIZE, 2)) {}
+ : input_(), buffer_(), flac_(FX_FLAC_ALLOC(FLAC_MAX_BLOCK_SIZE, 2)) {}
FoxenFlacDecoder::~FoxenFlacDecoder() {
free(flac_);
}
-auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
- -> Result<OutputFormat> {
- uint32_t bytes_used = input.size_bytes();
- fx_flac_state_t state =
- fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(input.data()),
- &bytes_used, NULL, NULL);
+auto FoxenFlacDecoder::OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> {
+ input_ = input;
+
+ bool eof = false;
+ fx_flac_state_t state;
+ do {
+ eof = buffer_.Refill(input_.get());
+ buffer_.ConsumeBytes([&](cpp::span<std::byte> buf) -> size_t {
+ uint32_t bytes_used = buf.size();
+ state =
+ fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(buf.data()),
+ &bytes_used, NULL, NULL);
+ return bytes_used;
+ });
+ } while (state != FLAC_END_OF_METADATA && !eof);
+
if (state != FLAC_END_OF_METADATA) {
if (state == FLAC_ERR) {
- return {bytes_used, cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
} else {
- return {bytes_used, cpp::fail(Error::kOutOfInput)};
+ return cpp::fail(Error::kOutOfInput);
}
}
@@ -43,14 +54,12 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
int64_t fs = fx_flac_get_streaminfo(flac_, FLAC_KEY_SAMPLE_RATE);
if (channels == FLAC_INVALID_METADATA_KEY ||
fs == FLAC_INVALID_METADATA_KEY) {
- return {bytes_used, cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
}
OutputFormat format{
.num_channels = static_cast<uint8_t>(channels),
.sample_rate_hz = static_cast<uint32_t>(fs),
- .duration_seconds = {},
- .bits_per_second = {},
};
uint64_t num_samples = fx_flac_get_streaminfo(flac_, FLAC_KEY_N_SAMPLES);
@@ -58,38 +67,32 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
format.duration_seconds = num_samples / fs;
}
- return {bytes_used, format};
+ return format;
}
-auto FoxenFlacDecoder::ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> {
- cpp::span<int32_t> output_as_samples{
- reinterpret_cast<int32_t*>(output.data()), output.size_bytes() / 4};
- uint32_t bytes_read = input.size_bytes();
- uint32_t samples_written = output_as_samples.size();
-
- fx_flac_state_t state =
- fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(input.data()),
- &bytes_read, output_as_samples.data(), &samples_written);
- if (state == FLAC_ERR) {
- return {bytes_read, cpp::fail(Error::kMalformedData)};
- }
+auto FoxenFlacDecoder::DecodeTo(cpp::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
+ bool is_eof = buffer_.Refill(input_.get());
- if (samples_written > 0) {
- return {bytes_read,
- OutputInfo{.samples_written = samples_written,
- .is_finished_writing = state == FLAC_END_OF_FRAME}};
+ fx_flac_state_t state;
+ uint32_t samples_written = output.size();
+
+ buffer_.ConsumeBytes([&](cpp::span<std::byte> buf) -> size_t {
+ uint32_t bytes_read = buf.size_bytes();
+ state = fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(buf.data()),
+ &bytes_read, output.data(), &samples_written);
+ return bytes_read;
+ });
+ if (state == FLAC_ERR) {
+ return cpp::fail(Error::kMalformedData);
}
- // No error, but no samples written. We must be out of data.
- return {bytes_read, cpp::fail(Error::kOutOfInput)};
+ return OutputInfo{.samples_written = samples_written,
+ .is_stream_finished = samples_written == 0 && is_eof};
}
-auto FoxenFlacDecoder::SeekStream(cpp::span<const std::byte> input,
- std::size_t target_sample) -> Result<void> {
- // TODO(jacqueline): Implement me.
- return {0, {}};
+auto FoxenFlacDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
+ return {};
}
} // namespace codecs
diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp
index 32ebef69..ece3d4fe 100644
--- a/src/codecs/include/codec.hpp
+++ b/src/codecs/include/codec.hpp
@@ -24,6 +24,34 @@
namespace codecs {
/*
+ * Interface for an abstract source of file-like data.
+ */
+class IStream {
+ public:
+ IStream(StreamType t) : t_(t) {}
+ virtual ~IStream() {}
+
+ auto type() -> StreamType { return t_; }
+
+ virtual auto Read(cpp::span<std::byte> dest) -> ssize_t = 0;
+
+ virtual auto CanSeek() -> bool = 0;
+
+ enum class SeekFrom {
+ kStartOfStream,
+ kEndOfStream,
+ kCurrentPosition,
+ };
+
+ virtual auto SeekTo(int64_t destination, SeekFrom from) -> void = 0;
+
+ virtual auto CurrentPosition() -> int64_t = 0;
+
+ protected:
+ StreamType t_;
+};
+
+/*
* Common interface to be implemented by all audio decoders.
*/
class ICodec {
@@ -63,32 +91,30 @@ class ICodec {
struct OutputFormat {
uint8_t num_channels;
uint32_t sample_rate_hz;
-
std::optional<uint32_t> duration_seconds;
- std::optional<uint32_t> bits_per_second;
+
+ bool operator==(const OutputFormat&) const = default;
};
/*
* Decodes metadata or headers from the given input stream, and returns the
* format for the samples that will be decoded from it.
*/
- virtual auto BeginStream(cpp::span<const std::byte> input)
- -> Result<OutputFormat> = 0;
+ virtual auto OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> = 0;
struct OutputInfo {
std::size_t samples_written;
- bool is_finished_writing;
+ bool is_stream_finished;
};
/*
* Writes PCM samples to the given output buffer.
*/
- virtual auto ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> = 0;
+ virtual auto DecodeTo(cpp::span<sample::Sample> destination)
+ -> cpp::result<OutputInfo, Error> = 0;
- virtual auto SeekStream(cpp::span<const std::byte> input,
- std::size_t target_sample) -> Result<void> = 0;
+ virtual auto SeekTo(size_t target_sample) -> cpp::result<void, Error> = 0;
};
auto CreateCodecForType(StreamType type) -> std::optional<ICodec*>;
diff --git a/src/codecs/include/foxenflac.hpp b/src/codecs/include/foxenflac.hpp
index abfa6d80..7522d967 100644
--- a/src/codecs/include/foxenflac.hpp
+++ b/src/codecs/include/foxenflac.hpp
@@ -15,6 +15,7 @@
#include "foxen/flac.h"
#include "sample.hpp"
+#include "source_buffer.hpp"
#include "span.hpp"
#include "codec.hpp"
@@ -26,13 +27,21 @@ class FoxenFlacDecoder : public ICodec {
FoxenFlacDecoder();
~FoxenFlacDecoder();
- auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
- auto ContinueStream(cpp::span<const std::byte>, cpp::span<sample::Sample>)
- -> Result<OutputInfo> override;
- auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
- -> Result<void> override;
+ auto OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> override;
+
+ auto DecodeTo(cpp::span<sample::Sample> destination)
+ -> cpp::result<OutputInfo, Error> override;
+
+ auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
+
+ FoxenFlacDecoder(const FoxenFlacDecoder&) = delete;
+ FoxenFlacDecoder& operator=(const FoxenFlacDecoder&) = delete;
private:
+ std::shared_ptr<IStream> input_;
+ SourceBuffer buffer_;
+
fx_flac_t* flac_;
};
diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp
index b81e4acb..2a8813e9 100644
--- a/src/codecs/include/mad.hpp
+++ b/src/codecs/include/mad.hpp
@@ -14,6 +14,7 @@
#include "mad.h"
#include "sample.hpp"
+#include "source_buffer.hpp"
#include "span.hpp"
#include "codec.hpp"
@@ -25,33 +26,31 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder();
~MadMp3Decoder();
- /*
- * Returns the output format for the next frame in the stream. MP3 streams
- * may represent multiple distinct tracks, with different bitrates, and so we
- * handle the stream only on a frame-by-frame basis.
- */
- auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
+ auto OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> override;
- /*
- * Writes samples for the current frame.
- */
- auto ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> override;
+ auto DecodeTo(cpp::span<sample::Sample> destination)
+ -> cpp::result<OutputInfo, Error> override;
- auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
- -> Result<void> override;
+ auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
+
+ MadMp3Decoder(const MadMp3Decoder&) = delete;
+ MadMp3Decoder& operator=(const MadMp3Decoder&) = delete;
private:
auto GetVbrLength(const mad_header& header) -> std::optional<uint32_t>;
+ auto GetBytesUsed() -> std::size_t;
+
+ std::shared_ptr<IStream> input_;
+ SourceBuffer buffer_;
mad_stream stream_;
mad_frame frame_;
mad_synth synth_;
int current_sample_;
-
- auto GetBytesUsed(std::size_t) -> std::size_t;
+ bool is_eof_;
+ bool is_eos_;
};
} // namespace codecs
diff --git a/src/codecs/include/opus.hpp b/src/codecs/include/opus.hpp
index 051cd0b9..45b1b07a 100644
--- a/src/codecs/include/opus.hpp
+++ b/src/codecs/include/opus.hpp
@@ -26,30 +26,21 @@ class XiphOpusDecoder : public ICodec {
XiphOpusDecoder();
~XiphOpusDecoder();
- /*
- * Returns the output format for the next frame in the stream. MP3 streams
- * may represent multiple distinct tracks, with different bitrates, and so we
- * handle the stream only on a frame-by-frame basis.
- */
- auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
-
- /*
- * Writes samples for the current frame.
- */
- auto ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> override;
-
- auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
- -> Result<void> override;
-
- auto ReadCallback() -> cpp::span<const std::byte>;
- auto AfterReadCallback(size_t bytes_read) -> void;
+ auto OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> override;
+
+ auto DecodeTo(cpp::span<sample::Sample> destination)
+ -> cpp::result<OutputInfo, Error> override;
+
+ auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
+
+ XiphOpusDecoder(const XiphOpusDecoder&) = delete;
+ XiphOpusDecoder& operator=(const XiphOpusDecoder&) = delete;
private:
+ std::shared_ptr<IStream> input_;
OggOpusFile* opus_;
- cpp::span<const std::byte> input_;
- size_t pos_in_input_;
+ uint8_t num_channels_;
};
} // namespace codecs
diff --git a/src/codecs/include/source_buffer.hpp b/src/codecs/include/source_buffer.hpp
new file mode 100644
index 00000000..d0d7635a
--- /dev/null
+++ b/src/codecs/include/source_buffer.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+
+#include "span.hpp"
+
+#include "codec.hpp"
+
+namespace codecs {
+
+class SourceBuffer {
+ public:
+ SourceBuffer();
+ ~SourceBuffer();
+
+ auto Refill(IStream* src) -> bool;
+ auto AddBytes(std::function<size_t(cpp::span<std::byte>)> writer) -> void;
+ auto ConsumeBytes(std::function<size_t(cpp::span<std::byte>)> reader) -> void;
+
+ SourceBuffer(const SourceBuffer&) = delete;
+ SourceBuffer& operator=(const SourceBuffer&) = delete;
+
+ private:
+ const cpp::span<std::byte> buffer_;
+ size_t bytes_in_buffer_;
+ size_t offset_of_bytes_;
+};
+
+} // namespace codecs
diff --git a/src/codecs/include/vorbis.hpp b/src/codecs/include/vorbis.hpp
index ab15af19..2f93c37e 100644
--- a/src/codecs/include/vorbis.hpp
+++ b/src/codecs/include/vorbis.hpp
@@ -28,30 +28,20 @@ class TremorVorbisDecoder : public ICodec {
TremorVorbisDecoder();
~TremorVorbisDecoder();
- /*
- * Returns the output format for the next frame in the stream. MP3 streams
- * may represent multiple distinct tracks, with different bitrates, and so we
- * handle the stream only on a frame-by-frame basis.
- */
- auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
-
- /*
- * Writes samples for the current frame.
- */
- auto ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> override;
-
- auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
- -> Result<void> override;
-
- auto ReadCallback() -> cpp::span<const std::byte>;
- auto AfterReadCallback(size_t bytes_read) -> void;
+ auto OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> override;
+
+ auto DecodeTo(cpp::span<sample::Sample> destination)
+ -> cpp::result<OutputInfo, Error> override;
+
+ auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
+
+ TremorVorbisDecoder(const TremorVorbisDecoder&) = delete;
+ TremorVorbisDecoder& operator=(const TremorVorbisDecoder&) = delete;
private:
+ std::shared_ptr<IStream> input_;
OggVorbis_File vorbis_;
- cpp::span<const std::byte> input_;
- size_t pos_in_input_;
};
} // namespace codecs
diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp
index a2739bcd..ce3a9cac 100644
--- a/src/codecs/mad.cpp
+++ b/src/codecs/mad.cpp
@@ -22,7 +22,10 @@
namespace codecs {
-MadMp3Decoder::MadMp3Decoder() {
+static constexpr char kTag[] = "mad";
+
+MadMp3Decoder::MadMp3Decoder()
+ : input_(), buffer_(), current_sample_(-1), is_eof_(false), is_eos_(false) {
mad_stream_init(&stream_);
mad_frame_init(&frame_);
mad_synth_init(&synth_);
@@ -33,185 +36,145 @@ MadMp3Decoder::~MadMp3Decoder() {
mad_synth_finish(&synth_);
}
-auto MadMp3Decoder::GetBytesUsed(std::size_t buffer_size) -> std::size_t {
+auto MadMp3Decoder::GetBytesUsed() -> std::size_t {
if (stream_.next_frame) {
- std::size_t remaining = stream_.bufend - stream_.next_frame;
- return buffer_size - remaining;
+ return stream_.next_frame - stream_.buffer;
} else {
return stream_.bufend - stream_.buffer;
}
}
-auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> input)
- -> Result<OutputFormat> {
- mad_stream_buffer(&stream_,
- reinterpret_cast<const unsigned char*>(input.data()),
- input.size_bytes());
- // Whatever was last synthesized is now invalid, so ensure we don't try to
- // send it.
- current_sample_ = -1;
+auto MadMp3Decoder::OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, ICodec::Error> {
+ input_ = input;
// To get the output format for MP3 streams, we simply need to decode the
// first frame header.
mad_header header;
mad_header_init(&header);
- while (mad_header_decode(&header, &stream_) < 0) {
- if (MAD_RECOVERABLE(stream_.error)) {
- // Recoverable errors are usually malformed parts of the stream.
- // We can recover from them by just retrying the decode.
- continue;
- }
- if (stream_.error == MAD_ERROR_BUFLEN) {
- return {GetBytesUsed(input.size_bytes()), cpp::fail(Error::kOutOfInput)};
- }
- return {GetBytesUsed(input.size_bytes()), cpp::fail(Error::kMalformedData)};
+ bool eof = false;
+ bool got_header = false;
+ while (!eof && !got_header) {
+ eof = buffer_.Refill(input_.get());
+
+ buffer_.ConsumeBytes([&](cpp::span<std::byte> buf) -> size_t {
+ mad_stream_buffer(&stream_,
+ reinterpret_cast<const unsigned char*>(buf.data()),
+ buf.size_bytes());
+
+ while (mad_header_decode(&header, &stream_) < 0) {
+ if (MAD_RECOVERABLE(stream_.error)) {
+ // Recoverable errors are usually malformed parts of the stream.
+ // We can recover from them by just retrying the decode.
+ continue;
+ }
+ if (stream_.error == MAD_ERROR_BUFLEN) {
+ return GetBytesUsed();
+ }
+ eof = true;
+ return 0;
+ }
+
+ got_header = true;
+ return GetBytesUsed();
+ });
+ }
+
+ if (!got_header) {
+ return cpp::fail(ICodec::Error::kMalformedData);
}
uint8_t channels = MAD_NCHANNELS(&header);
OutputFormat output{
.num_channels = channels,
.sample_rate_hz = header.samplerate,
- .duration_seconds = {},
- .bits_per_second = {},
};
auto vbr_length = GetVbrLength(header);
if (vbr_length) {
output.duration_seconds = vbr_length;
- } else {
- output.bits_per_second = header.bitrate;
}
-
- return {GetBytesUsed(input.size_bytes()), output};
+ return output;
}
-auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> {
- std::size_t bytes_read = 0;
- if (current_sample_ < 0) {
- mad_stream_buffer(&stream_,
- reinterpret_cast<const unsigned char*>(input.data()),
- input.size());
-
- // Decode the next frame. To signal errors, this returns -1 and
- // stashes an error code in the stream structure.
- while (mad_frame_decode(&frame_, &stream_) < 0) {
- if (MAD_RECOVERABLE(stream_.error)) {
- // Recoverable errors are usually malformed parts of the stream.
- // We can recover from them by just retrying the decode.
- continue;
- }
- if (stream_.error == MAD_ERROR_BUFLEN) {
- // The decoder ran out of bytes before it completed a frame. We
- // need to return back to the caller to give us more data.
- return {GetBytesUsed(input.size_bytes()),
- cpp::fail(Error::kOutOfInput)};
+auto MadMp3Decoder::DecodeTo(cpp::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
+ if (current_sample_ < 0 && !is_eos_) {
+ if (!is_eof_) {
+ is_eof_ = buffer_.Refill(input_.get());
+ if (is_eof_) {
+ buffer_.AddBytes([&](cpp::span<std::byte> buf) -> size_t {
+ if (buf.size() < 8) {
+ is_eof_ = false;
+ return 0;
+ }
+ ESP_LOGI(kTag, "adding MAD_HEADER_GUARD");
+ std::fill_n(buf.begin(), 8, std::byte(0));
+ return 8;
+ });
}
- // The error is unrecoverable. Give up.
- return {GetBytesUsed(input.size_bytes()),
- cpp::fail(Error::kMalformedData)};
}
- // We've successfully decoded a frame! Now synthesize samples to write out.
- mad_synth_frame(&synth_, &frame_);
- current_sample_ = 0;
- bytes_read = GetBytesUsed(input.size_bytes());
+ buffer_.ConsumeBytes([&](cpp::span<std::byte> buf) -> size_t {
+ mad_stream_buffer(&stream_,
+ reinterpret_cast<const unsigned char*>(buf.data()),
+ buf.size());
+
+ // Decode the next frame. To signal errors, this returns -1 and
+ // stashes an error code in the stream structure.
+ while (mad_frame_decode(&frame_, &stream_) < 0) {
+ if (MAD_RECOVERABLE(stream_.error)) {
+ // Recoverable errors are usually malformed parts of the stream.
+ // We can recover from them by just retrying the decode.
+ continue;
+ }
+ if (stream_.error == MAD_ERROR_BUFLEN) {
+ if (is_eof_) {
+ ESP_LOGI(kTag, "BUFLEN while eof; this is eos");
+ is_eos_ = true;
+ }
+ return GetBytesUsed();
+ }
+ // The error is unrecoverable. Give up.
+ is_eof_ = true;
+ is_eos_ = true;
+ return 0;
+ }
+
+ // We've successfully decoded a frame! Now synthesize samples to write
+ // out.
+ mad_synth_frame(&synth_, &frame_);
+ current_sample_ = 0;
+ return GetBytesUsed();
+ });
}
size_t output_sample = 0;
- while (current_sample_ < synth_.pcm.length) {
- if (output_sample + synth_.pcm.channels >= output.size()) {
- // We can't fit the next full frame into the buffer.
- return {bytes_read, OutputInfo{.samples_written = output_sample,
- .is_finished_writing = false}};
- }
+ if (current_sample_ >= 0) {
+ while (current_sample_ < synth_.pcm.length) {
+ if (output_sample + synth_.pcm.channels >= output.size()) {
+ // We can't fit the next full frame into the buffer.
+ return OutputInfo{.samples_written = output_sample,
+ .is_stream_finished = false};
+ }
- for (int channel = 0; channel < synth_.pcm.channels; channel++) {
- output[output_sample++] =
- sample::FromMad(synth_.pcm.samples[channel][current_sample_]);
+ for (int channel = 0; channel < synth_.pcm.channels; channel++) {
+ output[output_sample++] =
+ sample::FromMad(synth_.pcm.samples[channel][current_sample_]);
+ }
+ current_sample_++;
}
- current_sample_++;
}
// We wrote everything! Reset, ready for the next frame.
current_sample_ = -1;
- return {bytes_read, OutputInfo{.samples_written = output_sample,
- .is_finished_writing = true}};
+ return OutputInfo{.samples_written = output_sample,
+ .is_stream_finished = is_eos_};
}
-auto MadMp3Decoder::SeekStream(cpp::span<const std::byte> input,
- std::size_t target_sample) -> Result<void> {
- mad_stream_buffer(&stream_,
- reinterpret_cast<const unsigned char*>(input.data()),
- input.size());
- std::size_t current_sample = 0;
- std::size_t samples_per_frame = 0;
- while (true) {
- current_sample += samples_per_frame;
-
- // First, decode the header for this frame.
- mad_header header;
- mad_header_init(&header);
- while (mad_header_decode(&header, &stream_) < 0) {
- if (MAD_RECOVERABLE(stream_.error)) {
- // Recoverable errors are usually malformed parts of the stream.
- // We can recover from them by just retrying the decode.
- continue;
- } else {
- // Don't bother checking for other errors; if the first part of the
- // stream doesn't even contain a header then something's gone wrong.
- return {GetBytesUsed(input.size_bytes()),
- cpp::fail(Error::kMalformedData)};
- }
- }
-
- // Calculate samples per frame if we haven't already.
- if (samples_per_frame == 0) {
- samples_per_frame = 32 * MAD_NSBSAMPLES(&header);
- }
-
- // Work out how close we are to the target.
- std::size_t samples_to_go = target_sample - current_sample;
- std::size_t frames_to_go = samples_to_go / samples_per_frame;
- if (frames_to_go > 3) {
- // The target is far in the distance. Keep skipping through headers only.
- continue;
- }
-
- // The target is within the next few frames. We should decode these, as per
- // the LAME FAQ (https://lame.sourceforge.io/tech-FAQ.txt):
- // > The MP3 data for frame N is not stored in frame N, but can be spread
- // > over several frames. In a typical case, the data for frame N will
- // > have 20% of it stored in frame N-1 and 80% stored in frame N.
- while (mad_frame_decode(&frame_, &stream_) < 0) {
- if (MAD_RECOVERABLE(stream_.error)) {
- continue;
- }
- if (stream_.error == MAD_ERROR_BUFLEN) {
- return {GetBytesUsed(input.size_bytes()),
- cpp::fail(Error::kOutOfInput)};
- }
- // The error is unrecoverable. Give up.
- return {GetBytesUsed(input.size_bytes()),
- cpp::fail(Error::kMalformedData)};
- }
-
- if (frames_to_go <= 1) {
- // The target is within the next couple of frames. We should start
- // synthesizing a frame early because this guy says so:
- // https://lists.mars.org/hyperkitty/list/mad-dev@lists.mars.org/message/UZSHXZTIZEF7FZ4KFOR65DUCKAY2OCUT/
- mad_synth_frame(&synth_, &frame_);
- }
-
- if (frames_to_go == 0) {
- // The target is actually within this frame! Set up for the ContinueStream
- // call.
- current_sample_ =
- (target_sample > current_sample) ? target_sample - current_sample : 0;
- return {GetBytesUsed(input.size_bytes()), {}};
- }
- }
+auto MadMp3Decoder::SeekTo(std::size_t target_sample)
+ -> cpp::result<void, Error> {
+ return {};
}
/*
diff --git a/src/codecs/opus.cpp b/src/codecs/opus.cpp
index a71c5fc0..70ec9e45 100644
--- a/src/codecs/opus.cpp
+++ b/src/codecs/opus.cpp
@@ -8,6 +8,7 @@
#include <stdint.h>
#include <sys/_stdint.h>
+#include <sys/unistd.h>
#include <cstdint>
#include <cstring>
@@ -27,23 +28,49 @@ namespace codecs {
static constexpr char kTag[] = "opus";
-int read_cb(void* instance, unsigned char* ptr, int nbytes) {
- XiphOpusDecoder* dec = reinterpret_cast<XiphOpusDecoder*>(instance);
- auto input = dec->ReadCallback();
- size_t amount_to_read = std::min<size_t>(nbytes, input.size_bytes());
- std::memcpy(ptr, input.data(), amount_to_read);
- dec->AfterReadCallback(amount_to_read);
- return amount_to_read;
+static int read_cb(void* src, unsigned char* ptr, int nbytes) {
+ IStream* source = reinterpret_cast<IStream*>(src);
+ return source->Read(
+ {reinterpret_cast<std::byte*>(ptr), static_cast<size_t>(nbytes)});
+}
+
+static int seek_cb(void* src, int64_t offset, int whence) {
+ IStream* source = reinterpret_cast<IStream*>(src);
+ if (!source->CanSeek()) {
+ return -1;
+ }
+ IStream::SeekFrom from;
+ switch (whence) {
+ case SEEK_CUR:
+ from = IStream::SeekFrom::kCurrentPosition;
+ break;
+ case SEEK_END:
+ from = IStream::SeekFrom::kEndOfStream;
+ break;
+ case SEEK_SET:
+ from = IStream::SeekFrom::kStartOfStream;
+ break;
+ default:
+ return -1;
+ }
+ source->SeekTo(offset, from);
+ return 0;
+}
+
+static int64_t tell_cb(void* src) {
+ IStream* source = reinterpret_cast<IStream*>(src);
+ return source->CurrentPosition();
}
static const OpusFileCallbacks kCallbacks{
.read = read_cb,
- .seek = NULL,
- .tell = NULL, // Not seekable
+ .seek = seek_cb,
+ .tell = tell_cb,
.close = NULL,
};
-XiphOpusDecoder::XiphOpusDecoder() : opus_(nullptr) {}
+XiphOpusDecoder::XiphOpusDecoder()
+ : input_(nullptr), opus_(nullptr), num_channels_() {}
XiphOpusDecoder::~XiphOpusDecoder() {
if (opus_ != nullptr) {
@@ -51,12 +78,12 @@ XiphOpusDecoder::~XiphOpusDecoder() {
}
}
-auto XiphOpusDecoder::BeginStream(const cpp::span<const std::byte> input)
- -> Result<OutputFormat> {
+auto XiphOpusDecoder::OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> {
+ input_ = input;
+
int res;
- opus_ = op_open_callbacks(
- this, &kCallbacks, reinterpret_cast<const unsigned char*>(input.data()),
- input.size(), &res);
+ opus_ = op_open_callbacks(input.get(), &kCallbacks, nullptr, 0, &res);
if (res < 0) {
std::string err;
@@ -64,60 +91,72 @@ auto XiphOpusDecoder::BeginStream(const cpp::span<const std::byte> input)
case OP_EREAD:
err = "OP_EREAD";
break;
+ case OP_EFAULT:
+ err = "OP_EFAULT";
+ break;
+ case OP_EIMPL:
+ err = "OP_EIMPL";
+ break;
+ case OP_EINVAL:
+ err = "OP_EINVAL";
+ break;
+ case OP_ENOTFORMAT:
+ err = "OP_ENOTFORMAT";
+ break;
+ case OP_EBADHEADER:
+ err = "OP_EBADHEADER";
+ break;
+ case OP_EVERSION:
+ err = "OP_EVERSION";
+ break;
+ case OP_EBADLINK:
+ err = "OP_EBADLINK";
+ break;
+ case OP_EBADTIMESTAMP:
+ err = "OP_BADTIMESTAMP";
+ break;
default:
err = "unknown";
}
ESP_LOGE(kTag, "error beginning stream: %s", err.c_str());
- return {input.size(), cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
}
- return {input.size(), OutputFormat{
- .num_channels = 2,
- .sample_rate_hz = 48000,
- }};
+ num_channels_ = std::min<uint8_t>(2, op_channel_count(opus_, -1));
+
+ return OutputFormat{
+ .num_channels = num_channels_,
+ .sample_rate_hz = 48000,
+ };
}
-auto XiphOpusDecoder::ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> {
+auto XiphOpusDecoder::DecodeTo(cpp::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
cpp::span<int16_t> staging_buffer{
reinterpret_cast<int16_t*>(output.subspan(output.size() / 2).data()),
output.size_bytes() / 2};
- input_ = input;
- pos_in_input_ = 0;
-
- int bytes_written =
+ int samples_written =
op_read_stereo(opus_, staging_buffer.data(), staging_buffer.size());
- if (bytes_written < 0) {
- ESP_LOGE(kTag, "read failed %i", bytes_written);
- return {pos_in_input_, cpp::fail(Error::kMalformedData)};
- } else if (bytes_written == 0) {
- return {pos_in_input_, cpp::fail(Error::kOutOfInput)};
+
+ if (samples_written < 0) {
+ ESP_LOGE(kTag, "read failed %i", samples_written);
+ return cpp::fail(Error::kMalformedData);
}
- for (int i = 0; i < bytes_written / 2; i++) {
+ samples_written *= num_channels_;
+ for (int i = 0; i < samples_written; i++) {
output[i] = sample::FromSigned(staging_buffer[i], 16);
}
- return {pos_in_input_,
- OutputInfo{
- .samples_written = static_cast<size_t>(bytes_written / 2),
- .is_finished_writing = bytes_written == 0,
- }};
+ return OutputInfo{
+ .samples_written = static_cast<size_t>(samples_written / 2),
+ .is_stream_finished = samples_written == 0,
+ };
}
-auto XiphOpusDecoder::SeekStream(cpp::span<const std::byte> input,
- std::size_t target_sample) -> Result<void> {
+auto XiphOpusDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
return {};
}
-auto XiphOpusDecoder::ReadCallback() -> cpp::span<const std::byte> {
- return input_.subspan(pos_in_input_);
-}
-
-auto XiphOpusDecoder::AfterReadCallback(size_t bytes_read) -> void {
- pos_in_input_ += bytes_read;
-}
-
} // namespace codecs
diff --git a/src/codecs/source_buffer.cpp b/src/codecs/source_buffer.cpp
new file mode 100644
index 00000000..5955523e
--- /dev/null
+++ b/src/codecs/source_buffer.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "source_buffer.hpp"
+#include <sys/_stdint.h>
+
+#include <algorithm>
+#include <cstring>
+
+#include "esp_heap_caps.h"
+#include "esp_log.h"
+
+#include "codec.hpp"
+
+namespace codecs {
+
+static constexpr char kTag[] = "dec_buf";
+static constexpr size_t kBufferSize = 1024 * 8;
+
+SourceBuffer::SourceBuffer()
+ : buffer_(reinterpret_cast<std::byte*>(
+ heap_caps_malloc(kBufferSize, MALLOC_CAP_SPIRAM)),
+ kBufferSize),
+ bytes_in_buffer_(0),
+ offset_of_bytes_(0) {
+ assert(buffer_.data() != nullptr);
+}
+
+SourceBuffer::~SourceBuffer() {
+ free(buffer_.data());
+}
+
+auto SourceBuffer::Refill(IStream* src) -> bool {
+ if (bytes_in_buffer_ == buffer_.size_bytes()) {
+ return false;
+ }
+ bool eof = false;
+ AddBytes([&](cpp::span<std::byte> buf) -> size_t {
+ size_t bytes_read = src->Read(buf);
+ eof = bytes_read == 0;
+ return bytes_read;
+ });
+ return eof;
+}
+
+auto SourceBuffer::AddBytes(std::function<size_t(cpp::span<std::byte>)> writer)
+ -> void {
+ if (offset_of_bytes_ > 0) {
+ std::memmove(buffer_.data(), buffer_.data() + offset_of_bytes_,
+ bytes_in_buffer_);
+ offset_of_bytes_ = 0;
+ }
+ size_t added_bytes = std::invoke(writer, buffer_.subspan(bytes_in_buffer_));
+ assert(bytes_in_buffer_ + added_bytes <= buffer_.size_bytes());
+ bytes_in_buffer_ += added_bytes;
+}
+
+auto SourceBuffer::ConsumeBytes(
+ std::function<size_t(cpp::span<std::byte>)> reader) -> void {
+ size_t bytes_consumed = std::invoke(
+ reader, buffer_.subspan(offset_of_bytes_).first(bytes_in_buffer_));
+ assert(bytes_consumed <= bytes_in_buffer_);
+
+ bytes_in_buffer_ -= bytes_consumed;
+ if (bytes_in_buffer_ == 0) {
+ offset_of_bytes_ = 0;
+ } else {
+ offset_of_bytes_ += bytes_consumed;
+ }
+}
+
+} // namespace codecs
diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp
index 88ffbec4..6fa3256a 100644
--- a/src/codecs/vorbis.cpp
+++ b/src/codecs/vorbis.cpp
@@ -34,43 +34,59 @@ namespace codecs {
static constexpr char kTag[] = "vorbis";
-size_t read_cb(void* ptr, size_t size, size_t nmemb, void* instance) {
- TremorVorbisDecoder* dec = reinterpret_cast<TremorVorbisDecoder*>(instance);
- auto input = dec->ReadCallback();
- size_t amount_to_read = std::min<size_t>(size * nmemb, input.size_bytes());
- std::memcpy(ptr, input.data(), amount_to_read);
- dec->AfterReadCallback(amount_to_read);
- return amount_to_read;
+static size_t read_cb(void* ptr, size_t size, size_t nmemb, void* instance) {
+ IStream* source = reinterpret_cast<IStream*>(instance);
+ return source->Read({reinterpret_cast<std::byte*>(ptr), size * nmemb});
}
-int seek_cb(void* instance, ogg_int64_t offset, int whence) {
- // Seeking is handled separately.
- return -1;
+static int seek_cb(void* instance, ogg_int64_t offset, int whence) {
+ IStream* source = reinterpret_cast<IStream*>(instance);
+ if (!source->CanSeek()) {
+ return -1;
+ }
+ IStream::SeekFrom from;
+ switch (whence) {
+ case SEEK_CUR:
+ from = IStream::SeekFrom::kCurrentPosition;
+ break;
+ case SEEK_END:
+ from = IStream::SeekFrom::kEndOfStream;
+ break;
+ case SEEK_SET:
+ from = IStream::SeekFrom::kStartOfStream;
+ break;
+ default:
+ return -1;
+ }
+ source->SeekTo(offset, from);
+ return 0;
}
-int close_cb(void* instance) {
+static int close_cb(void* src) {
return 0;
}
+static long tell_cb(void* src) {
+ IStream* source = reinterpret_cast<IStream*>(src);
+ return source->CurrentPosition();
+}
+
static const ov_callbacks kCallbacks{
.read_func = read_cb,
.seek_func = seek_cb,
.close_func = close_cb,
- .tell_func = NULL, // Not seekable
+ .tell_func = tell_cb, // Not seekable
};
-TremorVorbisDecoder::TremorVorbisDecoder()
- : vorbis_(), input_(), pos_in_input_(0) {}
+TremorVorbisDecoder::TremorVorbisDecoder() : input_(), vorbis_() {}
TremorVorbisDecoder::~TremorVorbisDecoder() {
ov_clear(&vorbis_);
}
-auto TremorVorbisDecoder::BeginStream(const cpp::span<const std::byte> input)
- -> Result<OutputFormat> {
- int res = ov_open_callbacks(this, &vorbis_,
- reinterpret_cast<const char*>(input.data()),
- input.size(), kCallbacks);
+auto TremorVorbisDecoder::OpenStream(std::shared_ptr<IStream> input)
+ -> cpp::result<OutputFormat, Error> {
+ int res = ov_open_callbacks(input.get(), &vorbis_, NULL, 0, kCallbacks);
if (res < 0) {
std::string err;
switch (res) {
@@ -93,70 +109,51 @@ auto TremorVorbisDecoder::BeginStream(const cpp::span<const std::byte> input)
err = "unknown";
}
ESP_LOGE(kTag, "error beginning stream: %s", err.c_str());
- return {input.size(), cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
}
vorbis_info* info = ov_info(&vorbis_, -1);
if (info == NULL) {
ESP_LOGE(kTag, "failed to get stream info");
- return {input.size(), cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
}
- return {input.size(),
- OutputFormat{
- .num_channels = static_cast<uint8_t>(info->channels),
- .sample_rate_hz = static_cast<uint32_t>(info->rate),
- .bits_per_second = info->bitrate_nominal,
- }};
+ return OutputFormat{
+ .num_channels = static_cast<uint8_t>(info->channels),
+ .sample_rate_hz = static_cast<uint32_t>(info->rate),
+ };
}
-auto TremorVorbisDecoder::ContinueStream(cpp::span<const std::byte> input,
- cpp::span<sample::Sample> output)
- -> Result<OutputInfo> {
+auto TremorVorbisDecoder::DecodeTo(cpp::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
cpp::span<int16_t> staging_buffer{
reinterpret_cast<int16_t*>(output.subspan(output.size() / 2).data()),
output.size_bytes() / 2};
- input_ = input;
- pos_in_input_ = 0;
-
int bitstream;
long bytes_written =
ov_read(&vorbis_, reinterpret_cast<char*>(staging_buffer.data()),
staging_buffer.size_bytes(), &bitstream);
if (bytes_written == OV_HOLE) {
ESP_LOGE(kTag, "got OV_HOLE");
- return {pos_in_input_, cpp::fail(Error::kMalformedData)};
+ return cpp::fail(Error::kMalformedData);
} else if (bytes_written == OV_EBADLINK) {
ESP_LOGE(kTag, "got OV_EBADLINK");
- return {pos_in_input_, cpp::fail(Error::kMalformedData)};
- } else if (bytes_written == 0) {
- return {pos_in_input_, cpp::fail(Error::kOutOfInput)};
+ return cpp::fail(Error::kMalformedData);
}
for (int i = 0; i < bytes_written / 2; i++) {
output[i] = sample::FromSigned(staging_buffer[i], 16);
}
- return {pos_in_input_,
- OutputInfo{
- .samples_written = static_cast<size_t>(bytes_written / 2),
- .is_finished_writing = bytes_written == 0,
- }};
+ return OutputInfo{
+ .samples_written = static_cast<size_t>(bytes_written / 2),
+ .is_stream_finished = bytes_written == 0,
+ };
}
-auto TremorVorbisDecoder::SeekStream(cpp::span<const std::byte> input,
- std::size_t target_sample)
- -> Result<void> {
+auto TremorVorbisDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
return {};
}
-auto TremorVorbisDecoder::ReadCallback() -> cpp::span<const std::byte> {
- return input_.subspan(pos_in_input_);
-}
-
-auto TremorVorbisDecoder::AfterReadCallback(size_t bytes_read) -> void {
- pos_in_input_ += bytes_read;
-}
-
} // namespace codecs