From a6752d6ffe33994c757a6134524cf201c5c63cba Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Fri, 22 Sep 2023 17:48:20 +0200 Subject: Add internal utility to log audio streams to disk from real-time audio callbacks --- CMakeLists.txt | 2 + src/cubeb_audio_dump.cpp | 234 +++++++++++++++++++++++++++++++++++++++++++++++ src/cubeb_audio_dump.h | 108 ++++++++++++++++++++++ test/test_audio_dump.cpp | 74 +++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 src/cubeb_audio_dump.cpp create mode 100644 src/cubeb_audio_dump.h create mode 100644 test/test_audio_dump.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 75955be..541120a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ endif() add_library(cubeb src/cubeb.c + src/cubeb_audio_dump.cpp src/cubeb_mixer.cpp src/cubeb_resampler.cpp src/cubeb_log.cpp @@ -406,6 +407,7 @@ if(BUILD_TESTS) cubeb_add_test(duplex) cubeb_add_test(logging) cubeb_add_test(triple_buffer) + cubeb_add_test(audio_dump) if (USE_WASAPI) cubeb_add_test(overload_callback) diff --git a/src/cubeb_audio_dump.cpp b/src/cubeb_audio_dump.cpp new file mode 100644 index 0000000..8de3d88 --- /dev/null +++ b/src/cubeb_audio_dump.cpp @@ -0,0 +1,234 @@ +/* + * Copyright © 2023 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#define NOMINMAX + +#include "cubeb_audio_dump.h" +#include "cubeb/cubeb.h" +#include "cubeb_ringbuffer.h" +#include +#include +#include +#include + +using std::thread; +using std::vector; + +uint32_t +bytes_per_sample(cubeb_stream_params params) +{ + switch (params.format) { + case CUBEB_SAMPLE_S16LE: + case CUBEB_SAMPLE_S16BE: + return sizeof(int16_t); + case CUBEB_SAMPLE_FLOAT32LE: + case CUBEB_SAMPLE_FLOAT32BE: + return sizeof(float); + }; +} + +struct cubeb_audio_dump_stream { +public: + explicit cubeb_audio_dump_stream(cubeb_stream_params params) + : sample_size(bytes_per_sample(params)), + ringbuffer( + static_cast(params.rate * params.channels * sample_size)) + { + } + + int open(const char * name) + { + file = fopen(name, "wb"); + if (!file) { + return CUBEB_ERROR; + } + return CUBEB_OK; + } + int close() + { + if (fclose(file)) { + return CUBEB_ERROR; + } + return CUBEB_OK; + } + + // Directly write to the file. Useful to write the header. + size_t write(uint8_t * data, uint32_t count) + { + return fwrite(data, count, 1, file); + } + + size_t write_all() + { + int available = ringbuffer.available_read(); + size_t written = 0; + while (available) { + const int buf_sz = 16 * 1024; + uint8_t buf[buf_sz]; + int rv = ringbuffer.dequeue(buf, buf_sz); + available -= rv; + written += fwrite(buf, rv, 1, file); + } + return written; + } + int dump(void * samples, uint32_t count) + { + int bytes = static_cast(count * sample_size); + int rv = ringbuffer.enqueue(static_cast(samples), bytes); + return rv == bytes; + } + +private: + uint32_t sample_size; + FILE * file{}; + lock_free_queue ringbuffer; +}; + +struct cubeb_audio_dump_session { +public: + cubeb_audio_dump_session() = default; + ~cubeb_audio_dump_session() + { + assert(streams.empty()); + session_thread.join(); + } + cubeb_audio_dump_session(const cubeb_audio_dump_session &) = delete; + cubeb_audio_dump_session & + operator=(const cubeb_audio_dump_session &) = delete; + cubeb_audio_dump_session & operator=(cubeb_audio_dump_session &&) = delete; + + cubeb_audio_dump_stream_t create_stream(cubeb_stream_params params, + const char * name) + { + if (running) { + return nullptr; + } + auto * stream = new cubeb_audio_dump_stream(params); + streams.push_back(stream); + int rv = stream->open(name); + if (rv != CUBEB_OK) { + delete stream; + return nullptr; + } + + struct riff_header { + char chunk_id[4] = {'R', 'I', 'F', 'F'}; + int32_t chunk_size = 0; + char format[4] = {'W', 'A', 'V', 'E'}; + + char subchunk_id_1[4] = {'f', 'm', 't', 0x20}; + int32_t subchunk_1_size = 16; + int16_t audio_format{}; + int16_t num_channels{}; + int32_t sample_rate{}; + int32_t byte_rate{}; + int16_t block_align{}; + int16_t bits_per_sample{}; + + char subchunk_id_2[4] = {'d', 'a', 't', 'a'}; + int32_t subchunkd_2_size = std::numeric_limits::max(); + }; + + riff_header header; + // 1 is integer PCM, 3 is float PCM + header.audio_format = bytes_per_sample(params) == 2 ? 1 : 3; + header.num_channels = params.channels; + header.sample_rate = params.rate; + header.byte_rate = bytes_per_sample(params) * params.rate * params.channels; + header.block_align = params.channels * bytes_per_sample(params); + header.bits_per_sample = bytes_per_sample(params) * 8; + + stream->write(reinterpret_cast(&header), sizeof(riff_header)); + + return stream; + } + int delete_stream(cubeb_audio_dump_stream * stream) + { + assert(!running); + stream->close(); + streams.erase(std::remove(streams.begin(), streams.end(), stream), + streams.end()); + return CUBEB_OK; + } + int start() + { + assert(!running); + running = true; + session_thread = std::thread([this] { + while (running) { + for (auto * stream : streams) { + stream->write_all(); + } + const int DUMP_INTERVAL = 10; + std::this_thread::sleep_for(std::chrono::milliseconds(DUMP_INTERVAL)); + } + }); + return CUBEB_OK; + } + int stop() + { + assert(running); + running = false; + return CUBEB_OK; + } + +private: + thread session_thread; + vector streams{}; + std::atomic running = false; +}; + +int +cubeb_audio_dump_init(cubeb_audio_dump_session_t * session) +{ + *session = new cubeb_audio_dump_session; + return CUBEB_OK; +} + +int +cubeb_audio_dump_shutdown(cubeb_audio_dump_session_t session) +{ + delete session; + return CUBEB_OK; +} + +int +cubeb_audio_dump_stream_init(cubeb_audio_dump_session_t session, + cubeb_audio_dump_stream_t * stream, + cubeb_stream_params stream_params, + const char * name) +{ + *stream = session->create_stream(stream_params, name); + return CUBEB_OK; +} + +int +cubeb_audio_dump_stream_shutdown(cubeb_audio_dump_session_t session, + cubeb_audio_dump_stream_t stream) +{ + return session->delete_stream(stream); +} + +int +cubeb_audio_dump_start(cubeb_audio_dump_session_t session) +{ + return session->start(); +} + +int +cubeb_audio_dump_stop(cubeb_audio_dump_session_t session) +{ + return session->stop(); +} + +int +cubeb_audio_dump_write(cubeb_audio_dump_stream_t stream, void * audio_samples, + uint32_t count) +{ + stream->dump(audio_samples, count); + return CUBEB_OK; +} diff --git a/src/cubeb_audio_dump.h b/src/cubeb_audio_dump.h new file mode 100644 index 0000000..ae473e6 --- /dev/null +++ b/src/cubeb_audio_dump.h @@ -0,0 +1,108 @@ +/* + * Copyright © 2023 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_AUDIO_DUMP +#define CUBEB_AUDIO_DUMP + +#include "cubeb/cubeb.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct cubeb_audio_dump_stream * cubeb_audio_dump_stream_t; +typedef struct cubeb_audio_dump_session * cubeb_audio_dump_session_t; + +// Start audio dumping session +// This can only be called if the other API functions +// aren't currently being called: synchronized externally. +// This is not real-time safe. +// +// This is generally called when deciding to start logging some audio. +// +// Returns 0 in case of success. +int +cubeb_audio_dump_init(cubeb_audio_dump_session_t * session); + +// End audio dumping session +// This can only be called if the other API functions +// aren't currently being called: synchronized externally. +// +// This is generally called when deciding to stop logging some audio. +// +// This is not real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_shutdown(cubeb_audio_dump_session_t session); + +// Register a stream for dumping to a file +// This can only be called if cubeb_audio_dump_write +// isn't currently being called: synchronized externally. +// +// This is generally called when setting up a system-level stream side (either +// input or output). +// +// This is not real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_stream_init(cubeb_audio_dump_session_t session, + cubeb_audio_dump_stream_t * stream, + cubeb_stream_params stream_params, + const char * name); + +// Unregister a stream for dumping to a file +// This can only be called if cubeb_audio_dump_write +// isn't currently being called: synchronized externally. +// +// This is generally called when a system-level audio stream side +// (input/output) has been stopped and drained, and the audio callback isn't +// going to be called. +// +// This is not real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_stream_shutdown(cubeb_audio_dump_session_t session, + cubeb_audio_dump_stream_t stream); + +// Start dumping. +// cubeb_audio_dump_write can now be called. +// +// This starts dumping the audio to disk. Generally this is called when +// cubeb_stream_start is caled is called, but can be called at the beginning of +// the application. +// +// This is not real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_start(cubeb_audio_dump_session_t session); + +// Stop dumping. +// cubeb_audio_dump_write can't be called at this point. +// +// This stops dumping the audio to disk cubeb_stream_stop is caled is called, +// but can be called before exiting the application. +// +// This is not real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_stop(cubeb_audio_dump_session_t session); + +// Dump some audio samples for audio stream id. +// +// This is generally called from the real-time audio callback. +// +// This is real-time safe. +// Returns 0 in case of success. +int +cubeb_audio_dump_write(cubeb_audio_dump_stream_t stream, void * audio_samples, + uint32_t count); + +#ifdef __cplusplus +}; +#endif + +#endif diff --git a/test/test_audio_dump.cpp b/test/test_audio_dump.cpp new file mode 100644 index 0000000..f01c609 --- /dev/null +++ b/test/test_audio_dump.cpp @@ -0,0 +1,74 @@ +/* + * Copyright © 2023 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#define NOMINMAX +#define _USE_MATH_DEFINES + +#include "cubeb/cubeb.h" +#include + +#include "cubeb_audio_dump.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include + +TEST(cubeb, audio_dump) +{ + cubeb_audio_dump_session_t session; + int rv = cubeb_audio_dump_init(&session); + ASSERT_EQ(rv, 0); + + cubeb_stream_params params; + params.rate = 44100; + params.channels = 2; + params.format = CUBEB_SAMPLE_FLOAT32NE; + + cubeb_audio_dump_stream_t dump_stream; + rv = cubeb_audio_dump_stream_init(session, &dump_stream, params, "test.wav"); + ASSERT_EQ(rv, 0); + + rv = cubeb_audio_dump_start(session); + ASSERT_EQ(rv, 0); + + float phase = 0; + const size_t buf_sz = 2 * 44100 / 50; + float buf[buf_sz]; + for (uint32_t iteration = 0; iteration < 50; iteration++) { + uint32_t write_idx = 0; + for (uint32_t i = 0; i < buf_sz / params.channels; i++) { + for (uint32_t j = 0; j < params.channels; j++) { + buf[write_idx++] = sin(phase); + } + phase += 440 * M_PI * 2 / 44100; + if (phase > 2 * M_PI) { + phase -= 2 * M_PI; + } + } + rv = cubeb_audio_dump_write(dump_stream, buf, 2 * 44100 / 50); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_EQ(rv, 0); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + rv = cubeb_audio_dump_stop(session); + ASSERT_EQ(rv, 0); + + rv = cubeb_audio_dump_stream_shutdown(session, dump_stream); + ASSERT_EQ(rv, 0); + + rv = cubeb_audio_dump_shutdown(session); + ASSERT_EQ(rv, 0); + + std::ifstream file("test.wav"); + ASSERT_TRUE(file.good()); +} + +#undef NOMINMAX -- cgit v1.2.3