From 9a64a0e53fc001fd33edd7597c2d1ab0b65c2885 Mon Sep 17 00:00:00 2001 From: Hunter Rick Date: Fri, 15 Dec 2023 21:17:32 -0800 Subject: [PATCH] Libsndfile Sink and Source --- 3rdparty/SConscript | 44 ++ SConstruct | 9 + scripts/android_emu/run.sh | 2 +- scripts/ci_checks/android/linux.sh | 2 +- .../linux-arm/aarch64-linux-gnu-gcc-7.4.sh | 2 +- ...arm-bcm2708hardfp-linux-gnueabi-gcc-4.7.sh | 2 +- .../linux-arm/arm-linux-gnueabihf-gcc-4.9.sh | 2 +- .../linux-checks/conditional-build.sh | 4 +- .../linux-checks/pulseaudio-versions.sh | 2 +- scripts/ci_checks/linux-x86_64/alpine.sh | 2 +- scripts/ci_checks/linux-x86_64/archlinux.sh | 2 +- scripts/ci_checks/linux-x86_64/debian.sh | 2 +- scripts/ci_checks/linux-x86_64/fedora.sh | 2 +- scripts/ci_checks/linux-x86_64/opensuse.sh | 2 +- .../ci_checks/linux-x86_64/ubuntu-14.04.sh | 2 +- .../ci_checks/linux-x86_64/ubuntu-16.04.sh | 2 +- .../ci_checks/linux-x86_64/ubuntu-18.04.sh | 2 +- .../ci_checks/linux-x86_64/ubuntu-20.04.sh | 2 +- .../ci_checks/linux-x86_64/ubuntu-22.04.sh | 2 +- scripts/ci_checks/macos/standard-build.sh | 6 +- src/internal_modules/roc_audio/frame.h | 2 +- .../roc_sndio/backend_map.cpp | 4 + src/internal_modules/roc_sndio/backend_map.h | 8 + src/internal_modules/roc_sndio/isource.h | 2 +- .../roc_sndio/sndfile_backend.cpp | 136 ++++++ .../roc_sndio/sndfile_backend.h | 47 ++ .../target_sndfile/roc_sndio/sndfile_sink.cpp | 360 ++++++++++++++ .../target_sndfile/roc_sndio/sndfile_sink.h | 103 ++++ .../roc_sndio/sndfile_source.cpp | 456 ++++++++++++++++++ .../target_sndfile/roc_sndio/sndfile_source.h | 121 +++++ .../target_sndfile/test_sndfile_sink.cpp | 76 +++ .../target_sndfile/test_sndfile_source.cpp | 260 ++++++++++ src/tests/roc_sndio/target_sox/test_pump.cpp | 133 ----- .../{target_sox => }/test_helpers/mock_sink.h | 6 +- .../test_helpers/mock_source.h | 6 +- src/tests/roc_sndio/test_pump.cpp | 238 +++++++++ 36 files changed, 1892 insertions(+), 161 deletions(-) create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h create mode 100644 src/tests/roc_sndio/target_sndfile/test_sndfile_sink.cpp create mode 100644 src/tests/roc_sndio/target_sndfile/test_sndfile_source.cpp delete mode 100644 src/tests/roc_sndio/target_sox/test_pump.cpp rename src/tests/roc_sndio/{target_sox => }/test_helpers/mock_sink.h (92%) rename src/tests/roc_sndio/{target_sox => }/test_helpers/mock_source.h (93%) create mode 100644 src/tests/roc_sndio/test_pump.cpp diff --git a/3rdparty/SConscript b/3rdparty/SConscript index fb7974b03..96a5dc1d1 100644 --- a/3rdparty/SConscript +++ b/3rdparty/SConscript @@ -319,6 +319,50 @@ elif 'sox' in system_dependencies: env = conf.Finish() +# dep: sndfile +if 'sndfile' in autobuild_dependencies: + sndfile_deps = [] + + if 'alsa' in autobuild_dependencies: + sndfile_deps += ['alsa'] + + if 'pulseaudio' in autobuild_dependencies: + sndfile_deps += ['pulseaudio'] + + env.BuildThirdParty(thirdparty_versions, 'sndfile', deps=sndfile_deps) + + conf = Configure(env, custom_tests=env.CustomTests) + + if not 'alsa' in autobuild_dependencies: + for lib in [ + 'asound', + ]: + conf.CheckLib(lib) + + if meta.platform in ['darwin']: + env.Append(LINKFLAGS=[ + '-Wl,-framework,CoreAudio' + ]) + + 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', + expr='SNDFILE_LIB_VERSION_CODE >= SNDFILE_LIB_VERSION(1, 0, 28'): + env.Die("libsndfile >= 1.0.28 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: ragel if 'ragel' in autobuild_dependencies: env.BuildThirdParty(thirdparty_versions, 'ragel', is_native=True) diff --git a/SConstruct b/SConstruct index b699a5c71..e0ae518ed 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', @@ -821,6 +826,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/scripts/android_emu/run.sh b/scripts/android_emu/run.sh index 6998f3a46..bb5082d5a 100755 --- a/scripts/android_emu/run.sh +++ b/scripts/android_emu/run.sh @@ -73,7 +73,7 @@ then --enable-tests \ --disable-soversion \ --disable-tools \ - --build-3rdparty=libuv,openfec,openssl,speexdsp,cpputest + --build-3rdparty=libuv,openfec,openssl,speexdsp,cpputest,sndfile fi if [[ "${action}" == prep ]] diff --git a/scripts/ci_checks/android/linux.sh b/scripts/ci_checks/android/linux.sh index cd0cb6726..0d880d2b5 100755 --- a/scripts/ci_checks/android/linux.sh +++ b/scripts/ci_checks/android/linux.sh @@ -7,6 +7,6 @@ scons -Q \ --disable-soversion \ --disable-tools \ --enable-tests \ - --build-3rdparty=libuv,openfec,speexdsp,openssl,cpputest \ + --build-3rdparty=libuv,openfec,speexdsp,openssl,cpputest,sndfile \ --compiler=clang \ --host="$1" 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 e57d4e297..bbf77882b 100755 --- a/scripts/ci_checks/linux-checks/conditional-build.sh +++ b/scripts/ci_checks/linux-checks/conditional-build.sh @@ -11,7 +11,8 @@ scons -Q --enable-werror --build-3rdparty=all \ --disable-openfec \ --disable-speex \ --disable-sox \ - --disable-pulseaudio + --disable-pulseaudio \ + --disable-sndfile \ # optional dependencies: none, optional targets: all scons -Q --enable-werror --build-3rdparty=all \ @@ -24,6 +25,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/linux-x86_64/alpine.sh b/scripts/ci_checks/linux-x86_64/alpine.sh index e34f99c02..817f85d96 100755 --- a/scripts/ci_checks/linux-x86_64/alpine.sh +++ b/scripts/ci_checks/linux-x86_64/alpine.sh @@ -8,5 +8,5 @@ scons -Q \ --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=openfec,cpputest,google-benchmark \ + --build-3rdparty=openfec,cpputest,google-benchmark,sndfile \ test diff --git a/scripts/ci_checks/linux-x86_64/archlinux.sh b/scripts/ci_checks/linux-x86_64/archlinux.sh index ea88bcf2f..9a434c451 100755 --- a/scripts/ci_checks/linux-x86_64/archlinux.sh +++ b/scripts/ci_checks/linux-x86_64/archlinux.sh @@ -8,5 +8,5 @@ scons -Q \ --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=openfec,cpputest,google-benchmark:1.5.5 \ + --build-3rdparty=openfec,cpputest,google-benchmark:1.5.5,sndfile \ test diff --git a/scripts/ci_checks/linux-x86_64/debian.sh b/scripts/ci_checks/linux-x86_64/debian.sh index cda51f1f6..98ff64871 100755 --- a/scripts/ci_checks/linux-x86_64/debian.sh +++ b/scripts/ci_checks/linux-x86_64/debian.sh @@ -8,5 +8,5 @@ scons -Q \ --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=libuv,openfec,cpputest \ + --build-3rdparty=libuv,openfec,cpputest,sndfile \ test diff --git a/scripts/ci_checks/linux-x86_64/fedora.sh b/scripts/ci_checks/linux-x86_64/fedora.sh index aefdd7c6e..bc4e6bb70 100755 --- a/scripts/ci_checks/linux-x86_64/fedora.sh +++ b/scripts/ci_checks/linux-x86_64/fedora.sh @@ -8,5 +8,5 @@ scons -Q \ --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=openfec,cpputest \ + --build-3rdparty=openfec,cpputest,sndfile \ test diff --git a/scripts/ci_checks/linux-x86_64/opensuse.sh b/scripts/ci_checks/linux-x86_64/opensuse.sh index aefdd7c6e..bc4e6bb70 100755 --- a/scripts/ci_checks/linux-x86_64/opensuse.sh +++ b/scripts/ci_checks/linux-x86_64/opensuse.sh @@ -8,5 +8,5 @@ scons -Q \ --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=openfec,cpputest \ + --build-3rdparty=openfec,cpputest,sndfile \ test diff --git a/scripts/ci_checks/linux-x86_64/ubuntu-14.04.sh b/scripts/ci_checks/linux-x86_64/ubuntu-14.04.sh index 7a7276bb4..b6b2fe3ed 100755 --- a/scripts/ci_checks/linux-x86_64/ubuntu-14.04.sh +++ b/scripts/ci_checks/linux-x86_64/ubuntu-14.04.sh @@ -9,7 +9,7 @@ do --enable-tests \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=libuv,libatomic_ops,openfec,openssl,pulseaudio,cpputest \ + --build-3rdparty=libuv,libatomic_ops,openfec,openssl,pulseaudio,cpputest,sndfile \ --compiler=${comp} \ test done diff --git a/scripts/ci_checks/linux-x86_64/ubuntu-16.04.sh b/scripts/ci_checks/linux-x86_64/ubuntu-16.04.sh index 9a00deb30..75dd5c15a 100755 --- a/scripts/ci_checks/linux-x86_64/ubuntu-16.04.sh +++ b/scripts/ci_checks/linux-x86_64/ubuntu-16.04.sh @@ -10,7 +10,7 @@ do --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=libatomic_ops,openfec,openssl,cpputest,google-benchmark \ + --build-3rdparty=libatomic_ops,openfec,openssl,cpputest,google-benchmark,sndfile \ --compiler=${comp} \ test done diff --git a/scripts/ci_checks/linux-x86_64/ubuntu-18.04.sh b/scripts/ci_checks/linux-x86_64/ubuntu-18.04.sh index 081673c30..a1d0e3e38 100755 --- a/scripts/ci_checks/linux-x86_64/ubuntu-18.04.sh +++ b/scripts/ci_checks/linux-x86_64/ubuntu-18.04.sh @@ -10,7 +10,7 @@ do --enable-benchmarks \ --enable-examples \ --enable-doxygen \ - --build-3rdparty=openfec,google-benchmark \ + --build-3rdparty=openfec,google-benchmark,sndfile \ --compiler=${comp} \ test done diff --git a/scripts/ci_checks/linux-x86_64/ubuntu-20.04.sh b/scripts/ci_checks/linux-x86_64/ubuntu-20.04.sh index b585fbf9b..15146816e 100755 --- a/scripts/ci_checks/linux-x86_64/ubuntu-20.04.sh +++ b/scripts/ci_checks/linux-x86_64/ubuntu-20.04.sh @@ -9,7 +9,7 @@ do --enable-tests \ --enable-benchmarks \ --enable-examples \ - --build-3rdparty=openfec \ + --build-3rdparty=openfec,sndfile \ --compiler=${comp} \ test done diff --git a/scripts/ci_checks/linux-x86_64/ubuntu-22.04.sh b/scripts/ci_checks/linux-x86_64/ubuntu-22.04.sh index 2715a2819..cf3faa195 100755 --- a/scripts/ci_checks/linux-x86_64/ubuntu-22.04.sh +++ b/scripts/ci_checks/linux-x86_64/ubuntu-22.04.sh @@ -9,7 +9,7 @@ do --enable-tests \ --enable-benchmarks \ --enable-examples \ - --build-3rdparty=openfec \ + --build-3rdparty=openfec,sndfile \ --compiler=${comp} \ test done diff --git a/scripts/ci_checks/macos/standard-build.sh b/scripts/ci_checks/macos/standard-build.sh index 5660481d9..9d82281e0 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 \ automake scons ragel gengetopt \ libuv speexdsp sox openssl@3 \ - cpputest google-benchmark + cpputest google-benchmark libsndfile # debug build scons -Q \ @@ -15,7 +15,7 @@ scons -Q \ --enable-examples \ --enable-debug \ --sanitizers=all \ - --build-3rdparty=openfec \ + --build-3rdparty=openfec,sndfile \ test # release build @@ -24,5 +24,5 @@ scons -Q \ --enable-tests \ --enable-benchmarks \ --enable-examples \ - --build-3rdparty=openfec \ + --build-3rdparty=openfec,sndfile \ test diff --git a/src/internal_modules/roc_audio/frame.h b/src/internal_modules/roc_audio/frame.h index d0b44987e..e0f852294 100644 --- a/src/internal_modules/roc_audio/frame.h +++ b/src/internal_modules/roc_audio/frame.h @@ -40,7 +40,7 @@ class Frame : public core::NonCopyable<> { FlagIncomplete = (1 << 1), //! 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. FlagDrops = (1 << 2) }; diff --git a/src/internal_modules/roc_sndio/backend_map.cpp b/src/internal_modules/roc_sndio/backend_map.cpp index 5b85550e3..a1d148145 100644 --- a/src/internal_modules/roc_sndio/backend_map.cpp +++ b/src/internal_modules/roc_sndio/backend_map.cpp @@ -50,6 +50,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 019cf6ccd..0d5534d8e 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 @@ -67,6 +71,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_SOX + #ifdef ROC_TARGET_SOX core::Optional sox_backend_; #endif // ROC_TARGET_SOX diff --git a/src/internal_modules/roc_sndio/isource.h b/src/internal_modules/roc_sndio/isource.h index 41acdead4..d1526a4d7 100644 --- a/src/internal_modules/roc_sndio/isource.h +++ b/src/internal_modules/roc_sndio/isource.h @@ -26,7 +26,7 @@ class ISource : 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/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..71dba1c69 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp @@ -0,0 +1,136 @@ +/* + * 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 "roc_core/log.h" +#include "roc_core/macro_helpers.h" +#include "roc_core/scoped_lock.h" +#include "roc_core/scoped_ptr.h" +#include "roc_sndio/sndfile_backend.h" +#include "roc_sndio/sndfile_sink.h" +#include "roc_sndio/sndfile_source.h" + +namespace roc { +namespace sndio { + +SndfileBackend::SndfileBackend() + : first_created_(false) { + roc_log(LogDebug, "sndfile backend: initializing"); +} + +void SndfileBackend::discover_drivers(core::Array& driver_list) { + 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: %s", sf_error_number(errnum)); + } + + SF_FORMAT_INFO format_info; + + int wav_count = 0; + int mat_count = 0; + + for (int n = 0; n < total_number_of_drivers; n++) { + format_info.format = n; + if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR, &format_info, + sizeof(format_info))) { + roc_panic("sndfile backend: %s", sf_error_number(errnum)); + } + + const char* driver = format_info.extension; + + if (strcmp(driver, "wav") == 0) { + if (wav_count == 1) { + driver = "nist"; + } else if (wav_count == 2) { + driver = "wavex"; + } + wav_count++; + } + + if (strcmp(driver, "mat") == 0) { + if (mat_count == 0) { + driver = "mat4"; + } else if (mat_count == 1) { + driver = "mat5"; + } + mat_count++; + } + + if (!driver_list.push_back(DriverInfo(driver, DriverType_File, + DriverFlag_IsDefault + | DriverFlag_SupportsSource + | DriverFlag_SupportsSink, + this))) { + roc_panic("sndfile backend: can't 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; + } + + first_created_ = true; + + 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"); +} + +} // 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..e2a2ea270 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h @@ -0,0 +1,47 @@ +/* + * 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); + +private: + bool first_created_; +}; + +} // namespace sndio +} // namespace roc + +#endif // ROC_SNDIO_SNDFILE_BACKEND_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..f928f23ac --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp @@ -0,0 +1,360 @@ +/* + * 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_sink.h" +#include "roc_core/log.h" +#include "roc_core/panic.h" +#include "roc_sndio/backend_map.h" + +namespace roc { +namespace sndio { +namespace { + +struct FileMap { + const char* format_cstring; + int format_enum; +} file_type_map[] = { + { "aiff", SF_FORMAT_AIFF }, { "au", SF_FORMAT_AU }, { "avr", SF_FORMAT_AVR }, + { "caf", SF_FORMAT_CAF }, { "htk", SF_FORMAT_HTK }, { "iff", SF_FORMAT_SVX }, + { "mat", SF_FORMAT_MAT4 }, { "mat4", SF_FORMAT_MAT4 }, { "mat5", SF_FORMAT_MAT5 }, + { "mpc", SF_FORMAT_MPC2K }, { "paf", SF_FORMAT_PAF }, { "pvf", SF_FORMAT_PVF }, + { "raw", SF_FORMAT_RAW }, { "rf64", SF_FORMAT_RF64 }, { "sd2", SF_FORMAT_SD2 }, + { "sds", SF_FORMAT_SDS }, { "sf", SF_FORMAT_IRCAM }, { "voc", SF_FORMAT_VOC }, + { "w64", SF_FORMAT_W64 }, { "wav", SF_FORMAT_WAV }, { "nist", SF_FORMAT_NIST }, + { "wavex", SF_FORMAT_WAVEX }, { "wve", SF_FORMAT_WVE }, { "xi", SF_FORMAT_XI }, +}; + +bool detect_file_extension(const char** driver, const char* path) { + const char* dot = strrchr(path, '.'); + + if (!dot || dot == path) { + return false; + } + + const char* file_extension = dot + 1; + bool found_extension = false; + + for (size_t sndfile_extension = 0; sndfile_extension < ROC_ARRAY_SIZE(file_type_map); + sndfile_extension++) { + if (strcmp(file_type_map[sndfile_extension].format_cstring, file_extension) + == 0) { + found_extension = true; + } + } + + if (!found_extension) { + return false; + } + + roc_log(LogDebug, "detected file format type `%s'", file_extension); + *driver = file_extension; + return true; +} + +bool map_to_sndfile(SF_INFO& sfinfo, const char* driver, int& bits) { + int format = 0; + for (size_t format_struct_index = 0; + format_struct_index < sizeof(file_type_map) / sizeof(file_type_map[0]); + format_struct_index++) { + if (strcmp(file_type_map[format_struct_index].format_cstring, driver) == 0) { + format = file_type_map[format_struct_index].format_enum; + break; + } + } + + if (format == 0) { + return false; + } + + sfinfo.format = format | sfinfo.format; + + if (sf_format_check(&sfinfo)) { + bits = 32; + return true; + } + + int temp_format = 0; + const int format_count = 2; + + for (int format_attempt = 0; format_attempt < format_count; format_attempt++) { + if (format == SF_FORMAT_XI) { + sfinfo.channels = 1; + if (format_attempt == 0) { + temp_format = format | SF_FORMAT_DPCM_16; + bits = 16; + } else { + temp_format = format | SF_FORMAT_DPCM_8; + bits = 8; + } + } else { + if (format_attempt == 0) { + temp_format = format | SF_FORMAT_PCM_24; + bits = 24; + } else { + temp_format = format | SF_FORMAT_PCM_16; + bits = 16; + } + } + + sfinfo.format = temp_format; + + if (sf_format_check(&sfinfo)) { + return true; + } + } + + return false; +} + +} // namespace + +SndfileSink::SndfileSink(core::IArena& arena, const Config& config) + : sndfile_output_(NULL) + , buffer_(arena) + , buffer_size_(0) + , is_file_(true) + , valid_(false) { + BackendMap::instance(); + + 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; + sample_spec_ = config.sample_spec; + + if (frame_length_ == 0) { + roc_log(LogError, "sndfile sink: frame length is zero"); + return; + } + + memset(&sf_info_out_, 0, sizeof(sf_info_out_)); + + sf_info_out_.format = SF_FORMAT_PCM_32; + sf_info_out_.channels = (int)config.sample_spec.num_channels(); + sf_info_out_.samplerate = (int)config.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 (buffer_.size() != 0 || sndfile_output_) { + roc_panic("sndfile sink: can't call open() more than once"); + } + + if (!open_(driver, path)) { + return false; + } + + if (!setup_buffer_()) { + return false; + } + + return true; +} + +DeviceType SndfileSink::type() const { + return DeviceType_Sink; +} + +DeviceState SndfileSink::state() const { + return DeviceState_Active; +} + +void SndfileSink::pause() { + // no-op +} + +bool SndfileSink::resume() { + return true; +} + +bool SndfileSink::restart() { + return true; +} + +audio::SampleSpec SndfileSink::sample_spec() const { + roc_panic_if(!valid_); + + if (!sndfile_output_) { + roc_panic("sndfile sink: sample_rate(): non-open output file or device"); + } + + if (sf_info_out_.channels == 1) { + return audio::SampleSpec(size_t(sf_info_out_.samplerate), + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, + audio::ChanMask_Surround_Mono); + } + + if (sf_info_out_.channels == 2) { + return audio::SampleSpec(size_t(sf_info_out_.samplerate), + 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 (!sndfile_output_) { + roc_panic("sndfile sink: latency(): non-open output file or device"); + } + + return 0; +} + +bool SndfileSink::has_latency() const { + roc_panic_if(!valid_); + + if (!sndfile_output_) { + roc_panic("sndfile sink: has_latency(): non-open output file or device"); + } + + return false; +} + +bool SndfileSink::has_clock() const { + roc_panic_if(!valid_); + + if (!sndfile_output_) { + roc_panic("sndfile sink: has_clock(): non-open output file or device"); + } + + return !is_file_; +} + +void SndfileSink::write(audio::Frame& frame) { + roc_panic_if(!valid_); + const audio::sample_t* frame_data = frame.samples(); + size_t frame_size = frame.num_samples(); + audio::sample_t* buffer_data = buffer_.data(); + size_t buffer_pos = 0; + + while (frame_size > 0) { + for (; buffer_pos < buffer_size_ && frame_size > 0; buffer_pos++) { + buffer_data[buffer_pos] = *frame_data; + frame_data++; + frame_size--; + } + + if (buffer_pos > 0) { + if (sf_write_float(sndfile_output_, buffer_data, (sf_count_t)buffer_size_) + == 0 + && frame_size > 0) { + roc_panic( + "sndfile sink: Unable to write entire frame to file. Reached eof"); + } + } + buffer_pos = 0; + } +} + +bool SndfileSink::setup_buffer_() { + buffer_size_ = sample_spec_.ns_2_samples_overall(frame_length_); + if (buffer_size_ == 0) { + roc_log(LogError, "sndfile sink: buffer size is zero"); + return false; + } + if (!buffer_.resize(buffer_size_)) { + roc_log(LogError, "sndfile sink: can't allocate sample buffer"); + return false; + } + + return true; +} + +bool SndfileSink::open_(const char* driver, const char* path) { + unsigned long in_rate = (unsigned long)sf_info_out_.samplerate; + + if (sf_info_out_.samplerate == 0) { + sf_info_out_.samplerate = 48000; + } + + unsigned long out_rate = (unsigned long)sf_info_out_.samplerate; + + if (!driver) { + if (!detect_file_extension(&driver, path)) { + roc_log(LogDebug, "sndfile sink: Driver extension could not be detected"); + return false; + } + } + + int bits = 0; + if (!map_to_sndfile(sf_info_out_, driver, bits)) { + roc_log(LogDebug, + "sndfile sink: Cannot find valid subtype format for major format type"); + return false; + } + + sndfile_output_ = sf_open(path, SFM_WRITE, &sf_info_out_); + if (!sndfile_output_) { + roc_log(LogDebug, "sndfile sink: can't open: driver=%s path=%s", driver, path); + return false; + } + + sf_command(sndfile_output_, SFC_SET_UPDATE_HEADER_AUTO, NULL, SF_TRUE); + + if (in_rate != 0 && in_rate != out_rate) { + roc_log(LogError, + "sndfile sink:" + " can't open output file or device with the required sample rate:" + " required_by_output=%lu requested_by_user=%lu", + out_rate, in_rate); + return false; + } + sample_spec_.set_sample_rate((unsigned long)sf_info_out_.samplerate); + + roc_log(LogInfo, + "sndfile sink:" + " opened: bits=%lu out_rate=%lu in_rate=%lu ch=%lu is_file=%d", + (unsigned long)bits, out_rate, in_rate, (unsigned long)sf_info_out_.channels, + (int)is_file_); + + sf_seek(sndfile_output_, 0, SEEK_SET); + + return true; +} + +void SndfileSink::close_() { + if (!sndfile_output_) { + return; + } + + roc_log(LogDebug, "sndfile sink: closing output"); + + int err = sf_close(sndfile_output_); + if (err != 0) { + roc_panic("sndfile sink: can't close output: %s", sf_error_number(err)); + } + + sndfile_output_ = 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..4d587d625 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h @@ -0,0 +1,103 @@ +/* + * 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); + + //! 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 setup_buffer_(); + bool open_(const char* driver, const char* path); + void close_(); + + SNDFILE* sndfile_output_; + SF_INFO sf_info_out_; + + core::Array buffer_; + size_t buffer_size_; + core::nanoseconds_t frame_length_; + audio::SampleSpec sample_spec_; + + bool is_file_; + 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..999cb6076 --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp @@ -0,0 +1,456 @@ +/* + * 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 { +namespace { +struct BitMap { + int sub_type; + int bit_depth; +} bit_map[] = { + { SF_FORMAT_PCM_S8, 8 }, { SF_FORMAT_PCM_16, 16 }, { SF_FORMAT_PCM_24, 24 }, + { SF_FORMAT_PCM_S8, 32 }, { SF_FORMAT_PCM_U8, 8 }, { SF_FORMAT_FLOAT, 32 }, + { SF_FORMAT_DOUBLE, 64 }, { SF_FORMAT_ULAW, 8 }, { SF_FORMAT_ALAW, 8 }, + { SF_FORMAT_IMA_ADPCM, 4 }, { SF_FORMAT_MS_ADPCM, 4 }, { SF_FORMAT_GSM610, 13 }, + { SF_FORMAT_VOX_ADPCM, 4 }, { SF_FORMAT_G721_32, 4 }, { SF_FORMAT_G723_24, 4 }, + { SF_FORMAT_G723_40, 4 }, { SF_FORMAT_DWVW_12, 12 }, { SF_FORMAT_DWVW_16, 16 }, + { SF_FORMAT_DWVW_24, 24 }, { SF_FORMAT_DPCM_8, 8 }, { SF_FORMAT_DPCM_16, 16 }, + { SF_FORMAT_VORBIS, 32 }, { SF_FORMAT_DPCM_8, 8 }, { SF_FORMAT_DPCM_16, 16 }, +}; + +bool find_subtype(int& format_subtype, SNDFILE* sndfile_input_, SF_INFO sf_info_in_) { + SF_FORMAT_INFO format_info_major, format_info_subtype; + int major_count, subtype_count; + + sf_command(sndfile_input_, SFC_GET_FORMAT_MAJOR_COUNT, &major_count, sizeof(int)); + sf_command(sndfile_input_, SFC_GET_FORMAT_SUBTYPE_COUNT, &subtype_count, sizeof(int)); + + for (int maj_index = 0; maj_index < major_count; maj_index++) { + format_info_major.format = maj_index; + sf_command(sndfile_input_, SFC_GET_FORMAT_MAJOR, &format_info_major, + sizeof(format_info_major)); + + for (int subtype_index = 0; subtype_index < subtype_count; subtype_index++) { + format_info_subtype.format = subtype_index; + sf_command(sndfile_input_, SFC_GET_FORMAT_SUBTYPE, &format_info_subtype, + sizeof(format_info_subtype)); + + if ((format_info_major.format | format_info_subtype.format) + == sf_info_in_.format) { + format_subtype = format_info_subtype.format; + return true; + } + } + } + return false; +} + +bool map_to_bit_depth(int& in_bits, int format_subtype) { + for (size_t bit_map_index = 0; bit_map_index < ROC_ARRAY_SIZE(bit_map); + bit_map_index++) { + if (bit_map[bit_map_index].sub_type == format_subtype) { + in_bits = bit_map[bit_map_index].bit_depth; + return true; + } + } + return false; +} +} // namespace + +SndfileSource::SndfileSource(core::IArena& arena, const Config& config) + : driver_name_(arena) + , input_name_(arena) + , buffer_(arena) + , buffer_size_(0) + , sndfile_input_(NULL) + , is_file_(true) + , eof_(false) + , paused_(false) + , valid_(false) { + BackendMap::instance(); + + if (config.sample_spec.num_channels() == 0) { + roc_log(LogError, "sndfile source: # of channels is zero"); + return; + } + + if (config.latency != 0) { + roc_log(LogError, + "sndfile source: setting io latency not supported by sndfile backend"); + 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(&sf_info_in_, 0, sizeof(sf_info_in_)); + sample_rate_ = (int)config.sample_spec.sample_rate(); + precision_ = 32; + + 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_); + + roc_log(LogInfo, "sndfile source: opening: driver=%s path=%s", driver, path); + + if (buffer_.size() != 0 || sndfile_input_) { + roc_panic("sndfile source: can't call open() more than once"); + } + + if (!setup_names_(driver, path)) { + return false; + } + + if (!open_()) { + return false; + } + + if (!setup_buffer_()) { + return false; + } + + return true; +} + +DeviceType SndfileSource::type() const { + return DeviceType_Source; +} + +DeviceState SndfileSource::state() const { + roc_panic_if(!valid_); + + if (paused_) { + return DeviceState_Paused; + } else { + return DeviceState_Active; + } +} + +void SndfileSource::pause() { + roc_panic_if(!valid_); + + if (paused_) { + return; + } + + if (!sndfile_input_) { + roc_panic("sndfile source: pause: non-open input file or device"); + } + + roc_log(LogDebug, "sndfile source: pausing: driver=%s input=%s", driver_name_.c_str(), + input_name_.c_str()); + + if (!is_file_) { + close_(); + } + + paused_ = true; +} + +bool SndfileSource::resume() { + roc_panic_if(!valid_); + + if (!paused_) { + return true; + } + + roc_log(LogDebug, "sndfile source: resuming: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + + if (!sndfile_input_) { + if (!open_()) { + roc_log(LogError, + "sndfile source: open failed when resuming: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + return false; + } + } + + paused_ = false; + return true; +} + +bool SndfileSource::restart() { + roc_panic_if(!valid_); + + roc_log(LogDebug, "sndfile source: restarting: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + + if (is_file_ && !eof_) { + if (!seek_(0)) { + roc_log(LogError, + "sndfile source: seek failed when restarting: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + return false; + } + } else { + if (sndfile_input_) { + close_(); + } + + if (!open_()) { + roc_log(LogError, + "sndfile source: open failed when restarting: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + return false; + } + } + + paused_ = false; + eof_ = false; + + return true; +} + +audio::SampleSpec SndfileSource::sample_spec() const { + roc_panic_if(!valid_); + + if (!sndfile_input_) { + roc_panic("sndfile source: sample_rate(): non-open output file or device"); + } + + if (sf_info_in_.channels == 1) { + return audio::SampleSpec(size_t(sf_info_in_.samplerate), + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, + audio::ChanMask_Surround_Mono); + } + + if (sf_info_in_.channels == 2) { + return audio::SampleSpec(size_t(sf_info_in_.samplerate), + audio::ChanLayout_Surround, audio::ChanOrder_Smpte, + audio::ChanMask_Surround_Stereo); + } + + roc_panic("sndfile source: unsupported channel count"); +} + +core::nanoseconds_t SndfileSource::latency() const { + roc_panic_if(!valid_); + + if (!sndfile_input_) { + roc_panic("sndfile source: latency(): non-open output file or device"); + } + + return 0; +} + +bool SndfileSource::has_latency() const { + roc_panic_if(!valid_); + + if (!sndfile_input_) { + roc_panic("sndfile source: has_latency(): non-open input file or device"); + } + + return false; +} + +bool SndfileSource::has_clock() const { + roc_panic_if(!valid_); + + if (!sndfile_input_) { + roc_panic("sndfile source: has_clock(): non-open input file or device"); + } + + return !is_file_; +} + +void SndfileSource::reclock(core::nanoseconds_t) { + // no-op +} + +bool SndfileSource::read(audio::Frame& frame) { + roc_panic_if(!valid_); + + if (paused_ || eof_) { + return false; + } + + if (!sndfile_input_) { + roc_panic("sndfile source: read: non-open input file or device"); + } + + audio::sample_t* frame_data = frame.samples(); + size_t frame_left = frame.num_samples(); + + audio::sample_t* buffer_data = buffer_.data(); + + while (frame_left != 0) { + size_t n_samples = + (size_t)sf_read_float(sndfile_input_, buffer_data, (sf_count_t)buffer_size_); + + if (n_samples == 0) { + roc_log(LogDebug, "sndfile source: got eof from sndfile"); + eof_ = true; + break; + } + + for (size_t n = 0; n < n_samples; n++) { + frame_data[n] = buffer_data[n]; + } + + frame_data += n_samples; + frame_left -= n_samples; + } + + if (frame_left == frame.num_samples()) { + return false; + } + + if (frame_left != 0) { + memset(frame_data, 0, frame_left * sizeof(audio::sample_t)); + } + + return true; +} + +bool SndfileSource::seek_(size_t offset) { + roc_panic_if(!valid_); + + if (!sndfile_input_) { + roc_panic("sndfile source: seek: non-open input file or device"); + } + + if (!is_file_) { + roc_panic("sndfile source: seek: not a file"); + } + + roc_log(LogDebug, "sndfile source: resetting position to %lu", (unsigned long)offset); + + sf_count_t err = sf_seek(sndfile_input_, (sf_count_t)offset, SEEK_SET); + if (err == -1) { + roc_log(LogError, + "sndfile source: can't reset position to %lu: an attempt was made to " + "seek beyond the start or end of the file", + (unsigned long)offset); + return false; + } + + return true; +} + +bool SndfileSource::setup_names_(const char* driver, const char* path) { + if (driver) { + if (!driver_name_.assign(driver)) { + roc_log(LogError, "sndfile source: can't allocate string"); + return false; + } + } + + if (path) { + if (!input_name_.assign(path)) { + roc_log(LogError, "sndfile source: can't allocate string"); + return false; + } + } + + return true; +} + +bool SndfileSource::setup_buffer_() { + buffer_size_ = sample_spec_.ns_2_samples_overall(frame_length_); + if (buffer_size_ == 0) { + roc_log(LogError, "sndfile source: buffer size is zero"); + return false; + } + if (!buffer_.resize(buffer_size_)) { + roc_log(LogError, "sndfile source: can't allocate sample buffer"); + return false; + } + + return true; +} + +bool SndfileSource::open_() { + if (sndfile_input_) { + roc_panic("sndfile source: already opened"); + } + + sf_info_in_.format = 0; + + sndfile_input_ = sf_open(input_name_.is_empty() ? NULL : input_name_.c_str(), + SFM_READ, &sf_info_in_); + if (!sndfile_input_) { + roc_log(LogInfo, "sndfile source: can't open: driver=%s input=%s", + driver_name_.c_str(), input_name_.c_str()); + return false; + } + + if (sf_info_in_.channels != (int)sample_spec_.num_channels()) { + roc_log(LogError, + "sndfile source: can't open: unsupported # of channels: " + "expected=%lu actual=%lu", + (unsigned long)sample_spec_.num_channels(), + (unsigned long)sf_info_in_.channels); + return false; + } + + if (sample_rate_ != 0) { + sf_info_in_.samplerate = sample_rate_; + } + + sample_spec_.set_sample_rate((unsigned long)sf_info_in_.samplerate); + + int in_bits = 0; + int format_subtype = 0; + + if (!find_subtype(format_subtype, sndfile_input_, sf_info_in_)) { + roc_log(LogDebug, "sndfile source: FORMAT_SUBTYPE could not be detected."); + } + + if (!map_to_bit_depth(in_bits, format_subtype)) { + roc_log(LogDebug, "sndfile source: in_bits could not be detected."); + } + + roc_log(LogInfo, + "sndfile source:" + " in_bits=%lu out_bits=%lu in_rate=%lu out_rate=%lu" + " in_ch=%lu out_ch=%lu is_file=%d", + (unsigned long)in_bits, (unsigned long)precision_, + (unsigned long)sf_info_in_.samplerate, (unsigned long)sample_rate_, + (unsigned long)sf_info_in_.channels, (unsigned long)0, (int)is_file_); + + return true; +} + +void SndfileSource::close_() { + if (!sndfile_input_) { + return; + } + + roc_log(LogInfo, "sndfile source: closing input"); + + int err = sf_close(sndfile_input_); + if (err != 0) { + roc_panic("sndfile source: can't close input: %s", sf_error_number(err)); + } + + sndfile_input_ = 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..20988a80c --- /dev/null +++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h @@ -0,0 +1,121 @@ +/* + * 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); + + //! 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 setup_names_(const char* driver, const char* path); + bool setup_buffer_(); + + int map_to_sndfile_(); + bool open_(); + void close_(); + + bool seek_(size_t offset); + + int get_out_bits_(); + + core::StringBuffer driver_name_; + core::StringBuffer input_name_; + + core::Array buffer_; + size_t buffer_size_; + core::nanoseconds_t frame_length_; + audio::SampleSpec sample_spec_; + + SNDFILE* sndfile_input_; + SF_INFO sf_info_in_; + + int sample_rate_; + int precision_; + bool is_file_; + bool eof_; + bool paused_; + bool valid_; +}; + +} // namespace sndio +} // namespace roc + +#endif // ROC_SNDIO_SNDFILE_SOURCE_H_ diff --git a/src/tests/roc_sndio/target_sndfile/test_sndfile_sink.cpp b/src/tests/roc_sndio/target_sndfile/test_sndfile_sink.cpp new file mode 100644 index 000000000..e4808fadf --- /dev/null +++ b/src/tests/roc_sndio/target_sndfile/test_sndfile_sink.cpp @@ -0,0 +1,76 @@ +/* + * 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 "roc_core/heap_arena.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/sndfile_sink.h" + +namespace roc { +namespace sndio { + +namespace { + +enum { FrameSize = 500, SampleRate = 44100, ChMask = 0x3 }; + +core::HeapArena arena; + +} // namespace + +TEST_GROUP(sndfile_sink) { + Config sink_config; + + void setup() { + sink_config.sample_spec = audio::SampleSpec( + SampleRate, 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(sndfile_sink, noop) { + SndfileSink sndfile_sink(arena, sink_config); +} + +TEST(sndfile_sink, error) { + SndfileSink sndfile_sink(arena, sink_config); + + CHECK(!sndfile_sink.open(NULL, "/bad/file")); +} + +TEST(sndfile_sink, has_clock) { + SndfileSink sndfile_sink(arena, sink_config); + + core::TempFile file("test.wav"); + CHECK(sndfile_sink.open(NULL, file.path())); + CHECK(!sndfile_sink.has_clock()); +} + +TEST(sndfile_sink, sample_rate_auto) { + sink_config.sample_spec.set_sample_rate(0); + SndfileSink sndfile_sink(arena, sink_config); + + core::TempFile file("test.wav"); + CHECK(sndfile_sink.open(NULL, file.path())); + CHECK(sndfile_sink.sample_spec().sample_rate() != 0); +} + +TEST(sndfile_sink, sample_rate_force) { + sink_config.sample_spec.set_sample_rate(SampleRate); + SndfileSink sndfile_sink(arena, sink_config); + + core::TempFile file("test.wav"); + CHECK(sndfile_sink.open(NULL, file.path())); + CHECK(sndfile_sink.sample_spec().sample_rate() == SampleRate); +} + +} // namespace sndio +} // namespace roc diff --git a/src/tests/roc_sndio/target_sndfile/test_sndfile_source.cpp b/src/tests/roc_sndio/target_sndfile/test_sndfile_source.cpp new file mode 100644 index 000000000..1be069845 --- /dev/null +++ b/src/tests/roc_sndio/target_sndfile/test_sndfile_source.cpp @@ -0,0 +1,260 @@ +/* + * 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/stddefs.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/pump.h" +#include "roc_sndio/sndfile_sink.h" +#include "roc_sndio/sndfile_source.h" + +namespace roc { +namespace sndio { + +namespace { + +enum { + MaxBufSize = 8192, + FrameSize = 500, + SampleRate = 44100, + ChMask = 0x3, + NumChans = 2 +}; + +const audio::SampleSpec + SampleSpecs(SampleRate, 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(sndfile_source) { + Config sink_config; + Config source_config; + + void setup() { + sink_config.sample_spec = audio::SampleSpec( + SampleRate, audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); + sink_config.frame_length = FrameDuration; + + source_config.sample_spec = audio::SampleSpec( + SampleRate, audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask); + source_config.frame_length = FrameDuration; + } +}; + +TEST(sndfile_source, noop) { + SndfileSource sndfile_source(arena, source_config); +} + +TEST(sndfile_source, error) { + SndfileSource sndfile_source(arena, source_config); + + CHECK(!sndfile_source.open(NULL, "/bad/file")); +} + +TEST(sndfile_source, has_clock) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_source.open(NULL, file.path())); + CHECK(!sndfile_source.has_clock()); +} + +TEST(sndfile_source, sample_rate_auto) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_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; + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_source.open(NULL, file.path())); + CHECK(sndfile_source.sample_spec().sample_rate() == SampleRate); +} + +TEST(sndfile_source, sample_rate_mismatch) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(MaxBufSize * 10); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + source_config.sample_spec.set_sample_rate(SampleRate * 2); + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_source.open(NULL, file.path())); + CHECK(sndfile_source.sample_spec().sample_rate() == SampleRate * 2); +} + +TEST(sndfile_source, pause_resume) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_source.open(NULL, file.path())); + + audio::sample_t frame_data1[FrameSize * NumChans] = {}; + audio::Frame frame1(frame_data1, FrameSize * NumChans); + + CHECK(sndfile_source.state() == DeviceState_Active); + CHECK(sndfile_source.read(frame1)); + + sndfile_source.pause(); + CHECK(sndfile_source.state() == DeviceState_Paused); + + audio::sample_t frame_data2[FrameSize * NumChans] = {}; + audio::Frame frame2(frame_data2, FrameSize * NumChans); + + CHECK(!sndfile_source.read(frame2)); + + CHECK(sndfile_source.resume()); + CHECK(sndfile_source.state() == DeviceState_Active); + + CHECK(sndfile_source.read(frame2)); + + if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) == 0) { + FAIL("frames should not be equal"); + } +} + +TEST(sndfile_source, pause_restart) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_source.open(NULL, file.path())); + + audio::sample_t frame_data1[FrameSize * NumChans] = {}; + audio::Frame frame1(frame_data1, FrameSize * NumChans); + + CHECK(sndfile_source.state() == DeviceState_Active); + CHECK(sndfile_source.read(frame1)); + + sndfile_source.pause(); + CHECK(sndfile_source.state() == DeviceState_Paused); + + audio::sample_t frame_data2[FrameSize * NumChans] = {}; + audio::Frame frame2(frame_data2, FrameSize * NumChans); + + CHECK(!sndfile_source.read(frame2)); + + CHECK(sndfile_source.restart()); + CHECK(sndfile_source.state() == DeviceState_Active); + + CHECK(sndfile_source.read(frame2)); + + if (memcmp(frame_data1, frame_data2, sizeof(frame_data1)) != 0) { + FAIL("frames should be equal"); + } +} + +TEST(sndfile_source, eof_restart) { + core::TempFile file("test.wav"); + + { + test::MockSource mock_source; + mock_source.add(FrameSize * NumChans * 2); + + SndfileSink sndfile_sink(arena, sink_config); + CHECK(sndfile_sink.open(NULL, file.path())); // TODO beginning of segfault + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, FrameDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + } + + SndfileSource sndfile_source(arena, source_config); + + CHECK(sndfile_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(sndfile_source.read(frame)); + CHECK(sndfile_source.read(frame)); + CHECK(!sndfile_source.read(frame)); + + CHECK(sndfile_source.restart()); + } +} + +} // namespace sndio +} // namespace roc 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 b00c5df35..000000000 --- a/src/tests/roc_sndio/target_sox/test_pump.cpp +++ /dev/null @@ -1,133 +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::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::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_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 e9b5498ac..15d63026a 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 @@ -92,4 +92,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 03b9414d6..7da1a5d07 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 @@ -119,4 +119,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..02cc0a3ad --- /dev/null +++ b/src/tests/roc_sndio/test_pump.cpp @@ -0,0 +1,238 @@ +/* + * 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/stddefs.h" +#include "roc_core/temp_file.h" +#include "roc_sndio/config.h" +#include "roc_sndio/pump.h" +#ifdef ROC_TARGET_SOX +#include "roc_sndio/sox_sink.h" +#include "roc_sndio/sox_source.h" +#endif // ROC_TARGET_SOX +#ifdef ROC_TARGET_SNDFILE +#include "roc_sndio/sndfile_sink.h" +#include "roc_sndio/sndfile_source.h" +#endif // ROC_TARGET_SNDFILE + +namespace roc { +namespace sndio { + +namespace { + +enum { BufSize = 512, SampleRate = 44100, ChMask = 0x3 }; + +const audio::SampleSpec + SampleSpecs(SampleRate, 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::ChanLayout_Surround, + audio::ChanOrder_Smpte, ChMask); + config.frame_length = BufDuration; + } +}; + +TEST(pump, write_read) { +#ifdef ROC_TARGET_SOX + { 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()); +} +#endif // ROC_TARGET_SOX + +#ifdef ROC_TARGET_SNDFILE +{ + enum { NumSamples = BufSize * 10 }; + + test::MockSource mock_source; + mock_source.add(NumSamples); + + core::TempFile file("test.wav"); + + { + SndfileSink sndfile_sink(arena, config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_sink, BufDuration, + SampleSpecs, Pump::ModeOneshot); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + CHECK(mock_source.num_returned() >= NumSamples - BufSize); + } + + SoxSource sndfile_source(arena, config); + CHECK(sndfile_source.open(NULL, file.path())); + + test::MockSink mock_writer; + + Pump pump(buffer_factory, sndfile_source, NULL, mock_writer, BufDuration, SampleSpecs, + Pump::ModePermanent); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + mock_writer.check(0, mock_source.num_returned()); +} +#endif // ROC_TARGET_SNDFILE +} // namespace roc + +TEST(pump, write_overwrite_read) { +#ifdef ROC_TARGET_SOX + { + 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); + } +#endif // ROC_TARGET_SOX + +#ifdef ROC_TARGET_SNDFILE + { + enum { NumSamples = BufSize * 10 }; + + test::MockSource mock_source; + mock_source.add(NumSamples); + + core::TempFile file("test.wav"); + + { + SndfileSink sndfile_sink(arena, config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_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); + + { + SndfileSink sndfile_sink(arena, config); + CHECK(sndfile_sink.open(NULL, file.path())); + + Pump pump(buffer_factory, mock_source, NULL, sndfile_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); + + SndfileSource sndfile_source(arena, config); + CHECK(sndfile_source.open(NULL, file.path())); + + test::MockSink mock_writer; + + Pump pump(buffer_factory, sndfile_source, NULL, mock_writer, BufDuration, + SampleSpecs, Pump::ModePermanent); + CHECK(pump.is_valid()); + CHECK(pump.run()); + + mock_writer.check(num_returned1, num_returned2); + } +#endif // ROC_TARGET_SNDFILE +} + +} // namespace sndio +} // namespace roc