-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add internal utility to log audio streams to disk from real-time audi…
…o callbacks
- Loading branch information
Showing
4 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.