aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorPaul Adenot <[email protected]>2023-09-22 17:48:20 +0200
committerPaul Adenot <[email protected]>2024-04-16 16:31:18 +0200
commita6752d6ffe33994c757a6134524cf201c5c63cba (patch)
tree7432c020a7567492dcbcc1414ca4349bb54fe66d
parent529c3d26443003e474535766f7bb67ca4bb0edee (diff)
downloadcubeb-a6752d6ffe33994c757a6134524cf201c5c63cba.tar.gz
cubeb-a6752d6ffe33994c757a6134524cf201c5c63cba.zip
Add internal utility to log audio streams to disk from real-time audio callbacks
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/cubeb_audio_dump.cpp234
-rw-r--r--src/cubeb_audio_dump.h108
-rw-r--r--test/test_audio_dump.cpp74
4 files changed, 418 insertions, 0 deletions
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 <chrono>
+#include <limits>
+#include <thread>
+#include <vector>
+
+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<int>(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<int>(count * sample_size);
+ int rv = ringbuffer.enqueue(static_cast<uint8_t *>(samples), bytes);
+ return rv == bytes;
+ }
+
+private:
+ uint32_t sample_size;
+ FILE * file{};
+ lock_free_queue<uint8_t> 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<int32_t>::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<uint8_t *>(&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<cubeb_audio_dump_stream_t> streams{};
+ std::atomic<bool> 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 <ratio>
+
+#include "cubeb_audio_dump.h"
+#include "gtest/gtest.h"
+#include <chrono>
+#include <cmath>
+#include <fstream>
+#include <iostream>
+#include <thread>
+
+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