Skip to content

Commit

Permalink
Add internal utility to log audio streams to disk from real-time audi…
Browse files Browse the repository at this point in the history
…o callbacks
  • Loading branch information
padenot committed Sep 25, 2023
1 parent f9c118d commit 54d52a9
Show file tree
Hide file tree
Showing 4 changed files with 421 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
237 changes: 237 additions & 0 deletions src/cubeb_audio_dump.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* 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 <mutex>
#include <thread>
#include <vector>

using std::mutex;
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:
mutex session_mutex;
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;
}
108 changes: 108 additions & 0 deletions src/cubeb_audio_dump.h
Original file line number Diff line number Diff line change
@@ -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 externaly.
// 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 externaly.
//
// 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 externaly.
//
// 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 externaly.
//
// 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
Loading

0 comments on commit 54d52a9

Please sign in to comment.