diff --git a/3rdparty/SConscript b/3rdparty/SConscript index dd5338989..bf39a8ccc 100644 --- a/3rdparty/SConscript +++ b/3rdparty/SConscript @@ -18,8 +18,10 @@ thirdparty_versions = { 'alsa': '1.1.9', 'json-c': '0.12-20140410', 'libatomic_ops': '7.6.10', + 'libogg': '1.3.4', 'libunwind': '1.2.1', 'libuv': '1.35.0', + 'libvorbis': '1.3.7', 'ltdl': '2.4.6', 'openfec': '1.4.2.11', 'openssl': '3.0.8', @@ -48,6 +50,9 @@ external_dependencies.add('ragel') if 'pulseaudio' in external_dependencies: external_dependencies.add('sndfile') +if 'libvorbis' in external_dependencies: + external_dependencies.add('libogg') + # on Linux, PulseAudio needs ALSA if meta.platform in ['linux'] and 'pulseaudio' in external_dependencies: external_dependencies.add('alsa') @@ -254,6 +259,31 @@ elif 'sndfile' in system_dependencies: env = conf.Finish() +# dep: libogg +if 'libogg' in autobuild_dependencies: + env.BuildThirdParty(thirdparty_versions, 'libogg') + +# dep: libvorbis +if 'libvorbis' in autobuild_dependencies: + pa_deps = [ + 'libogg', + ] + + env.BuildThirdParty(thirdparty_versions, 'libogg') + env.BuildThirdParty(thirdparty_versions, 'libvorbis', + deps=pa_deps) + +elif 'libvorbis' in system_dependencies: + conf = Configure(env, custom_tests=env.CustomTests) + + if not conf.AddPkgConfigDependency('vorbis', '--cflags --libs', exclude_from_pc=True): + conf.env.AddManualDependency(libs=['vorbis', 'vorbisenc', 'vorbisfile'], exclude_from_pc=True) + + if not conf.CheckLibWithHeaderExt('vorbis', 'vorbis/vorbisenc.h', 'C', run=False): + env.Die("libvorbis not found (see 'config.log' for details)") + + env = conf.Finish() + # dep: alsa if 'alsa' in autobuild_dependencies: env.BuildThirdParty(thirdparty_versions, 'alsa') diff --git a/SConstruct b/SConstruct index 3bb2d1369..fe27be50d 100644 --- a/SConstruct +++ b/SConstruct @@ -209,6 +209,11 @@ AddOption('--disable-sox', action='store_true', help='disable SoX support in tools') +AddOption('--disable-libvorbis', + dest='disable_libvorbis', + action='store_true', + help='disable libvorbis support in tools') + AddOption('--disable-sndfile', dest='disable_sndfile', action='store_true', @@ -855,6 +860,10 @@ else: env.Append(ROC_TARGETS=[ 'target_pulseaudio', ]) + if not GetOption('disable_libvorbis'): + env.Append(ROC_TARGETS=[ + 'target_libvorbis', + ]) if 'target_gnu' not in env['ROC_TARGETS']: env.Append(ROC_TARGETS=[ diff --git a/scripts/ci_checks/linux-x86_64/opensuse.sh b/scripts/ci_checks/linux-x86_64/opensuse.sh index aa17878e0..2222819fc 100755 --- a/scripts/ci_checks/linux-x86_64/opensuse.sh +++ b/scripts/ci_checks/linux-x86_64/opensuse.sh @@ -7,5 +7,5 @@ scons -Q \ --enable-tests \ --enable-benchmarks \ --enable-examples \ - --build-3rdparty=openfec,cpputest \ + --build-3rdparty=openfec,cpputest,libvorbis \ test diff --git a/scripts/scons_helpers/build-3rdparty.py b/scripts/scons_helpers/build-3rdparty.py index bec792135..476c14711 100644 --- a/scripts/scons_helpers/build-3rdparty.py +++ b/scripts/scons_helpers/build-3rdparty.py @@ -1438,6 +1438,49 @@ def die(text, *args): install_files(ctx, 'builddir/src/pulse/libpulse-simple.so.0', ctx.pkg_rpath_dir) install_files(ctx, 'builddir/src/libpulsecommon-*.so', ctx.pkg_lib_dir) install_files(ctx, 'builddir/src/libpulsecommon-*.so', ctx.pkg_rpath_dir) +elif ctx.pkg_name == 'libogg': + download( + ctx, + 'https://downloads.xiph.org/releases/ogg/libogg-{ctx.pkg_ver}.tar.gz', + 'libogg-{ctx.pkg_ver}.tar.gz') + unpack(ctx, + 'libogg-{ctx.pkg_ver}.tar.gz', + 'libogg-{ctx.pkg_ver}') + changedir(ctx, 'src/libogg-{ctx.pkg_ver}') + execute(ctx, './configure --host={host} {vars} {flags} {opts}'.format( + host=ctx.toolchain, + vars=format_vars(ctx), + flags=format_flags(ctx, cflags='-fPIC'), + opts=' '.join([ + '--disable-shared', + '--enable-static', + ]))) + execute_make(ctx) + install_tree(ctx, 'include', ctx.pkg_inc_dir, include=['*.h']) + install_files(ctx, 'src/.libs/libogg.a', ctx.pkg_lib_dir) + install_files(ctx, 'ogg.pc', ctx.pkg_lib_dir) +elif ctx.pkg_name == 'libvorbis': + download( + ctx, + 'https://downloads.xiph.org/releases/vorbis/libvorbis-{ctx.pkg_ver}.tar.gz', + 'libvorbis-{ctx.pkg_ver}.tar.gz') + unpack(ctx, + 'libvorbis-{ctx.pkg_ver}.tar.gz', + 'libvorbis-{ctx.pkg_ver}') + changedir(ctx, 'src/libvorbis-{ctx.pkg_ver}') + execute(ctx, './configure --host={host} {vars} {flags} {opts}'.format( + host=ctx.toolchain, + vars=format_vars(ctx), + flags=format_flags(ctx, cflags='-fPIC'), + opts=' '.join([ + '--disable-shared', + '--enable-static', + ]))) + execute_make(ctx) + install_tree(ctx, 'include', ctx.pkg_inc_dir, include=['*.h']) + install_files(ctx, 'lib/.libs/libvorbis.a', ctx.pkg_lib_dir) + install_files(ctx, 'lib/.libs/libvorbisenc.a', ctx.pkg_lib_dir) + install_files(ctx, 'lib/.libs/libvorbisfile.a', ctx.pkg_lib_dir) elif ctx.pkg_name == 'ltdl': download( ctx, diff --git a/src/internal_modules/roc_audio/iframe_decoder.h b/src/internal_modules/roc_audio/iframe_decoder.h index cd7b0f6de..6c795c0b6 100644 --- a/src/internal_modules/roc_audio/iframe_decoder.h +++ b/src/internal_modules/roc_audio/iframe_decoder.h @@ -33,6 +33,27 @@ class IFrameDecoder : public core::ArenaAllocation { //! Check if the object was successfully constructed. virtual status::StatusCode init_status() const = 0; + //! Initialize the decoder with codec-specific headers. + //! + //! @b Parameters + //! - @p headers - pointer to the buffer containing codec-specific header data. + //! - @p headers_size - size of the header buffer in bytes. + //! + //! @remarks + //! This function sets up the decoder using the provided headers, which contain + //! essential configuration information for decoding the audio stream. The headers + //! must be in the format required by the specific codec. This function must be + //! called before decoding any frames. + //! + //! @returns + //! - true if the headers are successfully processed and the decoder is initialized. + //! - false if there is an error in processing the headers. + //! + //! @pre + //! Must be called before begin_frame(), read_samples(), drop_samples(), or + //! end_frame(). + virtual bool initialize_headers(const uint8_t* headers, size_t headers_size) = 0; + //! Get decoded stream position. //! //! @returns diff --git a/src/internal_modules/roc_audio/iframe_encoder.h b/src/internal_modules/roc_audio/iframe_encoder.h index bc9e6eeda..f2053d5ad 100644 --- a/src/internal_modules/roc_audio/iframe_encoder.h +++ b/src/internal_modules/roc_audio/iframe_encoder.h @@ -36,6 +36,18 @@ class IFrameEncoder : public core::ArenaAllocation { //! Get encoded frame size in bytes for given number of samples (per channel). virtual size_t encoded_byte_count(size_t n_samples) const = 0; + //! Get ponter of headers. + //! + //! @returns + //! A pointer to the headers. May return NULL if not applicable. + virtual const uint8_t* get_headers_frame() const = 0; + + //! Get the size of the headers. + //! + //! @returns + //! Size of the headers in bytes. + virtual size_t get_headers_frame_size() const = 0; + //! Start encoding a new frame. //! //! @remarks diff --git a/src/internal_modules/roc_audio/pcm_decoder.cpp b/src/internal_modules/roc_audio/pcm_decoder.cpp index 96f1c732d..4f5b90c90 100644 --- a/src/internal_modules/roc_audio/pcm_decoder.cpp +++ b/src/internal_modules/roc_audio/pcm_decoder.cpp @@ -32,6 +32,10 @@ status::StatusCode PcmDecoder::init_status() const { return status::StatusOK; } +bool PcmDecoder::initialize_headers(const uint8_t* headers, size_t headers_size) { + return true; +} + packet::stream_timestamp_t PcmDecoder::position() const { return stream_pos_; } diff --git a/src/internal_modules/roc_audio/pcm_decoder.h b/src/internal_modules/roc_audio/pcm_decoder.h index 5da76007e..7d9ab04d2 100644 --- a/src/internal_modules/roc_audio/pcm_decoder.h +++ b/src/internal_modules/roc_audio/pcm_decoder.h @@ -32,6 +32,9 @@ class PcmDecoder : public IFrameDecoder, public core::NonCopyable<> { //! Check if the object was successfully constructed. virtual status::StatusCode init_status() const; + //! Get the header information before start decoding + virtual bool initialize_headers(const uint8_t* headers, size_t headers_size); + //! Get current stream position. virtual packet::stream_timestamp_t position() const; diff --git a/src/internal_modules/roc_audio/pcm_encoder.cpp b/src/internal_modules/roc_audio/pcm_encoder.cpp index 49ef31433..bbcb5b229 100644 --- a/src/internal_modules/roc_audio/pcm_encoder.cpp +++ b/src/internal_modules/roc_audio/pcm_encoder.cpp @@ -33,6 +33,14 @@ size_t PcmEncoder::encoded_byte_count(size_t num_samples) const { return pcm_mapper_.output_byte_count(num_samples * n_chans_); } +const uint8_t* PcmEncoder::get_headers_frame() const { + return NULL; +} + +size_t PcmEncoder::get_headers_frame_size() const { + return 0; +} + void PcmEncoder::begin_frame(void* frame_data, size_t frame_size) { roc_panic_if_not(frame_data); diff --git a/src/internal_modules/roc_audio/pcm_encoder.h b/src/internal_modules/roc_audio/pcm_encoder.h index 775b7a1c9..552d38f5b 100644 --- a/src/internal_modules/roc_audio/pcm_encoder.h +++ b/src/internal_modules/roc_audio/pcm_encoder.h @@ -35,6 +35,12 @@ class PcmEncoder : public IFrameEncoder, public core::NonCopyable<> { //! Get encoded frame size in bytes for given number of samples per channel. virtual size_t encoded_byte_count(size_t num_samples) const; + //! Get headers frame. + const uint8_t* get_headers_frame() const; + + //! Get the size of the headers. + size_t get_headers_frame_size() const; + //! Start encoding a new frame. virtual void begin_frame(void* frame, size_t frame_size); diff --git a/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.cpp b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.cpp new file mode 100644 index 000000000..84c1f6457 --- /dev/null +++ b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2024 Roc Streaming authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "roc_audio/vorbis_decoder.h" +#include "roc_core/panic.h" + +namespace roc { +namespace audio { + +VorbisDecoder::VorbisDecoder(const SampleSpec& sample_spec, core::IArena& arena) + : IFrameDecoder(arena) + , initialized_(false) + , current_position_(0) + , available_samples_(0) + , pcm_samples_(NULL) + , current_sample_pos_(0) + , total_samples_in_frame_(0) + , headers_read_(false) { + vorbis_info_init(&vorbis_info_); + vorbis_comment_init(&vorbis_comment_); + ogg_sync_init(&ogg_sync_); + ogg_stream_init(&ogg_stream_, 0); + initialized_ = true; +} + +VorbisDecoder::~VorbisDecoder() { + if (initialized_) { + vorbis_block_clear(&vorbis_block_); + vorbis_dsp_clear(&vorbis_dsp_); + vorbis_info_clear(&vorbis_info_); + vorbis_comment_clear(&vorbis_comment_); + ogg_sync_clear(&ogg_sync_); + ogg_stream_clear(&ogg_stream_); + } +} + +status::StatusCode VorbisDecoder::init_status() const { + return initialized_ ? status::StatusOK : status::StatusAbort; +} + +packet::stream_timestamp_t VorbisDecoder::position() const { + return current_position_; +} + +packet::stream_timestamp_t VorbisDecoder::available() const { + return available_samples_; +} + +size_t VorbisDecoder::decoded_sample_count(const void* frame_data, + size_t frame_size) const { + const size_t nominal_bitrate = static_cast(vorbis_info_.bitrate_nominal); + const size_t num_channels = static_cast(vorbis_info_.channels); + + return frame_size * 8 / (nominal_bitrate / num_channels); +} + +bool VorbisDecoder::initialize_headers(const uint8_t* headers, size_t headers_size) { + roc_panic_if_not(headers); + + // Reset ogg_sync state to ensure clean reading + ogg_sync_reset(&ogg_sync_); + + // Add the combined headers to the ogg_sync state + add_data_to_ogg_sync_(headers, headers_size); + + // Process the headers to initialize decoder state + if (!read_headers_()) { + return false; + } + + headers_read_ = true; + return true; +} + +void VorbisDecoder::begin_frame(packet::stream_timestamp_t frame_position, + const void* frame_data, + size_t frame_size) { + roc_panic_if_not(initialized_); + + reset_frame_state_(frame_position); + + add_data_to_ogg_sync_(frame_data, frame_size); + + process_frame_packets_(); +} + +size_t VorbisDecoder::read_samples(sample_t* samples, size_t n_samples) { + roc_panic("TODO"); + return 0; +} + +size_t VorbisDecoder::drop_samples(size_t n_samples) { + roc_panic("TODO"); + return 0; +} + +void VorbisDecoder::end_frame() { + roc_panic("TODO"); +} + +void VorbisDecoder::reset_frame_state_(packet::stream_timestamp_t frame_position) { + current_position_ = frame_position; + available_samples_ = 0; + current_sample_pos_ = 0; + total_samples_in_frame_ = 0; +} + +void VorbisDecoder::add_data_to_ogg_sync_(const void* frame_data, size_t frame_size) { + char* buffer = ogg_sync_buffer(&ogg_sync_, static_cast(frame_size)); + memcpy(buffer, frame_data, frame_size); + ogg_sync_wrote(&ogg_sync_, static_cast(frame_size)); +} + +bool VorbisDecoder::read_headers_() { + ogg_page ogg_page; + int header_count = 0; + + // Loop to extract pages from the sync state + while (ogg_sync_pageout(&ogg_sync_, &ogg_page) == 1) { + if (ogg_stream_pagein(&ogg_stream_, &ogg_page) < 0) { + return false; + } + + ogg_packet header_packet; + + // Loop to extract packets from the stream state + while (ogg_stream_packetout(&ogg_stream_, &header_packet) == 1) { + // Pass the header to vorbis_synthesis_headerin regardless of type + if (vorbis_synthesis_headerin(&vorbis_info_, &vorbis_comment_, &header_packet) + < 0) { + return false; + } + + header_count++; + + // After processing three headers, initialize DSP and block + if (header_count == 3) { + if (vorbis_synthesis_init(&vorbis_dsp_, &vorbis_info_) == 0) { + if (vorbis_block_init(&vorbis_dsp_, &vorbis_block_) == 0) { + headers_read_ = true; + return true; + } + } + } + } + } + + return false; +} + +void VorbisDecoder::process_frame_packets_() { + ogg_page ogg_page; + while (ogg_sync_pageout(&ogg_sync_, &ogg_page) == 1) { + ogg_stream_pagein(&ogg_stream_, &ogg_page); + while (ogg_stream_packetout(&ogg_stream_, ¤t_packet_) == 1) { + process_packet_(); + } + } +} + +void VorbisDecoder::process_packet_() { + if (vorbis_synthesis(&vorbis_block_, ¤t_packet_) != 0) { + return; + } + + vorbis_synthesis_blockin(&vorbis_dsp_, &vorbis_block_); + + while (true) { + total_samples_in_frame_ = vorbis_synthesis_pcmout(&vorbis_dsp_, &pcm_samples_); + if (total_samples_in_frame_ <= 0) { + break; + } + + available_samples_ += static_cast(total_samples_in_frame_); + vorbis_synthesis_read(&vorbis_dsp_, total_samples_in_frame_); + } +} + +} // namespace audio +} // namespace roc diff --git a/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.h b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.h new file mode 100644 index 000000000..c3bea80f0 --- /dev/null +++ b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_decoder.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Roc Streaming authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +//! @file roc_audio/target_libvorbis/roc_audio/vorbis_decoder.h +//! @brief Vorbis audio decoder. + +#ifndef ROC_AUDIO_VORBIS_DECODER_H_ +#define ROC_AUDIO_VORBIS_DECODER_H_ + +#include "roc_audio/iframe_decoder.h" +#include "roc_audio/sample_spec.h" +#include + +namespace roc { +namespace audio { + +//! Vorbis Decoder. +class VorbisDecoder : public IFrameDecoder { +public: + //! Initialize. + VorbisDecoder(const SampleSpec& sample_spec, core::IArena& arena); + + //! End. + ~VorbisDecoder(); + + //! Check if the object was successfully constructed. + virtual status::StatusCode init_status() const; + + //! Get decoded stream position. + virtual packet::stream_timestamp_t position() const; + + //! Get number of samples available for decoding. + virtual packet::stream_timestamp_t available() const; + + //! Get number of samples per channel that can be decoded from given frame. + virtual size_t decoded_sample_count(const void* frame_data, size_t frame_size) const; + + //! Initialize decoder with combined Vorbis headers. + bool initialize_headers(const uint8_t* headers, size_t headers_size); + + //! Start decoding a new frame. + virtual void begin_frame(packet::stream_timestamp_t frame_position, + const void* frame_data, + size_t frame_size); + + //! Read samples from current frame. + virtual size_t read_samples(sample_t* samples, size_t n_samples); + + //! Shift samples from current frame. + virtual size_t drop_samples(size_t n_samples); + + //! Finish decoding current frame. + virtual void end_frame(); + +private: + void reset_frame_state_(packet::stream_timestamp_t frame_position); + void add_data_to_ogg_sync_(const void* frame_data, size_t frame_size); + bool read_headers_(); + void process_frame_packets_(); + void process_packet_(); + + bool initialized_; + vorbis_info vorbis_info_; + vorbis_comment vorbis_comment_; + vorbis_dsp_state vorbis_dsp_; + vorbis_block vorbis_block_; + ogg_packet current_packet_; + ogg_sync_state ogg_sync_; + ogg_stream_state ogg_stream_; + + packet::stream_timestamp_t current_position_; + size_t available_samples_; + float** pcm_samples_; + int current_sample_pos_; + int total_samples_in_frame_; + bool headers_read_; +}; + +} // namespace audio +} // namespace roc + +#endif // ROC_AUDIO_VORBIS_DECODER_H_ diff --git a/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.cpp b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.cpp new file mode 100644 index 000000000..1ab08729a --- /dev/null +++ b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.cpp @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2024 Roc Streaming authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "roc_audio/vorbis_encoder.h" +#include "roc_core/panic.h" + +namespace roc { +namespace audio { + +VorbisEncoder::VorbisEncoder(const SampleSpec& sample_spec, core::IArena& arena) + : IFrameEncoder(arena) + , initialized_(false) + , frame_data_(NULL) + , frame_size_(0) + , current_position_(0) + , headers_frame_(NULL) + , headers_frame_size_(0) { + const long num_channels = static_cast(sample_spec.num_channels()); + const long sample_rate = static_cast(sample_spec.sample_rate()); + + initialize_structures_(num_channels, sample_rate); + + create_headers_frame_(); + + initialized_ = true; +} + +VorbisEncoder::~VorbisEncoder() { + if (initialized_) { + vorbis_block_clear(&vorbis_block_); + vorbis_dsp_clear(&vorbis_dsp_); + vorbis_info_clear(&vorbis_info_); + vorbis_comment_clear(&vorbis_comment_); + ogg_stream_clear(&ogg_stream_); + if (headers_frame_) { + free(headers_frame_); + } + } +} + +status::StatusCode VorbisEncoder::init_status() const { + return initialized_ ? status::StatusOK : status::StatusAbort; +} + +size_t VorbisEncoder::encoded_byte_count(size_t n_samples) const { + roc_panic_if_not(initialized_); + + const size_t nominal_bitrate = static_cast(vorbis_info_.bitrate_nominal); + const size_t num_channels = static_cast(vorbis_info_.channels); + const size_t sample_rate = static_cast(vorbis_info_.rate); + + const size_t total_num_bits = nominal_bitrate * n_samples * num_channels; + + // Estimated encoded byte count + return total_num_bits / (sample_rate * 8); +} + +const uint8_t* VorbisEncoder::get_headers_frame() const { + return headers_frame_; +} + +size_t VorbisEncoder::get_headers_frame_size() const { + return headers_frame_size_; +} + +void VorbisEncoder::begin_frame(void* frame_data, size_t frame_size) { + roc_panic_if_not(frame_data); + + if (frame_data_) { + roc_panic("vorbis encoder: unpaired begin/end"); + } + + frame_data_ = static_cast(frame_data); + frame_size_ = frame_size; + current_position_ = 0; +} + +size_t VorbisEncoder::write_samples(const sample_t* samples, size_t n_samples) { + roc_panic_if_not(initialized_); + + if (!samples || n_samples == 0) { + return 0; + } + + buffer_samples_(samples, n_samples); + + process_encoding_(); + + return n_samples; +} + +void VorbisEncoder::end_frame() { + roc_panic_if_not(initialized_); + + // Indicate that no more samples are to be written + vorbis_analysis_wrote(&vorbis_dsp_, 0); + + // Encode the remaining data + process_encoding_(); + + frame_data_ = NULL; + frame_size_ = 0; +} + +void VorbisEncoder::initialize_structures_(long num_channels, long sample_rate) { + vorbis_info_init(&vorbis_info_); + vorbis_comment_init(&vorbis_comment_); + + const float quality = 0.5f; + + // Initialize vorbis_info structure + if (vorbis_encode_init_vbr(&vorbis_info_, num_channels, sample_rate, quality) != 0) { + roc_panic("vorbis encoder: failed to initialize vorbis encoder"); + } + + // Initialize vorbis_dsp_state for encoding + if (vorbis_analysis_init(&vorbis_dsp_, &vorbis_info_) != 0) { + roc_panic("vorbis encoder: failed to initialize vorbis dsp"); + } + + // Initialize ogg_stream_state for the stream + if (ogg_stream_init(&ogg_stream_, 0) != 0) { + roc_panic("vorbis encoder: failed to initialize ogg stream"); + } + + // Initialize vorbis_block for encoding + if (vorbis_block_init(&vorbis_dsp_, &vorbis_block_) != 0) { + roc_panic("vorbis encoder: failed to initialize vorbis block"); + } +} + +void VorbisEncoder::create_headers_frame_() { + ogg_packet header_packet; + ogg_packet header_comment; + ogg_packet header_codebook; + + if (vorbis_analysis_headerout(&vorbis_dsp_, &vorbis_comment_, &header_packet, + &header_comment, &header_codebook) + != 0) { + roc_panic("vorbis encoder: failed to create vorbis headers"); + } + + headers_frame_size_ = + calculate_total_headers_size_(header_packet, header_comment, header_codebook); + + headers_frame_ = static_cast(malloc(headers_frame_size_)); + if (!headers_frame_) { + roc_panic("vorbis encoder: failed to allocate memory for headers"); + } + + copy_headers_to_memory_(header_packet, header_comment, header_codebook); +} + +size_t VorbisEncoder::calculate_total_headers_size_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook) { + ogg_page ogg_page; + long total_size = 0; + + insert_headers_into_stream_(header_packet, header_comment, header_codebook); + + while (ogg_stream_flush(&ogg_stream_, &ogg_page)) { + total_size += ogg_page.header_len + ogg_page.body_len; + } + + return static_cast(total_size); +} + +void VorbisEncoder::copy_headers_to_memory_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook) { + ogg_page ogg_page; + size_t offset = 0; + + insert_headers_into_stream_(header_packet, header_comment, header_codebook); + + while (ogg_stream_flush(&ogg_stream_, &ogg_page)) { + const size_t header_len = static_cast(ogg_page.header_len); + const size_t body_len = static_cast(ogg_page.body_len); + + memcpy(headers_frame_ + offset, ogg_page.header, header_len); + offset += header_len; + memcpy(headers_frame_ + offset, ogg_page.body, body_len); + offset += body_len; + } +} + +void VorbisEncoder::insert_headers_into_stream_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook) { + ogg_stream_reset(&ogg_stream_); + ogg_stream_packetin(&ogg_stream_, &header_packet); + ogg_stream_packetin(&ogg_stream_, &header_comment); + ogg_stream_packetin(&ogg_stream_, &header_codebook); +} + +void VorbisEncoder::buffer_samples_(const sample_t* samples, size_t n_samples) { + const int int_n_samples = static_cast(n_samples); + + float** buffer = vorbis_analysis_buffer(&vorbis_dsp_, int_n_samples); + + for (int i = 0; i < int_n_samples; ++i) { + for (int ch = 0; ch < vorbis_info_.channels; ++ch) { + buffer[ch][i] = samples[i * vorbis_info_.channels + ch]; + } + } + + vorbis_analysis_wrote(&vorbis_dsp_, int_n_samples); +} + +void VorbisEncoder::process_encoding_() { + ogg_packet packet; + while (vorbis_analysis_blockout(&vorbis_dsp_, &vorbis_block_) == 1) { + vorbis_analysis(&vorbis_block_, 0); + vorbis_bitrate_addblock(&vorbis_block_); + + while (vorbis_bitrate_flushpacket(&vorbis_dsp_, &packet)) { + const size_t packet_bytes = static_cast(packet.bytes); + + if (current_position_ + packet_bytes > frame_size_) { + return; + } + + memcpy(frame_data_ + current_position_, packet.packet, packet_bytes); + current_position_ += packet_bytes; + } + } +} + +} // namespace audio +} // namespace roc diff --git a/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.h b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.h new file mode 100644 index 000000000..c16180a59 --- /dev/null +++ b/src/internal_modules/roc_audio/target_libvorbis/roc_audio/vorbis_encoder.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Roc Streaming authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +//! @file roc_audio/target_libvorbis/roc_audio/vorbis_encoder.h +//! @brief Vorbis audio encoder. + +#ifndef ROC_AUDIO_VORBIS_ENCODER_H_ +#define ROC_AUDIO_VORBIS_ENCODER_H_ + +#include "roc_audio/iframe_encoder.h" +#include "roc_audio/sample_spec.h" +#include +#include + +namespace roc { +namespace audio { + +//! Vorbis Encoder. +class VorbisEncoder : public IFrameEncoder { +public: + //! Initialize. + VorbisEncoder(const SampleSpec& sample_spec, core::IArena& arena); + + //! End. + ~VorbisEncoder(); + + //! Check if the object was successfully constructed. + virtual status::StatusCode init_status() const; + + //! Get encoded frame size in bytes for given number of samples per channel. + virtual size_t encoded_byte_count(size_t n_samples) const; + + //! Get combined Vorbis headers. + const uint8_t* get_headers_frame() const; + + //! Get the size of the combined headers. + size_t get_headers_frame_size() const; + + //! Start encoding a new frame. + virtual void begin_frame(void* frame, size_t frame_size); + + //! Encode samples. + virtual size_t write_samples(const sample_t* samples, size_t n_samples); + + //! Finish encoding frame. + virtual void end_frame(); + +private: + void initialize_structures_(long num_channels, long sample_rate); + void create_headers_frame_(); + size_t calculate_total_headers_size_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook); + void copy_headers_to_memory_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook); + void insert_headers_into_stream_(ogg_packet& header_packet, + ogg_packet& header_comment, + ogg_packet& header_codebook); + void buffer_samples_(const sample_t* samples, size_t n_samples); + void process_encoding_(); + + bool initialized_; + uint8_t* frame_data_; + size_t frame_size_; + size_t current_position_; + vorbis_info vorbis_info_; + vorbis_comment vorbis_comment_; + vorbis_dsp_state vorbis_dsp_; + vorbis_block vorbis_block_; + ogg_stream_state ogg_stream_; + + uint8_t* headers_frame_; + size_t headers_frame_size_; +}; + +} // namespace audio +} // namespace roc + +#endif // ROC_AUDIO_VORBIS_ENCODER_H_