diff --git a/3rdparty/SConscript b/3rdparty/SConscript index 636374055..25c03f5ae 100644 --- a/3rdparty/SConscript +++ b/3rdparty/SConscript @@ -24,7 +24,7 @@ thirdparty_versions = { 'openfec': '1.4.2.7', 'openssl': '3.0.8', 'pulseaudio': '12.2', - 'sndfile': '1.0.28', + 'sndfile': '1.0.26', 'sox': '14.4.2', 'speexdsp': '1.2.0', @@ -44,6 +44,10 @@ external_dependencies = set(thirdparty_versions.keys()) \ # ragel is always needed external_dependencies.add('ragel') +# ensures we don't build sndfile twice +if 'pulseaudio' in external_dependencies: + external_dependencies.add('sndfile') + # on Linux, PulseAudio needs ALSA if meta.platform in ['linux'] and 'pulseaudio' in external_dependencies: external_dependencies.add('alsa') @@ -230,6 +234,31 @@ elif 'speexdsp' in system_dependencies: env = conf.Finish() +# dep: sndfile +if 'sndfile' in autobuild_dependencies: + + env.BuildThirdParty(thirdparty_versions, 'sndfile') + + conf = Configure(env, custom_tests=env.CustomTests) + + env = conf.Finish() + +elif 'sndfile' in system_dependencies: + conf = Configure(env, custom_tests=env.CustomTests) + + if not conf.AddPkgConfigDependency('sndfile', '--cflags --libs', exclude_from_pc=True): + conf.env.AddManualDependency(libs=['sndfile'], exclude_from_pc=True) + + if not is_crosscompiling: + if not conf.CheckLibWithHeaderExt( + 'sndfile', 'sndfile.h', 'C'): + env.Die("libsndfile >= 1.0.26 not found (see 'config.log' for details)") + else: + if not conf.CheckLibWithHeaderExt('sndfile', 'sndfile.h', 'C', run=False): + env.Die("libsndfile not found (see 'config.log' for details)") + + env = conf.Finish() + # dep: alsa if 'alsa' in autobuild_dependencies: env.BuildThirdParty(thirdparty_versions, 'alsa') @@ -252,7 +281,6 @@ if 'pulseaudio' in autobuild_dependencies: env.BuildThirdParty(thirdparty_versions, 'ltdl') env.BuildThirdParty(thirdparty_versions, 'json-c') - env.BuildThirdParty(thirdparty_versions, 'sndfile') env.BuildThirdParty(thirdparty_versions, 'pulseaudio', deps=pa_deps, libs=['pulse', 'pulse-simple']) diff --git a/SConstruct b/SConstruct index 0b7c3e2ed..18f7dc993 100644 --- a/SConstruct +++ b/SConstruct @@ -209,6 +209,11 @@ AddOption('--disable-sox', action='store_true', help='disable SoX support in tools') +AddOption('--disable-sndfile', + dest='disable_sndfile', + action='store_true', + help='disable sndfile support in tools') + AddOption('--disable-openssl', dest='disable_openssl', action='store_true', @@ -825,6 +830,10 @@ else: env.Append(ROC_TARGETS=[ 'target_sox', ]) + if not GetOption('disable_sndfile'): + env.Append(ROC_TARGETS=[ + 'target_sndfile', + ]) if not GetOption('disable_alsa') and meta.platform in ['linux']: env.Append(ROC_TARGETS=[ 'target_alsa', diff --git a/debian/copyright b/debian/copyright index 10de120d7..df48106cb 100644 --- a/debian/copyright +++ b/debian/copyright @@ -68,4 +68,10 @@ Copyright: ForeverASilver JJ Hunter Rick + Pekureda + Anurag Soni + Samad Khan + Alyssa Ross + Arseniy136 + Amandeep Singh License: MPL-2.0 diff --git a/scripts/ci_checks/linux-arm/aarch64-linux-gnu-gcc-7.4.sh b/scripts/ci_checks/linux-arm/aarch64-linux-gnu-gcc-7.4.sh index 40cae407c..9fdd4ca40 100755 --- a/scripts/ci_checks/linux-arm/aarch64-linux-gnu-gcc-7.4.sh +++ b/scripts/ci_checks/linux-arm/aarch64-linux-gnu-gcc-7.4.sh @@ -6,7 +6,7 @@ toolchain="aarch64-linux-gnu" compiler="gcc-7.4.1-release" cpu="cortex-a53" # armv8 -third_party="libuv,libunwind,openfec,alsa,speexdsp,sox,openssl,cpputest" +third_party="libuv,libunwind,openfec,alsa,speexdsp,sox,openssl,cpputest,sndfile" for pulse_ver in 8.0 15.99.1 do diff --git a/scripts/ci_checks/linux-arm/arm-bcm2708hardfp-linux-gnueabi-gcc-4.7.sh b/scripts/ci_checks/linux-arm/arm-bcm2708hardfp-linux-gnueabi-gcc-4.7.sh index 8f0c36d3a..128bfc4b7 100755 --- a/scripts/ci_checks/linux-arm/arm-bcm2708hardfp-linux-gnueabi-gcc-4.7.sh +++ b/scripts/ci_checks/linux-arm/arm-bcm2708hardfp-linux-gnueabi-gcc-4.7.sh @@ -6,7 +6,7 @@ toolchain="arm-bcm2708hardfp-linux-gnueabi" compiler="gcc-4.7.1-release" cpu="arm1176" # armv6 -third_party="libuv,libunwind,libatomic_ops,openfec,alsa,pulseaudio:5.0,speexdsp,sox,openssl,cpputest" +third_party="libuv,libunwind,libatomic_ops,openfec,alsa,pulseaudio:5.0,speexdsp,sox,openssl,cpputest,sndfile" scons -Q \ --enable-werror \ diff --git a/scripts/ci_checks/linux-arm/arm-linux-gnueabihf-gcc-4.9.sh b/scripts/ci_checks/linux-arm/arm-linux-gnueabihf-gcc-4.9.sh index 0b474ab02..e877ac7a4 100755 --- a/scripts/ci_checks/linux-arm/arm-linux-gnueabihf-gcc-4.9.sh +++ b/scripts/ci_checks/linux-arm/arm-linux-gnueabihf-gcc-4.9.sh @@ -6,7 +6,7 @@ toolchain="arm-linux-gnueabihf" compiler="gcc-4.9.4-release" cpu="cortex-a15" # armv7 -third_party="libuv,libunwind,openfec,alsa,pulseaudio:10.0,speexdsp,sox,openssl,cpputest" +third_party="libuv,libunwind,openfec,alsa,pulseaudio:10.0,speexdsp,sox,openssl,cpputest,sndfile" scons -Q \ --enable-werror \ diff --git a/scripts/ci_checks/linux-checks/conditional-build.sh b/scripts/ci_checks/linux-checks/conditional-build.sh index 0ce4f2598..9b066ea76 100755 --- a/scripts/ci_checks/linux-checks/conditional-build.sh +++ b/scripts/ci_checks/linux-checks/conditional-build.sh @@ -12,7 +12,8 @@ scons -Q --enable-werror --build-3rdparty=all \ --disable-openssl \ --disable-speex \ --disable-sox \ - --disable-pulseaudio + --disable-pulseaudio \ + --disable-sndfile # optional dependencies: none, optional targets: all scons -Q --enable-werror --build-3rdparty=all \ @@ -26,6 +27,7 @@ scons -Q --enable-werror --build-3rdparty=all \ --disable-speex \ --disable-sox \ --disable-pulseaudio \ + --disable-sndfile \ test # optional dependencies: all, optional targets: all diff --git a/scripts/ci_checks/linux-checks/pulseaudio-versions.sh b/scripts/ci_checks/linux-checks/pulseaudio-versions.sh index 24d0fd399..b263007ca 100755 --- a/scripts/ci_checks/linux-checks/pulseaudio-versions.sh +++ b/scripts/ci_checks/linux-checks/pulseaudio-versions.sh @@ -8,6 +8,6 @@ do --enable-werror \ --enable-tests \ --enable-examples \ - --build-3rdparty=openfec,pulseaudio:$pulse_ver \ + --build-3rdparty=openfec,pulseaudio:$pulse_ver,sndfile \ test done diff --git a/scripts/ci_checks/macos/standard-build.sh b/scripts/ci_checks/macos/standard-build.sh index 55a3d396e..3e4d0d98a 100755 --- a/scripts/ci_checks/macos/standard-build.sh +++ b/scripts/ci_checks/macos/standard-build.sh @@ -5,7 +5,7 @@ set -euxo pipefail brew install --force --overwrite \ automake scons ragel gengetopt \ libuv speexdsp sox openssl@3 \ - cpputest google-benchmark + cpputest google-benchmark libsndfile # debug build scons -Q \ diff --git a/src/internal_modules/roc_audio/frame.h b/src/internal_modules/roc_audio/frame.h index 1dd212805..2fff211c5 100644 --- a/src/internal_modules/roc_audio/frame.h +++ b/src/internal_modules/roc_audio/frame.h @@ -52,7 +52,7 @@ class Frame : public core::NonCopyable<> { FlagNotComplete = (1 << 2), //! Set if some late packets were dropped while the frame was being built. - //! It's not necessarty that the frame itself is blank or incomplete. + //! It's not necessarily that the frame itself is blank or incomplete. FlagPacketDrops = (1 << 3) }; diff --git a/src/internal_modules/roc_audio/sample_spec.cpp b/src/internal_modules/roc_audio/sample_spec.cpp index fe9629092..de2e717b3 100644 --- a/src/internal_modules/roc_audio/sample_spec.cpp +++ b/src/internal_modules/roc_audio/sample_spec.cpp @@ -13,6 +13,7 @@ #include "roc_core/macro_helpers.h" #include "roc_core/panic.h" #include "roc_packet/units.h" +#include "sample_spec.h" namespace roc { namespace audio { @@ -136,6 +137,11 @@ bool SampleSpec::is_valid() const { && sample_rate_ != 0 && channel_set_.is_valid(); } +bool SampleSpec::is_empty() const { + return sample_fmt_ == SampleFormat_Invalid && pcm_fmt_ == PcmFormat_Invalid + && sample_rate_ == 0 && channel_set_.num_channels() == 0; +} + bool SampleSpec::is_raw() const { return sample_fmt_ == SampleFormat_Pcm && pcm_fmt_ == Sample_RawFormat; } diff --git a/src/internal_modules/roc_audio/sample_spec.h b/src/internal_modules/roc_audio/sample_spec.h index dc5f3cc9b..e0e8fd5a1 100644 --- a/src/internal_modules/roc_audio/sample_spec.h +++ b/src/internal_modules/roc_audio/sample_spec.h @@ -66,6 +66,9 @@ class SampleSpec { //! Check if sample spec has non-zero rate and valid channel set. bool is_valid() const; + //! Check if sample spec has a zero rate, empty channel set, and invalid_format. + bool is_empty() const; + //! Check if samples are in raw format. //! @returns //! true if sample_format() is SampleFormat_Pcm and pcm_format() diff --git a/src/internal_modules/roc_sndio/backend_map.cpp b/src/internal_modules/roc_sndio/backend_map.cpp index c26926685..1e0bacc55 100644 --- a/src/internal_modules/roc_sndio/backend_map.cpp +++ b/src/internal_modules/roc_sndio/backend_map.cpp @@ -56,7 +56,10 @@ void BackendMap::register_backends_() { pulseaudio_backend_.reset(new (pulseaudio_backend_) PulseaudioBackend); add_backend_(pulseaudio_backend_.get()); #endif // ROC_TARGET_PULSEAUDIO - +#ifdef ROC_TARGET_SNDFILE + sndfile_backend_.reset(new (sndfile_backend_) SndfileBackend); + add_backend_(sndfile_backend_.get()); +#endif // ROC_TARGET_SNDFILE #ifdef ROC_TARGET_SOX sox_backend_.reset(new (sox_backend_) SoxBackend); add_backend_(sox_backend_.get()); diff --git a/src/internal_modules/roc_sndio/backend_map.h b/src/internal_modules/roc_sndio/backend_map.h index 7459e06e2..8ec6f658d 100644 --- a/src/internal_modules/roc_sndio/backend_map.h +++ b/src/internal_modules/roc_sndio/backend_map.h @@ -22,6 +22,10 @@ #include "roc_sndio/pulseaudio_backend.h" #endif // ROC_TARGET_PULSEAUDIO +#ifdef ROC_TARGET_SNDFILE +#include "roc_sndio/sndfile_backend.h" +#endif // ROC_TARGET_SNDFILE + #ifdef ROC_TARGET_SOX #include "roc_sndio/sox_backend.h" #endif // ROC_TARGET_SOX @@ -69,6 +73,10 @@ class BackendMap : public core::NonCopyable<> { core::Optional pulseaudio_backend_; #endif // ROC_TARGET_PULSEAUDIO +#ifdef ROC_TARGET_SNDFILE + core::Optional sndfile_backend_; +#endif // ROC_TARGET_SNDFILE + #ifdef ROC_TARGET_SOX core::Optional sox_backend_; #endif // ROC_TARGET_SOX diff --git a/src/internal_modules/roc_sndio/ibackend.h b/src/internal_modules/roc_sndio/ibackend.h index 71a273f29..921b6c7f8 100644 --- a/src/internal_modules/roc_sndio/ibackend.h +++ b/src/internal_modules/roc_sndio/ibackend.h @@ -40,6 +40,8 @@ class IBackend { const char* path, const Config& config, core::IArena& arena) = 0; + //! Returns name of backend. + virtual const char* name() const = 0; }; } // namespace sndio diff --git a/src/internal_modules/roc_sndio/isource.h b/src/internal_modules/roc_sndio/isource.h index f9de99f93..6630975c4 100644 --- a/src/internal_modules/roc_sndio/isource.h +++ b/src/internal_modules/roc_sndio/isource.h @@ -26,7 +26,7 @@ class ISource : virtual public IDevice, public audio::IFrameReader { //! Adjust source clock to match consumer clock. //! @remarks - //! Invoked regularly after reading every or a several frames. + //! Invoked regularly after reading every or several frames. //! @p timestamp defines the time in Unix domain when the last sample of the last //! frame read from source is going to be actually processed by consumer. virtual void reclock(core::nanoseconds_t timestamp) = 0; diff --git a/src/internal_modules/roc_sndio/print_supported.cpp b/src/internal_modules/roc_sndio/print_supported.cpp index b74e88a1a..333803397 100644 --- a/src/internal_modules/roc_sndio/print_supported.cpp +++ b/src/internal_modules/roc_sndio/print_supported.cpp @@ -61,7 +61,7 @@ bool print_supported(BackendDispatcher& backend_dispatcher, core::IArena& arena) } prn.writef("\nsupported formats for audio files:\n"); - print_string_list(prn, list, ".", ""); + print_string_list(prn, list, "", ""); return true; } diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp index b5474fbb7..ed5005cf0 100644 --- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp +++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp @@ -59,5 +59,9 @@ IDevice* PulseaudioBackend::open_device(DeviceType device_type, return device.release(); } +const char* PulseaudioBackend::name() const { + return "PulseAudio"; +} + } // namespace sndio } // namespace roc diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h index 1bb9785cb..ed206da02 100644 --- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h +++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h @@ -33,6 +33,8 @@ class PulseaudioBackend : public IBackend, core::NonCopyable<> { const char* path, const Config& config, core::IArena& arena); + + virtual const char* name() const; }; } // namespace sndio diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp new file mode 100644 index 000000000..5a1651ab8 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 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_sndio/sndfile_backend.h" +#include "roc_core/log.h" +#include "roc_core/scoped_ptr.h" +#include "roc_sndio/sndfile_extension_table.h" +#include "roc_sndio/sndfile_sink.h" +#include "roc_sndio/sndfile_source.h" + +namespace roc { +namespace sndio { + +SndfileBackend::SndfileBackend() { + roc_log(LogDebug, "sndfile backend: initializing"); +} + +void SndfileBackend::discover_drivers(core::Array& driver_list) { + SF_FORMAT_INFO format_info; + int total_number_of_drivers; + + if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, + &total_number_of_drivers, sizeof(int))) { + roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed %s", + sf_error_number(errnum)); + } + + for (int format_index = 0; format_index < total_number_of_drivers; format_index++) { + format_info.format = format_index; + if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR, &format_info, + sizeof(format_info))) { + roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed %s", + sf_error_number(errnum)); + } + + const char* driver = format_info.extension; + + for (size_t map_index = 0; map_index < ROC_ARRAY_SIZE(file_type_map); + map_index++) { + if (file_type_map[map_index].format_id == format_info.format) { + driver = file_type_map[map_index].driver_name; + } + } + + if (!driver_list.push_back( + DriverInfo(driver, DriverType_File, + DriverFlag_SupportsSource | DriverFlag_SupportsSink, this))) { + roc_panic("sndfile backend: driver_list.push_back(DriverInfo) failed to add " + "driver"); + } + } +} + +IDevice* SndfileBackend::open_device(DeviceType device_type, + DriverType driver_type, + const char* driver, + const char* path, + const Config& config, + core::IArena& arena) { + if (driver_type != DriverType_File) { + roc_log(LogDebug, "sndfile backend: driver=%s is not a file type", driver); + return NULL; + } + + switch (device_type) { + case DeviceType_Sink: { + core::ScopedPtr sink(new (arena) SndfileSink(arena, config), arena); + if (!sink || !sink->is_valid()) { + roc_log(LogDebug, "sndfile backend: can't construct sink: driver=%s path=%s", + driver, path); + return NULL; + } + + if (!sink->open(driver, path)) { + roc_log(LogDebug, "sndfile backend: open failed: driver=%s path=%s", driver, + path); + return NULL; + } + + return sink.release(); + } break; + + case DeviceType_Source: { + core::ScopedPtr source(new (arena) SndfileSource(arena, config), + arena); + if (!source || !source->is_valid()) { + roc_log(LogDebug, + "sndfile backend: can't construct source: driver=%s path=%s", driver, + path); + return NULL; + } + + if (!source->open(driver, path)) { + roc_log(LogDebug, "sndfile backend: open failed: driver=%s path=%s", driver, + path); + return NULL; + } + + return source.release(); + } break; + + default: + break; + } + + roc_panic("sndfile backend: invalid device type"); +} + +const char* SndfileBackend::name() const { + return "sndfile"; +} +} // namespace sndio +} // namespace roc diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h new file mode 100644 index 000000000..ca2899212 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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_sndio/target_sndfile/roc_sndio/sndfile_backend.h +//! @brief SndFile backend. + +#ifndef ROC_SNDIO_SNDFILE_BACKEND_H_ +#define ROC_SNDIO_SNDFILE_BACKEND_H_ + +#include + +#include "roc_audio/sample_spec.h" +#include "roc_core/noncopyable.h" +#include "roc_sndio/ibackend.h" + +namespace roc { +namespace sndio { + +//! Sndfile backend. +class SndfileBackend : public IBackend, core::NonCopyable<> { +public: + SndfileBackend(); + + //! Append supported drivers to the list. + virtual void discover_drivers(core::Array& driver_list); + + //! Create and open a sink or source. + virtual IDevice* open_device(DeviceType device_type, + DriverType driver_type, + const char* driver, + const char* path, + const Config& config, + core::IArena& arena); + //! Returns name of backend. + virtual const char* name() const; +}; + +} // namespace sndio +} // namespace roc + +#endif // ROC_SNDIO_SNDFILE_BACKEND_H_ diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.cpp new file mode 100644 index 000000000..c95c9d213 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.cpp @@ -0,0 +1,16 @@ +/* + * 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 "sndfile_extension_table.h" +#include "sndfile.h" + +FileMap file_type_map[5] = { { SF_FORMAT_MAT4, "mat4", NULL }, + { SF_FORMAT_MAT5, "mat5", NULL }, + { SF_FORMAT_WAV, "wav", "wav" }, + { SF_FORMAT_NIST, "nist", NULL }, + { SF_FORMAT_WAVEX, "wavex", NULL } }; diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.h new file mode 100644 index 000000000..661d51a5c --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_extension_table.h @@ -0,0 +1,28 @@ +/* + * 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_sndio/target_sndfile/roc_sndio/sndfile_extension_table.h +//! @brief Sndfile driver map. + +#ifndef ROC_SNDIO_SNDFILE_EXTENSION_TABLE_H_ +#define ROC_SNDIO_SNDFILE_EXTENSION_TABLE_H_ + +//! Sndfile driver map. +struct FileMap { + //! SF_FORMAT ID corresponding to the enum value in sndfile.h + int format_id; + //! Name of driver mapped to SF_FORMAT + const char* driver_name; + //! File extension associated with driver and SF_FORMAT if it exists. + const char* file_extension; +}; + +//! Declare the file_type_map as extern +extern FileMap file_type_map[5]; + +#endif // ROC_SNDIO_SNDFILE_EXTENSION_TABLE_H_ diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp new file mode 100644 index 000000000..05dea2674 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2023 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/. + */ + +#define BUFFER_SIZE 512 + +#include "roc_sndio/sndfile_sink.h" +#include "roc_core/log.h" +#include "roc_core/panic.h" +#include "roc_sndio/backend_map.h" +#include "roc_sndio/sndfile_extension_table.h" + +namespace roc { +namespace sndio { +namespace { + +bool map_to_sub_format(SF_INFO& file_info_, int format_enum) { + // Provides the minimum number of sub formats needed to support all possible major + // formats + int high_to_low_sub_formats[4] = { SF_FORMAT_PCM_24, SF_FORMAT_PCM_16, + SF_FORMAT_DPCM_16 }; + + for (size_t format_attempt = 0; + format_attempt < ROC_ARRAY_SIZE(high_to_low_sub_formats); format_attempt++) { + file_info_.format = format_enum | high_to_low_sub_formats[format_attempt]; + + if (sf_format_check(&file_info_)) { + return true; + } + } + + return false; +} + +bool map_to_sndfile(const char** driver, const char* path, SF_INFO& file_info_) { + const char* file_extension; + const char* dot = strrchr(path, '.'); + + if (!dot || dot == path) { + return false; + } + + file_extension = dot + 1; + + int format_enum = 0; + + if (*driver == NULL) { + for (size_t file_map_index = 0; file_map_index < ROC_ARRAY_SIZE(file_type_map); + file_map_index++) { + if (file_type_map[file_map_index].file_extension != NULL) { + if (strcmp(file_extension, file_type_map[file_map_index].file_extension) + == 0) { + format_enum = file_type_map[file_map_index].format_id; + *driver = file_extension; + break; + } + } + } + } else { + for (size_t file_map_index = 0; file_map_index < ROC_ARRAY_SIZE(file_type_map); + file_map_index++) { + if (strcmp(*driver, file_type_map[file_map_index].driver_name) == 0) { + format_enum = file_type_map[file_map_index].format_id; + break; + } + } + } + + if (format_enum == 0) { + SF_FORMAT_INFO info; + int major_count, format_index; + if (int errnum = + sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_count, sizeof(int))) { + roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed %s", + sf_error_number(errnum)); + } + + for (format_index = 0; format_index < major_count; format_index++) { + info.format = format_index; + if (int errnum = + sf_command(NULL, SFC_GET_FORMAT_MAJOR, &info, sizeof(info))) { + roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed %s", + sf_error_number(errnum)); + } + + if (*driver == NULL) { + if (strcmp(info.extension, file_extension) == 0) { + format_enum = info.format; + *driver = file_extension; + break; + } + } else { + if (strcmp(info.extension, *driver) == 0) { + format_enum = info.format; + break; + } + } + } + } + + if (format_enum == 0) { + return false; + } + + roc_log(LogDebug, "detected file format type `%s'", *driver); + + file_info_.format = format_enum | file_info_.format; + + if (sf_format_check(&file_info_)) { + return true; + } else { + return map_to_sub_format(file_info_, format_enum); + } +} + +} // namespace + +SndfileSink::SndfileSink(core::IArena& arena, const Config& config) + : file_(NULL) + , valid_(false) { + if (config.sample_spec.num_channels() == 0) { + roc_log(LogError, "sndfile sink: # of channels is zero"); + return; + } + + if (config.latency != 0) { + roc_log(LogError, + "sndfile sink: setting io latency not supported by sndfile backend"); + return; + } + + frame_length_ = config.frame_length; + + if (frame_length_ == 0) { + roc_log(LogError, "sndfile sink: frame length is zero"); + return; + } + + sample_spec_ = config.sample_spec; + + if (sample_spec_.sample_format() == audio::SampleFormat_Invalid) { + sample_spec_.set_sample_format(audio::SampleFormat_Pcm); + sample_spec_.set_pcm_format(audio::PcmFormat_SInt32); + } + + if (sample_spec_.sample_rate() == 0) { + sample_spec_.set_sample_rate(41000); + } + + if (!sample_spec_.channel_set().is_valid()) { + sample_spec_.channel_set().set_layout(audio::ChanLayout_Surround); + sample_spec_.channel_set().set_order(audio::ChanOrder_Smpte); + sample_spec_.channel_set().set_channel_range(0, 2, true); + } + + memset(&file_info_, 0, sizeof(file_info_)); + + // TODO(gh-696): map format from sample_space + file_info_.format = SF_FORMAT_PCM_32; + file_info_.channels = (int)sample_spec_.num_channels(); + file_info_.samplerate = (int)sample_spec_.sample_rate(); + + valid_ = true; +} + +SndfileSink::~SndfileSink() { + close_(); +} + +bool SndfileSink::is_valid() const { + return valid_; +} + +bool SndfileSink::open(const char* driver, const char* path) { + roc_panic_if(!valid_); + + roc_log(LogDebug, "sndfile sink: opening: driver=%s path=%s", driver, path); + + if (file_) { + roc_panic("sndfile sink: can't call open() more than once"); + } + + if (!open_(driver, path)) { + return false; + } + + return true; +} + +ISink* SndfileSink::to_sink() { + return this; +} + +ISource* SndfileSink::to_source() { + return NULL; +} + +DeviceType SndfileSink::type() const { + return DeviceType_Sink; +} + +DeviceState SndfileSink::state() const { + return DeviceState_Active; +} + +void SndfileSink::pause() { + // no-op +} + +bool SndfileSink::resume() { + // no-op + return true; +} + +bool SndfileSink::restart() { + // no-op + return true; +} + +audio::SampleSpec SndfileSink::sample_spec() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile sink: sample_rate(): non-open output file"); + } + + if (file_info_.channels == 1) { + return audio::SampleSpec(size_t(file_info_.samplerate), audio::Sample_RawFormat, + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, + audio::ChanMask_Surround_Mono); + } + + if (file_info_.channels == 2) { + return audio::SampleSpec(size_t(file_info_.samplerate), audio::Sample_RawFormat, + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, + audio::ChanMask_Surround_Stereo); + } + + roc_panic("sndfile sink: unsupported channel count"); +} + +core::nanoseconds_t SndfileSink::latency() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile sink: latency(): non-open output file"); + } + + return 0; +} + +bool SndfileSink::has_latency() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile sink: has_latency(): non-open output file"); + } + + return false; +} + +bool SndfileSink::has_clock() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile sink: has_clock(): non-open output file"); + } + + return false; +} + +void SndfileSink::write(audio::Frame& frame) { + roc_panic_if(!valid_); + + audio::sample_t* frame_data = frame.raw_samples(); + sf_count_t frame_left = (sf_count_t)frame.num_raw_samples(); + + // Write entire float buffer in one call + sf_count_t count = sf_write_float(file_, frame_data, frame_left); + + int errnum = sf_error(file_); + if (count != frame_left || errnum != 0) { + // TODO(gh-183): return error instead of panic + roc_panic("sndfile source: sf_write_float() failed: %s", sf_error_number(errnum)); + } +} + +bool SndfileSink::open_(const char* driver, const char* path) { + unsigned long in_rate = (unsigned long)file_info_.samplerate; + + unsigned long out_rate = (unsigned long)file_info_.samplerate; + + if (!map_to_sndfile(&driver, path, file_info_)) { + roc_log(LogDebug, + "sndfile sink: map_to_sndfile(): Cannot find valid subtype format for " + "major format type"); + return false; + } + + file_ = sf_open(path, SFM_WRITE, &file_info_); + if (!file_) { + roc_log(LogDebug, "sndfile sink: %s, can't open: driver=%s path=%s", + sf_strerror(file_), driver, path); + return false; + } + + if (sf_command(file_, SFC_SET_UPDATE_HEADER_AUTO, NULL, SF_TRUE) == 0) { + roc_log(LogDebug, + "sndfile sink: sf_command(SFC_SET_UPDATE_HEADER_AUTO) returned false"); + return false; + } + + sample_spec_.set_sample_rate((unsigned long)file_info_.samplerate); + + roc_log(LogInfo, + "sndfile sink:" + " opened: out_rate=%lu in_rate=%lu ch=%lu", + out_rate, in_rate, (unsigned long)file_info_.channels); + + sf_count_t err = sf_seek(file_, 0, SEEK_SET); + if (err == -1) { + roc_log(LogError, "sndfile sink: sf_seek(): %s", sf_strerror(file_)); + return false; + } + + return true; +} + +void SndfileSink::close_() { + if (!file_) { + return; + } + + roc_log(LogDebug, "sndfile sink: closing output"); + + int err = sf_close(file_); + if (err != 0) { + roc_panic("sndfile sink: sf_close() failed. Cannot close output: %s", + sf_error_number(err)); + } + + file_ = NULL; +} + +} // namespace sndio +} // namespace roc diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h new file mode 100644 index 000000000..a23971e98 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 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_sndio/target_sndfile/roc_sndio/sndfile_sink.h +//! @brief SndFile sink. + +#ifndef ROC_SNDIO_SNDFILE_SINK_H_ +#define ROC_SNDIO_SNDFILE_SINK_H_ + +#include + +#include "roc_audio/sample_spec.h" +#include "roc_core/array.h" +#include "roc_core/iarena.h" +#include "roc_core/noncopyable.h" +#include "roc_core/stddefs.h" +#include "roc_packet/units.h" +#include "roc_sndio/config.h" +#include "roc_sndio/isink.h" + +namespace roc { +namespace sndio { + +//! Sndfile sink. +//! @remarks +//! Writes samples to output file or device. +//! Supports multiple drivers for different file types and audio systems. +class SndfileSink : public ISink, public core::NonCopyable<> { +public: + //! Initialize. + SndfileSink(core::IArena& arena, const Config& config); + + virtual ~SndfileSink(); + + //! Check if the object was successfully constructed. + bool is_valid() const; + + //! Open output file or device. + //! + //! @b Parameters + //! - @p driver is output driver name; + //! - @p path is output file or device name, "-" for stdout. + //! + //! @remarks + //! If @p driver or @p path are NULL, defaults are used. + bool open(const char* driver, const char* path); + + //! Cast IDevice to ISink. + virtual ISink* to_sink(); + + //! Cast IDevice to ISink. + virtual ISource* to_source(); + + //! Get device type. + virtual DeviceType type() const; + + //! Get device state. + virtual DeviceState state() const; + + //! Pause reading. + virtual void pause(); + + //! Resume paused reading. + virtual bool resume(); + + //! Restart reading from the beginning. + virtual bool restart(); + + //! Get sample specification of the sink. + virtual audio::SampleSpec sample_spec() const; + + //! Get latency of the sink. + virtual core::nanoseconds_t latency() const; + + //! Check if the sink supports latency reports. + virtual bool has_latency() const; + + //! Check if the sink has own clock. + virtual bool has_clock() const; + + //! Write audio frame. + virtual void write(audio::Frame& frame); + +private: + bool open_(const char* driver, const char* path); + void close_(); + + SNDFILE* file_; + SF_INFO file_info_; + + core::nanoseconds_t frame_length_; + audio::SampleSpec sample_spec_; + bool valid_; +}; + +} // namespace sndio +} // namespace roc + +#endif // ROC_SNDIO_SNDFILE_SINK_H_ diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp new file mode 100644 index 000000000..b8ffaf674 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 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_sndio/sndfile_source.h" +#include "roc_core/log.h" +#include "roc_core/panic.h" +#include "roc_sndio/backend_map.h" + +namespace roc { +namespace sndio { + +SndfileSource::SndfileSource(core::IArena& arena, const Config& config) + : file_(NULL) + , path_(NULL) + , eof_(false) + , valid_(false) { + if (config.latency != 0) { + roc_log(LogError, + "sndfile source: setting io latency not supported by sndfile backend"); + return; + } + + if (!config.sample_spec.is_empty()) { + roc_log(LogError, "sndfile source: setting io encoding not supported"); + return; + } + + frame_length_ = config.frame_length; + sample_spec_ = config.sample_spec; + + if (frame_length_ == 0) { + roc_log(LogError, "sndfile source: frame length is zero"); + return; + } + + memset(&file_info_, 0, sizeof(file_info_)); + sample_rate_ = config.sample_spec.sample_rate(); + valid_ = true; +} + +SndfileSource::~SndfileSource() { + close_(); +} + +bool SndfileSource::is_valid() const { + return valid_; +} + +bool SndfileSource::open(const char* driver, const char* path) { + roc_panic_if(!valid_); + + if (path) { + path_ = path; + } + + roc_log(LogInfo, "sndfile source: opening: driver=%s path=%s", driver, path); + + if (file_) { + roc_panic("sndfile source: can't call open() more than once"); + } + + if (!open_(path)) { + return false; + } + + return true; +} + +ISink* SndfileSource::to_sink() { + return NULL; +} + +ISource* SndfileSource::to_source() { + return this; +} + +DeviceType SndfileSource::type() const { + return DeviceType_Source; +} + +DeviceState SndfileSource::state() const { + roc_panic_if(!valid_); + return DeviceState_Active; +} + +void SndfileSource::pause() { + // no-op +} + +bool SndfileSource::resume() { + // no-op + return true; +} + +bool SndfileSource::restart() { + roc_panic_if(!valid_); + + roc_log(LogDebug, "sndfile source: restarting"); + + if (!eof_) { + if (!seek_(0)) { + roc_log(LogError, "sndfile source: seek failed when restarting"); + return false; + } + } else { + if (file_) { + close_(); + } + + if (!open_(path_)) { + roc_log(LogError, "sndfile source: open failed when restarting"); + return false; + } + } + + eof_ = false; + + return true; +} + +audio::SampleSpec SndfileSource::sample_spec() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: sample_rate(): non-open output file"); + } + + audio::ChannelSet channel_set; + channel_set.set_layout(audio::ChanLayout_Surround); + channel_set.set_order(audio::ChanOrder_Smpte); + channel_set.set_channel_range(0, (size_t)file_info_.channels - 1, true); + + return audio::SampleSpec(size_t(file_info_.samplerate), audio::Sample_RawFormat, + channel_set); +} + +core::nanoseconds_t SndfileSource::latency() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: latency(): non-open output file"); + } + + return 0; +} + +bool SndfileSource::has_latency() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: has_latency(): non-open input file"); + } + + return false; +} + +bool SndfileSource::has_clock() const { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: has_clock(): non-open input file"); + } + + return false; +} + +void SndfileSource::reclock(core::nanoseconds_t) { + // no-op +} + +bool SndfileSource::read(audio::Frame& frame) { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: read: non-open input file"); + } + + audio::sample_t* frame_data = frame.raw_samples(); + // size_t num_channels = (size_t)file_info_.channels; + sf_count_t frame_left = (sf_count_t)frame.num_raw_samples(); + // size_t samples_per_ch = frame.num_raw_samples() / num_channels; + + sf_count_t n_samples = sf_read_float(file_, frame_data, frame_left); + if (sf_error(file_) != 0) { + // TODO(gh-183): return error instead of panic + roc_panic("sndfile source: sf_read_float() failed: %s", sf_strerror(file_)); + } + + if (n_samples == 0) { + eof_ = true; + } + + if (n_samples < frame_left && n_samples != 0) { + memset(frame.raw_samples() + (size_t)n_samples, 0, + (size_t)(frame_left - n_samples) * sizeof(audio::sample_t)); + } + + return !eof_; +} + +bool SndfileSource::seek_(size_t offset) { + roc_panic_if(!valid_); + + if (!file_) { + roc_panic("sndfile source: seek: non-open input file"); + } + + roc_log(LogDebug, "sndfile source: resetting position to %lu", (unsigned long)offset); + + sf_count_t err = sf_seek(file_, (sf_count_t)offset, SEEK_SET); + if (err == -1) { + roc_log(LogError, "sndfile source: sf_seek(): %s", sf_strerror(file_)); + return false; + } + + return true; +} + +bool SndfileSource::open_(const char* path) { + if (file_) { + roc_panic("sndfile source: can't open: already opened"); + } + + file_info_.format = 0; + + file_ = sf_open(path, SFM_READ, &file_info_); + if (!file_) { + roc_log(LogInfo, "sndfile source: can't open: input=%s, %s", !path ? NULL : path, + sf_strerror(file_)); + return false; + } + + sample_spec_.set_sample_rate((unsigned long)file_info_.samplerate); + + roc_log(LogInfo, + "sndfile source:" + " in_rate=%lu out_rate=%lu" + " in_ch=%lu out_ch=%lu", + (unsigned long)file_info_.samplerate, (unsigned long)sample_rate_, + (unsigned long)file_info_.channels, (unsigned long)0); + + return true; +} + +void SndfileSource::close_() { + if (!file_) { + return; + } + + roc_log(LogInfo, "sndfile source: closing input"); + int err = sf_close(file_); + if (err != 0) { + roc_panic("sndfile source: sf_close() failed. Cannot close input: %s", + sf_error_number(err)); + } + + file_ = NULL; +} + +} // namespace sndio +} // namespace roc diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h new file mode 100644 index 000000000..60c2706b4 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 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_sndio/target_sndfile/roc_sndio/sndfile_source.h +//! @brief Sndfile source. + +#ifndef ROC_SNDIO_SNDFILE_SOURCE_H_ +#define ROC_SNDIO_SNDFILE_SOURCE_H_ + +#include + +#include "roc_audio/sample_spec.h" +#include "roc_core/array.h" +#include "roc_core/iarena.h" +#include "roc_core/noncopyable.h" +#include "roc_core/stddefs.h" +#include "roc_core/string_buffer.h" +#include "roc_packet/units.h" +#include "roc_sndio/config.h" +#include "roc_sndio/isource.h" + +namespace roc { +namespace sndio { + +//! Sndfile source. +//! @remarks +//! Reads samples from input file or device. +//! Supports multiple drivers for different file types and audio systems. +class SndfileSource : public ISource, private core::NonCopyable<> { +public: + //! Initialize. + SndfileSource(core::IArena& arena, const Config& config); + + virtual ~SndfileSource(); + + //! Check if the object was successfully constructed. + bool is_valid() const; + + //! Open input file or device. + //! + //! @b Parameters + //! - @p driver is input driver name; + //! - @p path is input file or device name, "-" for stdin. + //! + //! @remarks + //! If @p driver or @p path are NULL, defaults are used. + bool open(const char* driver, const char* path); + + //! Cast IDevice to ISink. + virtual ISink* to_sink(); + + //! Cast IDevice to ISink. + virtual ISource* to_source(); + + //! Get device type. + virtual DeviceType type() const; + + //! Get device state. + virtual DeviceState state() const; + + //! Pause reading. + virtual void pause(); + + //! Resume paused reading. + virtual bool resume(); + + //! Restart reading from the beginning. + virtual bool restart(); + + //! Get sample specification of the source. + virtual audio::SampleSpec sample_spec() const; + + //! Get latency of the source. + virtual core::nanoseconds_t latency() const; + + //! Check if the source supports latency reports. + virtual bool has_latency() const; + + //! Check if the source has own clock. + virtual bool has_clock() const; + + //! Adjust source clock to match consumer clock. + virtual void reclock(core::nanoseconds_t timestamp); + + //! Read frame. + virtual bool read(audio::Frame&); + +private: + bool open_(const char* path); + void close_(); + + bool seek_(size_t offset); + + core::nanoseconds_t frame_length_; + audio::SampleSpec sample_spec_; + + SNDFILE* file_; + SF_INFO file_info_; + const char* path_; + size_t sample_rate_; + bool eof_; + bool valid_; +}; + +} // namespace sndio +} // namespace roc + +#endif // ROC_SNDIO_SNDFILE_SOURCE_H_ diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp index 11ff72cfa..20a40b5eb 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp @@ -294,5 +294,9 @@ IDevice* SoxBackend::open_device(DeviceType device_type, roc_panic("sox backend: invalid device type"); } +const char* SoxBackend::name() const { + return "SoX"; +} + } // namespace sndio } // namespace roc diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h index a0a863b37..01d0162d3 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h @@ -42,6 +42,8 @@ class SoxBackend : public IBackend, core::NonCopyable<> { const char* path, const Config& config, core::IArena& arena); + //! Returns name of backend. + virtual const char* name() const; private: bool first_created_; diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp index b8a762b07..3b5a3743e 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp @@ -26,17 +26,13 @@ SoxSource::SoxSource(core::IArena& arena, const Config& config) , valid_(false) { BackendMap::instance(); - if (config.sample_spec.num_channels() == 0) { - roc_log(LogError, "sox source: # of channels is zero"); - return; - } - if (config.latency != 0) { roc_log(LogError, "sox source: setting io latency not supported by sox backend"); return; } frame_length_ = config.frame_length; + sample_spec_ = config.sample_spec; if (frame_length_ == 0) { @@ -113,7 +109,7 @@ void SoxSource::pause() { } if (!input_) { - roc_panic("sox source: pause: non-open input file or device"); + roc_panic("sox source: pause: non-open device"); } roc_log(LogDebug, "sox source: pausing: driver=%s input=%s", driver_name_.c_str(), @@ -156,12 +152,17 @@ bool SoxSource::restart() { if (is_file_ && !eof_) { if (!seek_(0)) { + roc_panic("Reached"); roc_log(LogError, "sox source: seek failed when restarting: driver=%s input=%s", driver_name_.c_str(), input_name_.c_str()); return false; } } else { + if (is_file_) { + sample_spec_.clear(); + } + if (input_) { close_(); } @@ -358,16 +359,25 @@ bool SoxSource::open_() { return false; } - if (input_->signal.channels != sample_spec_.num_channels()) { - roc_log(LogError, - "sox source: can't open: unsupported # of channels: " - "expected=%lu actual=%lu", - (unsigned long)sample_spec_.num_channels(), - (unsigned long)input_->signal.channels); - return false; + is_file_ = !(input_->handler.flags & SOX_FILE_DEVICE); + + if (is_file_) { + if (!sample_spec_.is_empty()) { + roc_log(LogError, "sox source: setting io encoding for files not supported"); + return false; + } + sample_spec_ = sample_spec(); + } else { + if (input_->signal.channels != sample_spec_.num_channels()) { + roc_log(LogError, + "sox source: can't open: unsupported # of channels: " + "expected=%lu actual=%lu", + (unsigned long)sample_spec_.num_channels(), + (unsigned long)input_->signal.channels); + return false; + } } - is_file_ = !(input_->handler.flags & SOX_FILE_DEVICE); sample_spec_.set_sample_rate((unsigned long)input_->signal.rate); roc_log(LogInfo, diff --git a/src/internal_modules/roc_sndio/wav_backend.cpp b/src/internal_modules/roc_sndio/wav_backend.cpp index 4909da682..6014add82 100644 --- a/src/internal_modules/roc_sndio/wav_backend.cpp +++ b/src/internal_modules/roc_sndio/wav_backend.cpp @@ -76,5 +76,9 @@ IDevice* WavBackend::open_device(DeviceType device_type, roc_panic("wav backend: invalid device type"); } +const char* WavBackend::name() const { + return "wav"; +} + } // namespace sndio } // namespace roc diff --git a/src/internal_modules/roc_sndio/wav_backend.h b/src/internal_modules/roc_sndio/wav_backend.h index 42d5d89cb..54ce2c46c 100644 --- a/src/internal_modules/roc_sndio/wav_backend.h +++ b/src/internal_modules/roc_sndio/wav_backend.h @@ -34,6 +34,9 @@ class WavBackend : public IBackend, core::NonCopyable<> { const char* path, const Config& config, core::IArena& arena); + + //! Returns name of backend. + virtual const char* name() const; }; } // namespace sndio diff --git a/src/internal_modules/roc_sndio/wav_source.cpp b/src/internal_modules/roc_sndio/wav_source.cpp index 4dd260c61..e00353d64 100644 --- a/src/internal_modules/roc_sndio/wav_source.cpp +++ b/src/internal_modules/roc_sndio/wav_source.cpp @@ -14,14 +14,15 @@ namespace roc { namespace sndio { WavSource::WavSource(core::IArena& arena, const Config& config) - : eof_(false) + : file_opened_(false) + , eof_(false) , valid_(false) { if (config.latency != 0) { roc_log(LogError, "wav source: setting io latency not supported"); return; } - if (config.sample_spec.is_valid()) { + if (!config.sample_spec.is_empty()) { roc_log(LogError, "wav source: setting io encoding not supported"); return; } diff --git a/src/tests/roc_sndio/target_sox/test_pump.cpp b/src/tests/roc_sndio/target_sox/test_pump.cpp deleted file mode 100644 index 01c3710c9..000000000 --- a/src/tests/roc_sndio/target_sox/test_pump.cpp +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2015 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 - -#include "test_helpers/mock_sink.h" -#include "test_helpers/mock_source.h" - -#include "roc_core/buffer_factory.h" -#include "roc_core/heap_arena.h" -#include "roc_core/stddefs.h" -#include "roc_core/temp_file.h" -#include "roc_sndio/pump.h" -#include "roc_sndio/sox_sink.h" -#include "roc_sndio/sox_source.h" - -namespace roc { -namespace sndio { - -namespace { - -enum { BufSize = 512, SampleRate = 44100, ChMask = 0x3 }; - -const audio::SampleSpec SampleSpecs(SampleRate, - audio::Sample_RawFormat, - audio::ChanLayout_Surround, - audio::ChanOrder_Smpte, - ChMask); - -const core::nanoseconds_t BufDuration = BufSize * core::Second - / core::nanoseconds_t(SampleSpecs.sample_rate() * SampleSpecs.num_channels()); - -core::HeapArena arena; -core::BufferFactory buffer_factory(arena, BufSize); - -} // namespace - -TEST_GROUP(pump) { - Config config; - - void setup() { - config.sample_spec = - audio::SampleSpec(SampleRate, audio::Sample_RawFormat, - audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); - config.frame_length = BufDuration; - } -}; - -TEST(pump, write_read) { - enum { NumSamples = BufSize * 10 }; - - test::MockSource mock_source; - mock_source.add(NumSamples); - - core::TempFile file("test.wav"); - - { - SoxSink sox_sink(arena, config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, BufDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - - CHECK(mock_source.num_returned() >= NumSamples - BufSize); - } - - SoxSource sox_source(arena, config); - CHECK(sox_source.open(NULL, file.path())); - - test::MockSink mock_writer; - - Pump pump(buffer_factory, sox_source, NULL, mock_writer, BufDuration, SampleSpecs, - Pump::ModePermanent); - CHECK(pump.is_valid()); - CHECK(pump.run()); - - mock_writer.check(0, mock_source.num_returned()); -} - -TEST(pump, write_overwrite_read) { - enum { NumSamples = BufSize * 10 }; - - test::MockSource mock_source; - mock_source.add(NumSamples); - - core::TempFile file("test.wav"); - - { - SoxSink sox_sink(arena, config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, BufDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - mock_source.add(NumSamples); - - size_t num_returned1 = mock_source.num_returned(); - CHECK(num_returned1 >= NumSamples - BufSize); - - { - SoxSink sox_sink(arena, config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, BufDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - size_t num_returned2 = mock_source.num_returned() - num_returned1; - CHECK(num_returned1 >= NumSamples - BufSize); - - SoxSource sox_source(arena, config); - CHECK(sox_source.open(NULL, file.path())); - - test::MockSink mock_writer; - - Pump pump(buffer_factory, sox_source, NULL, mock_writer, BufDuration, SampleSpecs, - Pump::ModePermanent); - CHECK(pump.is_valid()); - CHECK(pump.run()); - - mock_writer.check(num_returned1, num_returned2); -} - -} // namespace sndio -} // namespace roc diff --git a/src/tests/roc_sndio/target_sox/test_sox_sink.cpp b/src/tests/roc_sndio/target_sox/test_sox_sink.cpp deleted file mode 100644 index db03ec349..000000000 --- a/src/tests/roc_sndio/target_sox/test_sox_sink.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2015 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 - -#include "roc_core/heap_arena.h" -#include "roc_core/temp_file.h" -#include "roc_sndio/sox_sink.h" - -namespace roc { -namespace sndio { - -namespace { - -enum { FrameSize = 500, SampleRate = 44100, ChMask = 0x3 }; - -core::HeapArena arena; - -} // namespace - -TEST_GROUP(sox_sink) { - Config sink_config; - - void setup() { - sink_config.sample_spec = - audio::SampleSpec(SampleRate, audio::Sample_RawFormat, - audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); - - sink_config.frame_length = FrameSize * core::Second - / core::nanoseconds_t(sink_config.sample_spec.sample_rate() - * sink_config.sample_spec.num_channels()); - } -}; - -TEST(sox_sink, noop) { - SoxSink sox_sink(arena, sink_config); -} - -TEST(sox_sink, error) { - SoxSink sox_sink(arena, sink_config); - - CHECK(!sox_sink.open(NULL, "/bad/file")); -} - -TEST(sox_sink, has_clock) { - SoxSink sox_sink(arena, sink_config); - - core::TempFile file("test.wav"); - CHECK(sox_sink.open(NULL, file.path())); - CHECK(!sox_sink.has_clock()); -} - -TEST(sox_sink, sample_rate_auto) { - sink_config.sample_spec.set_sample_rate(0); - SoxSink sox_sink(arena, sink_config); - - core::TempFile file("test.wav"); - CHECK(sox_sink.open(NULL, file.path())); - CHECK(sox_sink.sample_spec().sample_rate() != 0); -} - -TEST(sox_sink, sample_rate_force) { - sink_config.sample_spec.set_sample_rate(SampleRate); - SoxSink sox_sink(arena, sink_config); - - core::TempFile file("test.wav"); - CHECK(sox_sink.open(NULL, file.path())); - CHECK(sox_sink.sample_spec().sample_rate() == SampleRate); -} - -} // namespace sndio -} // namespace roc diff --git a/src/tests/roc_sndio/target_sox/test_sox_source.cpp b/src/tests/roc_sndio/target_sox/test_sox_source.cpp deleted file mode 100644 index 9248ce138..000000000 --- a/src/tests/roc_sndio/target_sox/test_sox_source.cpp +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2015 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 - -#include "test_helpers/mock_source.h" - -#include "roc_core/buffer_factory.h" -#include "roc_core/heap_arena.h" -#include "roc_core/stddefs.h" -#include "roc_core/temp_file.h" -#include "roc_sndio/pump.h" -#include "roc_sndio/sox_sink.h" -#include "roc_sndio/sox_source.h" - -namespace roc { -namespace sndio { - -namespace { - -enum { - MaxBufSize = 8192, - FrameSize = 500, - SampleRate = 44100, - ChMask = 0x3, - NumChans = 2 -}; - -const audio::SampleSpec SampleSpecs(SampleRate, - audio::Sample_RawFormat, - audio::ChanLayout_Surround, - audio::ChanOrder_Smpte, - ChMask); - -const core::nanoseconds_t FrameDuration = FrameSize * core::Second - / core::nanoseconds_t(SampleSpecs.sample_rate() * SampleSpecs.num_channels()); - -core::HeapArena arena; -core::BufferFactory buffer_factory(arena, MaxBufSize); - -} // namespace - -TEST_GROUP(sox_source) { - Config sink_config; - Config source_config; - - void setup() { - sink_config.sample_spec = - audio::SampleSpec(SampleRate, audio::Sample_RawFormat, - audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); - sink_config.frame_length = FrameDuration; - - source_config.sample_spec = - audio::SampleSpec(SampleRate, audio::Sample_RawFormat, - audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); - source_config.frame_length = FrameDuration; - } -}; - -TEST(sox_source, noop) { - SoxSource sox_source(arena, source_config); -} - -TEST(sox_source, error) { - SoxSource sox_source(arena, source_config); - - CHECK(!sox_source.open(NULL, "/bad/file")); -} - -TEST(sox_source, has_clock) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(MaxBufSize * 10); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - CHECK(!sox_source.has_clock()); -} - -TEST(sox_source, sample_rate_auto) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(MaxBufSize * 10); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - source_config.sample_spec.set_sample_rate(0); - source_config.frame_length = FrameDuration; - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - CHECK(sox_source.sample_spec().sample_rate() == SampleRate); -} - -TEST(sox_source, sample_rate_mismatch) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(MaxBufSize * 10); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - source_config.sample_spec.set_sample_rate(SampleRate * 2); - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - CHECK(sox_source.sample_spec().sample_rate() == SampleRate * 2); -} - -TEST(sox_source, pause_resume) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(FrameSize * NumChans * 2); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - - audio::sample_t frame_data1[FrameSize * NumChans] = {}; - audio::Frame frame1(frame_data1, FrameSize * NumChans); - - CHECK(sox_source.state() == DeviceState_Active); - CHECK(sox_source.read(frame1)); - - sox_source.pause(); - CHECK(sox_source.state() == DeviceState_Paused); - - audio::sample_t frame_data2[FrameSize * NumChans] = {}; - audio::Frame frame2(frame_data2, FrameSize * NumChans); - - CHECK(!sox_source.read(frame2)); - - CHECK(sox_source.resume()); - CHECK(sox_source.state() == DeviceState_Active); - - CHECK(sox_source.read(frame2)); - - if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) == 0) { - FAIL("frames should not be equal"); - } -} - -TEST(sox_source, pause_restart) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(FrameSize * NumChans * 2); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - - audio::sample_t frame_data1[FrameSize * NumChans] = {}; - audio::Frame frame1(frame_data1, FrameSize * NumChans); - - CHECK(sox_source.state() == DeviceState_Active); - CHECK(sox_source.read(frame1)); - - sox_source.pause(); - CHECK(sox_source.state() == DeviceState_Paused); - - audio::sample_t frame_data2[FrameSize * NumChans] = {}; - audio::Frame frame2(frame_data2, FrameSize * NumChans); - - CHECK(!sox_source.read(frame2)); - - CHECK(sox_source.restart()); - CHECK(sox_source.state() == DeviceState_Active); - - CHECK(sox_source.read(frame2)); - - if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) != 0) { - FAIL("frames should be equal"); - } -} - -TEST(sox_source, eof_restart) { - core::TempFile file("test.wav"); - - { - test::MockSource mock_source; - mock_source.add(FrameSize * NumChans * 2); - - SoxSink sox_sink(arena, sink_config); - CHECK(sox_sink.open(NULL, file.path())); - - Pump pump(buffer_factory, mock_source, NULL, sox_sink, FrameDuration, SampleSpecs, - Pump::ModeOneshot); - CHECK(pump.is_valid()); - CHECK(pump.run()); - } - - SoxSource sox_source(arena, source_config); - - CHECK(sox_source.open(NULL, file.path())); - - audio::sample_t frame_data[FrameSize * NumChans] = {}; - audio::Frame frame(frame_data, FrameSize * NumChans); - - for (int i = 0; i < 3; i++) { - CHECK(sox_source.read(frame)); - CHECK(sox_source.read(frame)); - CHECK(!sox_source.read(frame)); - - CHECK(sox_source.restart()); - } -} - -} // namespace sndio -} // namespace roc diff --git a/src/tests/roc_sndio/test_backend_sink.cpp b/src/tests/roc_sndio/test_backend_sink.cpp new file mode 100644 index 000000000..0ce5867a1 --- /dev/null +++ b/src/tests/roc_sndio/test_backend_sink.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2015 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 + +#include "roc_core/heap_arena.h" +#include "roc_core/scoped_ptr.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/backend_map.h" +#include "roc_sndio/pump.h" + +namespace roc { +namespace sndio { + +namespace { + +enum { FrameSize = 500, SampleRate = 44100, ChMask = 0x3 }; + +core::HeapArena arena; + +} // namespace + +TEST_GROUP(backend_sink) { + Config sink_config; + + void setup() { + sink_config.sample_spec = + audio::SampleSpec(SampleRate, audio::Sample_RawFormat, + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); + + sink_config.frame_length = FrameSize * core::Second + / core::nanoseconds_t(sink_config.sample_spec.sample_rate() + * sink_config.sample_spec.num_channels()); + } + + bool supports_wav(IBackend & backend) { + bool supports = false; + core::Array driver_list(arena); + backend.discover_drivers(driver_list); + for (size_t n = 0; n < driver_list.size(); n++) { + if (strcmp(driver_list[n].name, "wav") == 0) { + supports = true; + break; + } + } + + return supports; + } +}; + +TEST(backend_sink, write_open) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + core::TempFile file("test.wav"); + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + } +} + +TEST(backend_sink, error) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, "/bad/file", sink_config, arena); + CHECK(backend_device == NULL); + } +} + +TEST(backend_sink, has_clock) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + CHECK(!backend_sink->has_clock()); + } +} + +TEST(backend_sink, sample_rate_auto) { + sink_config.sample_spec.set_sample_rate(0); + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + CHECK(backend_sink->sample_spec().sample_rate() != 0); + } +} + +TEST(backend_sink, sample_rate_force) { + sink_config.sample_spec.set_sample_rate(SampleRate); + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + CHECK(backend_sink->sample_spec().sample_rate() == SampleRate); + } +} + +} // namespace sndio +} // namespace roc diff --git a/src/tests/roc_sndio/test_backend_source.cpp b/src/tests/roc_sndio/test_backend_source.cpp new file mode 100644 index 000000000..61601f475 --- /dev/null +++ b/src/tests/roc_sndio/test_backend_source.cpp @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2023 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 + +#include "test_helpers/mock_source.h" + +#include "roc_core/buffer_factory.h" +#include "roc_core/heap_arena.h" +#include "roc_core/scoped_ptr.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/backend_map.h" +#include "roc_sndio/pump.h" + +namespace roc { +namespace sndio { + +namespace { + +enum { + MaxBufSize = 8192, + FrameSize = 500, + SampleRate = 44100, + ChMask = 0x3, + NumChans = 2 +}; + +const audio::SampleSpec SampleSpecs(SampleRate, + audio::Sample_RawFormat, + audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, + ChMask); + +const core::nanoseconds_t FrameDuration = FrameSize * core::Second + / core::nanoseconds_t(SampleSpecs.sample_rate() * SampleSpecs.num_channels()); + +core::HeapArena arena; +core::BufferFactory buffer_factory(arena, MaxBufSize); + +bool supports_wav(IBackend& backend) { + bool supports = false; + core::Array driver_list(arena); + backend.discover_drivers(driver_list); + for (size_t n = 0; n < driver_list.size(); n++) { + if (strcmp(driver_list[n].name, "wav") == 0) { + supports = true; + break; + } + } + + return supports; +} + +} // namespace + +TEST_GROUP(backend_source) { + Config sink_config; + Config source_config; + + void setup() { + sink_config.sample_spec = + audio::SampleSpec(SampleRate, audio::Sample_RawFormat, + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); + sink_config.frame_length = FrameDuration; + + source_config.sample_spec = audio::SampleSpec(); + source_config.frame_length = FrameDuration; + } +}; + +TEST(backend_source, read_open) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + { + test::MockSource mock_source; + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, NULL, file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + } +} + +TEST(backend_source, error) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + if (!supports_wav(backend)) { + continue; + } + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, NULL, "/bad/file", source_config, arena); + CHECK(backend_device == NULL); + } +} + +TEST(backend_source, has_clock) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, NULL, file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, NULL, file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + CHECK(!backend_source->has_clock()); + } +} + +TEST(backend_source, sample_rate_auto) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + source_config.sample_spec.set_sample_rate(0); + source_config.frame_length = FrameDuration; + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + + CHECK(backend_source->sample_spec().sample_rate() == SampleRate); + } +} + +TEST(backend_source, sample_rate_mismatch) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + source_config.sample_spec.set_sample_rate(SampleRate * 2); + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + + CHECK(backend_device == NULL); + } +} + +TEST(backend_source, pause_resume) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + + audio::sample_t frame_data1[FrameSize * NumChans] = {}; + audio::Frame frame1(frame_data1, FrameSize * NumChans); + + // TODO(gh-706): check state + + CHECK(backend_source->read(frame1)); + + audio::sample_t frame_data2[FrameSize * NumChans] = {}; + audio::Frame frame2(frame_data2, FrameSize * NumChans); + + backend_source->pause(); + if (strcmp(backend.name(), "SoX") == 0) { + // TODO(gh-706): check state + + CHECK(!backend_source->read(frame2)); + + CHECK(backend_source->resume()); + // TODO(gh-706): check state + + CHECK(backend_source->read(frame2)); + } else { + // TODO(gh-706): check state + + CHECK(backend_source->read(frame2)); + + CHECK(backend_source->resume()); + // TODO(gh-706): check state + + CHECK(!backend_source->read(frame2)); + } + + if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) == 0) { + FAIL("frames should not be equal"); + } + } +} + +TEST(backend_source, pause_restart) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + + audio::sample_t frame_data1[FrameSize * NumChans] = {}; + audio::Frame frame1(frame_data1, FrameSize * NumChans); + + // TODO(gh-706): check state + + CHECK(backend_source->read(frame1)); + + backend_source->pause(); + + audio::sample_t frame_data2[FrameSize * NumChans] = {}; + audio::Frame frame2(frame_data2, FrameSize * NumChans); + + if (strcmp(backend.name(), "SoX") == 0) { + // TODO(gh-706): check state + + CHECK(!backend_source->read(frame2)); + + CHECK(backend_source->restart()); + // TODO(gh-706): check state + + CHECK(backend_source->read(frame2)); + } else { + // TODO(gh-706): check state + + CHECK(backend_source->read(frame2)); + + CHECK(backend_source->restart()); + // TODO(gh-706): check state + + CHECK(backend_source->read(frame2)); + } + + if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) != 0) { + FAIL("frames should be equal"); + } + } +} + +TEST(backend_source, eof_restart) { + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + + audio::sample_t frame_data[FrameSize * NumChans] = {}; + audio::Frame frame(frame_data, FrameSize * NumChans); + + for (int i = 0; i < 3; i++) { + CHECK(backend_source->read(frame)); + CHECK(backend_source->read(frame)); + CHECK(!backend_source->read(frame)); + + CHECK(backend_source->restart()); + } + } +} + +} // namespace sndio + +} // namespace roc diff --git a/src/tests/roc_sndio/target_sox/test_helpers/mock_sink.h b/src/tests/roc_sndio/test_helpers/mock_sink.h similarity index 92% rename from src/tests/roc_sndio/target_sox/test_helpers/mock_sink.h rename to src/tests/roc_sndio/test_helpers/mock_sink.h index ad95ef38f..ac625c1c3 100644 --- a/src/tests/roc_sndio/target_sox/test_helpers/mock_sink.h +++ b/src/tests/roc_sndio/test_helpers/mock_sink.h @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#ifndef ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SINK_H_ -#define ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SINK_H_ +#ifndef ROC_SNDIO_TEST_HELPERS_MOCK_SINK_H_ +#define ROC_SNDIO_TEST_HELPERS_MOCK_SINK_H_ #include @@ -100,4 +100,4 @@ class MockSink : public ISink { } // namespace sndio } // namespace roc -#endif // ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SINK_H_ +#endif // ROC_SNDIO_TEST_HELPERS_MOCK_SINK_H_ diff --git a/src/tests/roc_sndio/target_sox/test_helpers/mock_source.h b/src/tests/roc_sndio/test_helpers/mock_source.h similarity index 93% rename from src/tests/roc_sndio/target_sox/test_helpers/mock_source.h rename to src/tests/roc_sndio/test_helpers/mock_source.h index 2b5aa27b9..88a1a9393 100644 --- a/src/tests/roc_sndio/target_sox/test_helpers/mock_source.h +++ b/src/tests/roc_sndio/test_helpers/mock_source.h @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#ifndef ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SOURCE_H_ -#define ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SOURCE_H_ +#ifndef ROC_SNDIO_TEST_HELPERS_MOCK_SOURCE_H_ +#define ROC_SNDIO_TEST_HELPERS_MOCK_SOURCE_H_ #include @@ -127,4 +127,4 @@ class MockSource : public ISource { } // namespace sndio } // namespace roc -#endif // ROC_SNDIO_TARGET_SOX_TEST_HELPERS_MOCK_SOURCE_H_ +#endif // ROC_SNDIO_TEST_HELPERS_MOCK_SOURCE_H_ diff --git a/src/tests/roc_sndio/test_pump.cpp b/src/tests/roc_sndio/test_pump.cpp new file mode 100644 index 000000000..47a44d3b2 --- /dev/null +++ b/src/tests/roc_sndio/test_pump.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2023 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 + +#include "test_helpers/mock_sink.h" +#include "test_helpers/mock_source.h" + +#include "roc_core/buffer_factory.h" +#include "roc_core/heap_arena.h" +#include "roc_core/scoped_ptr.h" +#include "roc_core/stddefs.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/backend_map.h" +#include "roc_sndio/config.h" +#include "roc_sndio/pump.h" +#ifdef ROC_TARGET_SNDFILE +#include "roc_sndio/sndfile_sink.h" +#include "roc_sndio/sndfile_source.h" +#endif // ROC_TARGET_SNDFILE +#ifdef ROC_TARGET_SOX +#include "roc_sndio/sox_sink.h" +#include "roc_sndio/sox_source.h" +#endif // ROC_TARGET_SOX + +namespace roc { +namespace sndio { + +namespace { + +enum { BufSize = 512, SampleRate = 44100, ChMask = 0x3 }; + +const audio::SampleSpec SampleSpecs(SampleRate, + audio::Sample_RawFormat, + audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, + ChMask); + +const core::nanoseconds_t BufDuration = BufSize * core::Second + / core::nanoseconds_t(SampleSpecs.sample_rate() * SampleSpecs.num_channels()); + +core::HeapArena arena; +core::BufferFactory buffer_factory(arena, BufSize); + +bool supports_wav(IBackend& backend) { + bool supports = false; + core::Array driver_list(arena); + backend.discover_drivers(driver_list); + for (size_t n = 0; n < driver_list.size(); n++) { + if (strcmp(driver_list[n].name, "wav") == 0) { + supports = true; + break; + } + } + return supports; +} + +} // namespace + +TEST_GROUP(pump) { + Config source_config; + Config sink_config; + + void setup() { + source_config.sample_spec = audio::SampleSpec(); + + source_config.frame_length = BufDuration; + + sink_config.sample_spec = + audio::SampleSpec(SampleRate, audio::Sample_RawFormat, + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); + sink_config.frame_length = BufDuration; + } +}; + +TEST(pump, write_read) { + enum { NumSamples = BufSize * 10 }; + + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + test::MockSource mock_source; + mock_source.add(NumSamples); + core::TempFile file("test.wav"); + + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, BufDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + CHECK(mock_source.num_returned() >= NumSamples - BufSize); + } + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + test::MockSink mock_writer; + + Pump pump(buffer_factory, *backend_source, NULL, mock_writer, BufDuration, + SampleSpecs, Pump::ModePermanent); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + mock_writer.check(0, mock_source.num_returned()); + } +} + +TEST(pump, write_overwrite_read) { + enum { NumSamples = BufSize * 10 }; + + for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends(); + n_backend++) { + test::MockSource mock_source; + mock_source.add(NumSamples); + + core::TempFile file("test.wav"); + IBackend& backend = BackendMap::instance().nth_backend(n_backend); + + if (!supports_wav(backend)) { + continue; + } + + { + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, BufDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + mock_source.add(NumSamples); + + size_t num_returned1 = mock_source.num_returned(); + CHECK(num_returned1 >= NumSamples - BufSize); + + { + IDevice* backend_device = backend.open_device( + DeviceType_Sink, DriverType_File, "wav", file.path(), sink_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_sink(backend_device->to_sink(), arena); + CHECK(backend_sink != NULL); + Pump pump(buffer_factory, mock_source, NULL, *backend_sink, BufDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + size_t num_returned2 = mock_source.num_returned() - num_returned1; + CHECK(num_returned1 >= NumSamples - BufSize); + + IDevice* backend_device = backend.open_device( + DeviceType_Source, DriverType_File, "wav", file.path(), source_config, arena); + CHECK(backend_device != NULL); + core::ScopedPtr backend_source(backend_device->to_source(), arena); + CHECK(backend_source != NULL); + + test::MockSink mock_writer; + + Pump pump(buffer_factory, *backend_source, NULL, mock_writer, BufDuration, + SampleSpecs, Pump::ModePermanent); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + mock_writer.check(num_returned1, num_returned2); + } +} +} // namespace sndio +} // namespace roc