From 650264ea86f39d0e1232046babcd6e0004254d6b Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:07:45 -0800 Subject: [PATCH] media: add libvorbis encoder --- src/media/CMakeLists.txt | 3 + src/media/vorbis.cpp | 138 +++++++++++++++++++++++++++++++++++++ src/media/vorbis.hpp | 54 +++++++++++++++ src/media/vorbis_error.hpp | 54 +++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/media/vorbis.cpp create mode 100644 src/media/vorbis.hpp create mode 100644 src/media/vorbis_error.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index fc4c6202a..1ecf75d21 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -6,4 +6,7 @@ target_sources(SRB2SDL2 PRIVATE options.hpp video_encoder.hpp video_frame.hpp + vorbis.cpp + vorbis.hpp + vorbis_error.hpp ) diff --git a/src/media/vorbis.cpp b/src/media/vorbis.cpp new file mode 100644 index 000000000..6b33aa22e --- /dev/null +++ b/src/media/vorbis.cpp @@ -0,0 +1,138 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include + +#include +#include + +#include "../cxxutil.hpp" +#include "vorbis.hpp" +#include "vorbis_error.hpp" + +using namespace srb2::media; + +// clang-format off +const Options VorbisEncoder::options_("vorbis", { + {"quality", Options::range("0", -0.1f, 1.f)}, + {"max_bitrate", Options::range_min("-1", -1)}, + {"nominal_bitrate", Options::range_min("-1", -1)}, + {"min_bitrate", Options::range_min("-1", -1)}, +}); +// clang-format on + +VorbisEncoder::VorbisEncoder(Config cfg) +{ + const long max_bitrate = options_.get("max_bitrate"); + const long nominal_bitrate = options_.get("nominal_bitrate"); + const long min_bitrate = options_.get("min_bitrate"); + + vorbis_info_init(&vi_); + + if (max_bitrate != -1 || nominal_bitrate != -1 || min_bitrate != -1) + { + // managed bitrate mode + VorbisError error = + vorbis_encode_init(&vi_, cfg.channels, cfg.sample_rate, max_bitrate, nominal_bitrate, min_bitrate); + + if (error != 0) + { + throw std::invalid_argument(fmt::format( + "vorbis_encode_init: {}, max_bitrate={}, nominal_bitrate={}, min_bitrate={}", + error, + max_bitrate, + nominal_bitrate, + min_bitrate + )); + } + } + else + { + // variable bitrate mode + const float quality = options_.get("quality"); + + VorbisError error = vorbis_encode_init_vbr(&vi_, cfg.channels, cfg.sample_rate, quality); + + if (error != 0) + { + throw std::invalid_argument(fmt::format("vorbis_encode_init: {}, quality={}", error, quality)); + } + } + + SRB2_ASSERT(vorbis_analysis_init(&vd_, &vi_) == 0); + SRB2_ASSERT(vorbis_block_init(&vd_, &vb_) == 0); +} + +VorbisEncoder::~VorbisEncoder() +{ + vorbis_block_clear(&vb_); + vorbis_dsp_clear(&vd_); + vorbis_info_clear(&vi_); +} + +VorbisEncoder::headers_t VorbisEncoder::generate_headers() +{ + headers_t op; + + vorbis_comment vc; + vorbis_comment_init(&vc); + + VorbisError error = vorbis_analysis_headerout(&vd_, &vc, &op[0], &op[1], &op[2]); + + if (error != 0) + { + throw std::invalid_argument(fmt::format("vorbis_analysis_headerout: {}", error)); + } + + vorbis_comment_clear(&vc); + + return op; +} + +void VorbisEncoder::analyse(sample_buffer_t in) +{ + const int ch = channels(); + + const std::size_t n = in.size() / ch; + float** fv = vorbis_analysis_buffer(&vd_, n); + + for (std::size_t i = 0; i < n; ++i) + { + auto s = in.subspan(i * ch, ch); + + fv[0][i] = s[0]; + fv[1][i] = s[1]; + } + + // automatically handles end of stream if n = 0 + SRB2_ASSERT(vorbis_analysis_wrote(&vd_, n) == 0); + + while (vorbis_analysis_blockout(&vd_, &vb_) > 0) + { + SRB2_ASSERT(vorbis_analysis(&vb_, nullptr) == 0); + SRB2_ASSERT(vorbis_bitrate_addblock(&vb_) == 0); + + ogg_packet op; + + while (vorbis_bitrate_flushpacket(&vd_, &op) > 0) + { + write_packet(&op); + } + } +} + +void VorbisEncoder::write_packet(ogg_packet* op) +{ + using T = const std::byte; + tcb::span p(reinterpret_cast(op->packet), static_cast(op->bytes)); + + write_frame(p, std::chrono::duration(vorbis_granule_time(&vd_, op->granulepos)), true); +} diff --git a/src/media/vorbis.hpp b/src/media/vorbis.hpp new file mode 100644 index 000000000..333f31f8e --- /dev/null +++ b/src/media/vorbis.hpp @@ -0,0 +1,54 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VORBIS_HPP__ +#define __SRB2_MEDIA_VORBIS_HPP__ + +#include + +#include + +#include "audio_encoder.hpp" +#include "options.hpp" + +namespace srb2::media +{ + +class VorbisEncoder : public AudioEncoder +{ +public: + static const Options options_; + + VorbisEncoder(Config config); + ~VorbisEncoder(); + + virtual void encode(sample_buffer_t samples) override final { analyse(samples); } + virtual void flush() override final { analyse(); } + + virtual const char* name() const override final { return "Vorbis"; } + virtual int channels() const override final { return vi_.channels; } + virtual int sample_rate() const override final { return vi_.rate; } + +protected: + using headers_t = std::array; + + headers_t generate_headers(); + +private: + vorbis_info vi_; + vorbis_dsp_state vd_; + vorbis_block vb_; + + void analyse(sample_buffer_t samples = {}); + void write_packet(ogg_packet* op); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VORBIS_HPP__ diff --git a/src/media/vorbis_error.hpp b/src/media/vorbis_error.hpp new file mode 100644 index 000000000..7c4f9d3d6 --- /dev/null +++ b/src/media/vorbis_error.hpp @@ -0,0 +1,54 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VORBIS_ERROR_HPP__ +#define __SRB2_MEDIA_VORBIS_ERROR_HPP__ + +#include + +#include +#include + +class VorbisError +{ +public: + VorbisError(int error) : error_(error) {} + + operator int() const { return error_; } + + std::string name() const + { + switch (error_) + { + case OV_EFAULT: + return "Internal error (OV_EFAULT)"; + case OV_EINVAL: + return "Invalid settings (OV_EINVAL)"; + case OV_EIMPL: + return "Invalid settings (OV_EIMPL)"; + default: + return fmt::format("error {}", error_); + } + } + +private: + int error_; +}; + +template <> +struct fmt::formatter : formatter +{ + template + auto format(const VorbisError& error, FormatContext& ctx) const + { + return formatter::format(error.name(), ctx); + } +}; + +#endif // __SRB2_MEDIA_VORBIS_ERROR_HPP__