Bug 1331869 - Update cubeb from upstream to d96e35f02d. r=kinetik,padenot

MozReview-Commit-ID: 9H3cKh82Jwv

--HG--
extra : rebase_source : fc8f877e627aced0ff0fcc2d18766ba05560d6af
This commit is contained in:
Alex Chronopoulos 2017-01-19 18:02:42 +02:00
parent 349ec8590a
commit 538d575a01
28 changed files with 1938 additions and 488 deletions

View File

@ -5,4 +5,4 @@ Makefile.in build files for the Mozilla build system.
The cubeb git repository is: git://github.com/kinetiknz/cubeb.git
The git commit ID used was 051cd8478dd48a18e2e91e5f07837640f40618c1.
The git commit ID used was d96e35f02dbb9a093e5bfdff4f2948b7a6e9d3f9.

View File

@ -16,6 +16,13 @@
#include <unistd.h>
#endif
template<typename T, size_t N>
constexpr size_t
ARRAY_LENGTH(T(&)[N])
{
return N;
}
void delay(unsigned int ms)
{
#if defined(_WIN32)
@ -30,6 +37,34 @@ void delay(unsigned int ms)
#define M_PI 3.14159265358979323846
#endif
typedef struct {
char const * name;
unsigned int const channels;
cubeb_channel_layout const layout;
} layout_info;
layout_info const layout_infos[CUBEB_LAYOUT_MAX] = {
{ "undefined", 0, CUBEB_LAYOUT_UNDEFINED },
{ "dual mono", 2, CUBEB_LAYOUT_DUAL_MONO },
{ "dual mono lfe", 3, CUBEB_LAYOUT_DUAL_MONO_LFE },
{ "mono", 1, CUBEB_LAYOUT_MONO },
{ "mono lfe", 2, CUBEB_LAYOUT_MONO_LFE },
{ "stereo", 2, CUBEB_LAYOUT_STEREO },
{ "stereo lfe", 3, CUBEB_LAYOUT_STEREO_LFE },
{ "3f", 3, CUBEB_LAYOUT_3F },
{ "3f lfe", 4, CUBEB_LAYOUT_3F_LFE },
{ "2f1", 3, CUBEB_LAYOUT_2F1 },
{ "2f1 lfe", 4, CUBEB_LAYOUT_2F1_LFE },
{ "3f1", 4, CUBEB_LAYOUT_3F1 },
{ "3f1 lfe", 5, CUBEB_LAYOUT_3F1_LFE },
{ "2f2", 4, CUBEB_LAYOUT_2F2 },
{ "2f2 lfe", 5, CUBEB_LAYOUT_2F2_LFE },
{ "3f2", 5, CUBEB_LAYOUT_3F2 },
{ "3f2 lfe", 6, CUBEB_LAYOUT_3F2_LFE },
{ "3f3r lfe", 7, CUBEB_LAYOUT_3F3R_LFE },
{ "3f4 lfe", 8, CUBEB_LAYOUT_3F4_LFE }
};
int has_available_input_device(cubeb * ctx)
{
cubeb_device_collection * devices;

View File

@ -24,7 +24,6 @@
#define M_PI 3.14159265358979323846
#endif
#define NELEMS(x) ((int) (sizeof(x) / sizeof(x[0])))
#define VOLUME 0.2
float get_frequency(int channel_index)
@ -118,7 +117,7 @@ int supports_channel_count(const char* backend_id, int nchannels)
(strcmp(backend_id, "opensl") != 0 && strcmp(backend_id, "audiotrack") != 0);
}
int run_test(int num_channels, int sampling_rate, int is_float)
int run_test(int num_channels, layout_info layout, int sampling_rate, int is_float)
{
int r = CUBEB_OK;
@ -142,12 +141,13 @@ int run_test(int num_channels, int sampling_rate, int is_float)
goto cleanup;
}
fprintf(stderr, "Testing %d channel(s), %d Hz, %s (%s)\n", num_channels, sampling_rate, is_float ? "float" : "short", cubeb_get_backend_id(ctx));
fprintf(stderr, "Testing %d channel(s), layout: %s, %d Hz, %s (%s)\n", num_channels, layout.name, sampling_rate, is_float ? "float" : "short", cubeb_get_backend_id(ctx));
cubeb_stream_params params;
params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE;
params.rate = sampling_rate;
params.channels = num_channels;
params.layout = layout.layout;
synth = synth_create(params.channels, params.rate);
if (synth == NULL) {
@ -200,6 +200,7 @@ int run_panning_volume_test(int is_float)
params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE;
params.rate = 44100;
params.channels = 2;
params.layout = CUBEB_LAYOUT_STEREO;
synth = synth_create(params.channels, params.rate);
if (synth == NULL) {
@ -259,7 +260,7 @@ TEST(cubeb, run_panning_volume_test_float)
TEST(cubeb, run_channel_rate_test)
{
int channel_values[] = {
unsigned int channel_values[] = {
1,
2,
3,
@ -274,13 +275,16 @@ TEST(cubeb, run_channel_rate_test)
48000,
};
for(int j = 0; j < NELEMS(channel_values); ++j) {
for(int i = 0; i < NELEMS(freq_values); ++i) {
for(unsigned int j = 0; j < ARRAY_LENGTH(channel_values); ++j) {
for(unsigned int i = 0; i < ARRAY_LENGTH(freq_values); ++i) {
ASSERT_TRUE(channel_values[j] < MAX_NUM_CHANNELS);
fprintf(stderr, "--------------------------\n");
ASSERT_EQ(run_test(channel_values[j], freq_values[i], 0), CUBEB_OK);
ASSERT_EQ(run_test(channel_values[j], freq_values[i], 1), CUBEB_OK);
for (unsigned int k = 0 ; k < ARRAY_LENGTH(layout_infos); ++k ) {
if (layout_infos[k].channels == channel_values[j]) {
ASSERT_EQ(run_test(channel_values[j], layout_infos[k], freq_values[i], 0), CUBEB_OK);
ASSERT_EQ(run_test(channel_values[j], layout_infos[k], freq_values[i], 1), CUBEB_OK);
}
}
}
}
}

View File

@ -108,9 +108,11 @@ TEST(cubeb, duplex)
input_params.format = STREAM_FORMAT;
input_params.rate = 48000;
input_params.channels = 1;
input_params.layout = CUBEB_LAYOUT_MONO;
output_params.format = STREAM_FORMAT;
output_params.rate = 48000;
output_params.channels = 2;
output_params.layout = CUBEB_LAYOUT_STEREO;
r = cubeb_get_min_latency(ctx, output_params, &latency_frames);

View File

@ -9,6 +9,7 @@ TEST(cubeb, latency)
uint32_t max_channels;
uint32_t preferred_rate;
uint32_t latency_frames;
cubeb_channel_layout layout;
r = cubeb_init(&ctx, "Cubeb audio test");
ASSERT_EQ(r, CUBEB_OK);
@ -25,10 +26,17 @@ TEST(cubeb, latency)
ASSERT_GT(preferred_rate, 0u);
}
r = cubeb_get_preferred_channel_layout(ctx, &layout);
ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED);
cubeb_stream_params params = {
CUBEB_SAMPLE_FLOAT32NE,
preferred_rate,
max_channels
max_channels,
(r == CUBEB_OK) ? layout : CUBEB_LAYOUT_UNDEFINED
#if defined(__ANDROID__)
, CUBEB_STREAM_TYPE_MUSIC
#endif
};
r = cubeb_get_min_latency(ctx, params, &latency_frames);
ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED);

View File

@ -0,0 +1,181 @@
/*
* Copyright © 2016 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#include "gtest/gtest.h"
#include "cubeb/cubeb.h"
#include "cubeb_mixer.h"
#include "common.h"
#include <vector>
using std::vector;
#define STREAM_FREQUENCY 48000
#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE
float const M = 1.0f; // Mono
float const L = 2.0f; // Left
float const R = 3.0f; // Right
float const C = 4.0f; // Center
float const LS = 5.0f; // Left Surround
float const RS = 6.0f; // Right Surround
float const RLS = 7.0f; // Rear Left Surround
float const RC = 8.0f; // Rear Center
float const RRS = 9.0f; // Rear Right Surround
float const LFE = 10.0f; // Low Frequency Effects
float const INV_SQRT_2 = 0.707106f; // 1/sqrt(2)
static float const DOWNMIX_3F2_RESULTS[2][12][5] = {
// 3F2
{
{ INV_SQRT_2*(L+R) + C + 0.5f*(LS+RS) }, // Mono
{ INV_SQRT_2*(L+R) + C + 0.5f*(LS+RS), 0 }, // Mono-LFE
{ L + INV_SQRT_2*(C+LS), R + INV_SQRT_2*(C+RS) }, // Stereo
{ L + INV_SQRT_2*(C+LS), R + INV_SQRT_2*(C+RS), 0 }, // Stereo-LFE
{ L + INV_SQRT_2*LS, R + INV_SQRT_2*RS, C }, // 3F
{ L + INV_SQRT_2*LS, R + INV_SQRT_2*RS, C, 0 }, // 3F-LFE
{ L + C*INV_SQRT_2, R + C*INV_SQRT_2, INV_SQRT_2*(LS+RS) }, // 2F1
{ L + C*INV_SQRT_2, R + C*INV_SQRT_2, 0, INV_SQRT_2*(LS+RS) }, // 2F1-LFE
{ L, R, C, INV_SQRT_2*(LS+RS) }, // 3F1
{ L, R, C, 0, INV_SQRT_2*(LS+RS) }, // 3F1-LFE
{ L + INV_SQRT_2*C, R + INV_SQRT_2*C, LS, RS }, // 2F2
{ L + INV_SQRT_2*C, R + INV_SQRT_2*C, 0, LS, RS } // 2F2-LFE
},
// 3F2-LFE
{
{ INV_SQRT_2*(L+R) + C + 0.5f*(LS+RS) }, // Mono
{ INV_SQRT_2*(L+R) + C + 0.5f*(LS+RS), LFE }, // Mono-LFE
{ L + INV_SQRT_2*(C+LS), R + INV_SQRT_2*(C+RS) }, // Stereo
{ L + INV_SQRT_2*(C+LS), R + INV_SQRT_2*(C+RS), LFE }, // Stereo-LFE
{ L + INV_SQRT_2*LS, R + INV_SQRT_2*RS, C }, // 3F
{ L + INV_SQRT_2*LS, R + INV_SQRT_2*RS, C, LFE }, // 3F-LFE
{ L + C*INV_SQRT_2, R + C*INV_SQRT_2, INV_SQRT_2*(LS+RS) }, // 2F1
{ L + C*INV_SQRT_2, R + C*INV_SQRT_2, LFE, INV_SQRT_2*(LS+RS) }, // 2F1-LFE
{ L, R, C, INV_SQRT_2*(LS+RS) }, // 3F1
{ L, R, C, LFE, INV_SQRT_2*(LS+RS) }, // 3F1-LFE
{ L + INV_SQRT_2*C, R + INV_SQRT_2*C, LS, RS }, // 2F2
{ L + INV_SQRT_2*C, R + INV_SQRT_2*C, LFE, LS, RS } // 2F2-LFE
}
};
typedef struct {
cubeb_channel_layout layout;
float data[10];
} audio_input;
audio_input audio_inputs[CUBEB_LAYOUT_MAX] = {
{ CUBEB_LAYOUT_UNDEFINED, { } },
{ CUBEB_LAYOUT_DUAL_MONO, { L, R } },
{ CUBEB_LAYOUT_DUAL_MONO_LFE, { L, R, LFE } },
{ CUBEB_LAYOUT_MONO, { M } },
{ CUBEB_LAYOUT_MONO_LFE, { M, LFE } },
{ CUBEB_LAYOUT_STEREO, { L, R } },
{ CUBEB_LAYOUT_STEREO_LFE, { L, R, LFE } },
{ CUBEB_LAYOUT_3F, { L, R, C } },
{ CUBEB_LAYOUT_3F_LFE, { L, R, C, LFE } },
{ CUBEB_LAYOUT_2F1, { L, R, RC } },
{ CUBEB_LAYOUT_2F1_LFE, { L, R, LFE, RC } },
{ CUBEB_LAYOUT_3F1, { L, R, C, RC } },
{ CUBEB_LAYOUT_3F1_LFE, { L, R, C, LFE, RC } },
{ CUBEB_LAYOUT_2F2, { L, R, LS, RS } },
{ CUBEB_LAYOUT_2F2_LFE, { L, R, LFE, LS, RS } },
{ CUBEB_LAYOUT_3F2, { L, R, C, LS, RS } },
{ CUBEB_LAYOUT_3F2_LFE, { L, R, C, LFE, LS, RS } },
{ CUBEB_LAYOUT_3F3R_LFE, { L, R, C, LFE, RC, LS, RS } },
{ CUBEB_LAYOUT_3F4_LFE, { L, R, C, LFE, RLS, RRS, LS, RS } }
};
void
downmix_test(float const * data, cubeb_channel_layout in_layout, cubeb_channel_layout out_layout)
{
if (in_layout == CUBEB_LAYOUT_UNDEFINED) {
return; // Only possible output layout would be UNDEFINED.
}
cubeb_stream_params in_params = {
STREAM_FORMAT,
STREAM_FREQUENCY,
layout_infos[in_layout].channels,
in_layout
#if defined(__ANDROID__)
, CUBEB_STREAM_TYPE_MUSIC
#endif
};
cubeb_stream_params out_params = {
STREAM_FORMAT,
STREAM_FREQUENCY,
// To downmix audio data with undefined layout, its channel number must be
// smaller than or equal to the input channels.
(out_layout == CUBEB_LAYOUT_UNDEFINED) ?
layout_infos[in_layout].channels : layout_infos[out_layout].channels,
out_layout
#if defined(__ANDROID__)
, CUBEB_STREAM_TYPE_MUSIC
#endif
};
if (!cubeb_should_downmix(&in_params, &out_params)) {
return;
}
fprintf(stderr, "Downmix from %s to %s\n", layout_infos[in_layout].name, layout_infos[out_layout].name);
unsigned int const inframes = 10;
vector<float> in(in_params.channels * inframes);
vector<float> out(out_params.channels * inframes);
for (unsigned int offset = 0 ; offset < inframes * in_params.channels ; offset += in_params.channels) {
for (unsigned int i = 0 ; i < in_params.channels ; ++i) {
in[offset + i] = data[i];
}
}
cubeb_downmix_float(in.data(), inframes, out.data(), in_params.channels, out_params.channels, in_params.layout, out_params.layout);
uint32_t in_layout_mask = 0;
for (unsigned int i = 0 ; i < in_params.channels; ++i) {
in_layout_mask |= 1 << CHANNEL_INDEX_TO_ORDER[in_layout][i];
}
uint32_t out_layout_mask = 0;
for (unsigned int i = 0 ; out_layout != CUBEB_LAYOUT_UNDEFINED && i < out_params.channels; ++i) {
out_layout_mask |= 1 << CHANNEL_INDEX_TO_ORDER[out_layout][i];
}
for (unsigned int i = 0 ; i < inframes * out_params.channels ; ++i) {
unsigned int index = i % out_params.channels;
// downmix_3f2
if ((in_layout == CUBEB_LAYOUT_3F2 || in_layout == CUBEB_LAYOUT_3F2_LFE) &&
out_layout >= CUBEB_LAYOUT_MONO && out_layout <= CUBEB_LAYOUT_2F2_LFE) {
auto & downmix_results = DOWNMIX_3F2_RESULTS[in_layout - CUBEB_LAYOUT_3F2][out_layout - CUBEB_LAYOUT_MONO];
fprintf(stderr, "[3f2] Expect: %lf, Get: %lf\n", downmix_results[index], out[index]);
ASSERT_EQ(out[index], downmix_results[index]);
continue;
}
// mix_remap
if (out_layout_mask & in_layout_mask) {
uint32_t mask = 1 << CHANNEL_INDEX_TO_ORDER[out_layout][index];
fprintf(stderr, "[map channels] Expect: %lf, Get: %lf\n", (mask & in_layout_mask) ? audio_inputs[out_layout].data[index] : 0, out[index]);
ASSERT_EQ(out[index], (mask & in_layout_mask) ? audio_inputs[out_layout].data[index] : 0);
continue;
}
// downmix_fallback
fprintf(stderr, "[fallback] Expect: %lf, Get: %lf\n", audio_inputs[in_layout].data[index], out[index]);
ASSERT_EQ(out[index], audio_inputs[in_layout].data[index]);
}
}
TEST(cubeb, run_mixing_test)
{
for (unsigned int i = 0 ; i < ARRAY_LENGTH(audio_inputs) ; ++i) {
for (unsigned int j = 0 ; j < ARRAY_LENGTH(layout_infos) ; ++j) {
downmix_test(audio_inputs[i].data, audio_inputs[i].layout, layout_infos[j].layout);
}
}
}

View File

@ -95,6 +95,7 @@ TEST(cubeb, record)
params.format = STREAM_FORMAT;
params.rate = SAMPLE_FREQUENCY;
params.channels = 1;
params.layout = CUBEB_LAYOUT_MONO;
r = cubeb_stream_init(ctx, &stream, "Cubeb record (mono)", NULL, &params, NULL, nullptr,
4096, data_cb_record, state_cb_record, &stream_state);

View File

@ -286,7 +286,6 @@ long data_cb_resampler(cubeb_stream * /*stm*/, void * user_ptr,
const float * in = reinterpret_cast<const float*>(input_buffer);
float * out = reinterpret_cast<float*>(output_buffer);
state->input.push(in, frame_count * state->input_channels);
/* Check how much output frames we need to write */
@ -362,7 +361,6 @@ void test_resampler_duplex(uint32_t input_channels, uint32_t output_channels,
fill_with_sine(expected_resampled_output.data() + latency,
output_rate, output_channels, output_rate * duration_s - latency, 0);
while (state.output_phase_index != state.max_output_phase_index) {
uint32_t leftover_samples = input_buffer.length() * input_channels;
input_buffer.reserve(input_array_frame_count);

View File

@ -18,19 +18,13 @@
#define STREAM_RATE 44100
#define STREAM_LATENCY 100 * STREAM_RATE / 1000
#define STREAM_CHANNELS 1
#define STREAM_LAYOUT CUBEB_LAYOUT_MONO
#if (defined(_WIN32) || defined(__WIN32__))
#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE
#else
#define STREAM_FORMAT CUBEB_SAMPLE_S16LE
#endif
template<typename T, size_t N>
constexpr size_t
ARRAY_LENGTH(T(&)[N])
{
return N;
}
int is_windows_7()
{
#ifdef __MINGW32__
@ -95,7 +89,6 @@ TEST(cubeb, init_destroy_context)
ASSERT_EQ(r, CUBEB_OK);
ASSERT_NE(ctx, nullptr);
backend_id = cubeb_get_backend_id(ctx);
ASSERT_TRUE(backend_id);
@ -138,6 +131,7 @@ TEST(cubeb, context_variables)
params.channels = STREAM_CHANNELS;
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -170,6 +164,7 @@ TEST(cubeb, init_destroy_stream)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -198,6 +193,7 @@ TEST(cubeb, init_destroy_multiple_streams)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -230,6 +226,7 @@ TEST(cubeb, configure_stream)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = 2; // panning
params.layout = CUBEB_LAYOUT_STEREO;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -265,6 +262,7 @@ test_init_start_stop_destroy_multiple_streams(int early, int delay_ms)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -280,7 +278,6 @@ test_init_start_stop_destroy_multiple_streams(int early, int delay_ms)
}
}
if (!early) {
for (i = 0; i < ARRAY_LENGTH(stream); ++i) {
r = cubeb_stream_start(stream[i]);
@ -352,6 +349,7 @@ TEST(cubeb, init_destroy_multiple_contexts_and_streams)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -392,6 +390,7 @@ TEST(cubeb, basic_stream_operations)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -442,6 +441,7 @@ TEST(cubeb, stream_position)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif
@ -581,6 +581,7 @@ TEST(cubeb, drain)
params.format = STREAM_FORMAT;
params.rate = STREAM_RATE;
params.channels = STREAM_CHANNELS;
params.layout = STREAM_LAYOUT;
#if defined(__ANDROID__)
params.stream_type = CUBEB_STREAM_TYPE_MUSIC;
#endif

View File

@ -110,6 +110,7 @@ TEST(cubeb, tone)
params.format = STREAM_FORMAT;
params.rate = SAMPLE_FREQUENCY;
params.channels = 1;
params.layout = CUBEB_LAYOUT_MONO;
user_data = (struct cb_user_data *) malloc(sizeof(*user_data));
if (user_data == NULL) {

View File

@ -176,12 +176,71 @@ typedef enum {
CUBEB_LOG_VERBOSE = 2, /**< Verbose logging of callbacks, can have performance implications. */
} cubeb_log_level;
/** SMPTE channel layout (also known as wave order)
* DUAL-MONO L R
* DUAL-MONO-LFE L R LFE
* MONO M
* MONO-LFE M LFE
* STEREO L R
* STEREO-LFE L R LFE
* 3F L R C
* 3F-LFE L R C LFE
* 2F1 L R S
* 2F1-LFE L R LFE S
* 3F1 L R C S
* 3F1-LFE L R C LFE S
* 2F2 L R LS RS
* 2F2-LFE L R LFE LS RS
* 3F2 L R C LS RS
* 3F2-LFE L R C LFE LS RS
* 3F3R-LFE L R C LFE RC LS RS
* 3F4-LFE L R C LFE RLS RRS LS RS
*
* The abbreviation of channel name is defined in following table:
* Abbr Channel name
* ---------------------------
* M Mono
* L Left
* R Right
* C Center
* LS Left Surround
* RS Right Surround
* RLS Rear Left Surround
* RC Rear Center
* RRS Rear Right Surround
* LFE Low Frequency Effects
*/
typedef enum {
CUBEB_LAYOUT_UNDEFINED, // Indicate the speaker's layout is undefined.
CUBEB_LAYOUT_DUAL_MONO,
CUBEB_LAYOUT_DUAL_MONO_LFE,
CUBEB_LAYOUT_MONO,
CUBEB_LAYOUT_MONO_LFE,
CUBEB_LAYOUT_STEREO,
CUBEB_LAYOUT_STEREO_LFE,
CUBEB_LAYOUT_3F,
CUBEB_LAYOUT_3F_LFE,
CUBEB_LAYOUT_2F1,
CUBEB_LAYOUT_2F1_LFE,
CUBEB_LAYOUT_3F1,
CUBEB_LAYOUT_3F1_LFE,
CUBEB_LAYOUT_2F2,
CUBEB_LAYOUT_2F2_LFE,
CUBEB_LAYOUT_3F2,
CUBEB_LAYOUT_3F2_LFE,
CUBEB_LAYOUT_3F3R_LFE,
CUBEB_LAYOUT_3F4_LFE,
CUBEB_LAYOUT_MAX
} cubeb_channel_layout;
/** Stream format initialization parameters. */
typedef struct {
cubeb_sample_format format; /**< Requested sample format. One of
#cubeb_sample_format. */
unsigned int rate; /**< Requested sample rate. Valid range is [1000, 192000]. */
unsigned int channels; /**< Requested channel count. Valid range is [1, 8]. */
cubeb_sample_format format; /**< Requested sample format. One of
#cubeb_sample_format. */
unsigned int rate; /**< Requested sample rate. Valid range is [1000, 192000]. */
unsigned int channels; /**< Requested channel count. Valid range is [1, 8]. */
cubeb_channel_layout layout; /**< Requested channel layout. This must be consistent with the provided channels. */
#if defined(__ANDROID__)
cubeb_stream_type stream_type; /**< Used to map Android audio stream types */
#endif
@ -376,7 +435,7 @@ CUBEB_EXPORT int cubeb_get_max_channel_count(cubeb * context, uint32_t * max_cha
/** Get the minimal latency value, in frames, that is guaranteed to work
when creating a stream for the specified sample rate. This is platform,
hardware and backend dependant.
hardware and backend dependent.
@param context A pointer to the cubeb context.
@param params On some backends, the minimum achievable latency depends on
the characteristics of the stream.
@ -390,7 +449,7 @@ CUBEB_EXPORT int cubeb_get_min_latency(cubeb * context,
uint32_t * latency_frames);
/** Get the preferred sample rate for this backend: this is hardware and
platform dependant, and can avoid resampling, and/or trigger fastpaths.
platform dependent, and can avoid resampling, and/or trigger fastpaths.
@param context A pointer to the cubeb context.
@param rate The samplerate (in Hz) the current configuration prefers.
@retval CUBEB_OK
@ -398,6 +457,15 @@ CUBEB_EXPORT int cubeb_get_min_latency(cubeb * context,
@retval CUBEB_ERROR_NOT_SUPPORTED */
CUBEB_EXPORT int cubeb_get_preferred_sample_rate(cubeb * context, uint32_t * rate);
/** Get the preferred layout for this backend: this is hardware and
platform dependent.
@param context A pointer to the cubeb context.
@param layout The layout of the current speaker configuration.
@retval CUBEB_OK
@retval CUBEB_ERROR_INVALID_PARAMETER
@retval CUBEB_ERROR_NOT_SUPPORTED */
CUBEB_EXPORT int cubeb_get_preferred_channel_layout(cubeb * context, cubeb_channel_layout * layout);
/** Destroy an application context. This must be called after all stream have
* been destroyed.
@param context A pointer to the cubeb context.*/

View File

@ -35,6 +35,14 @@ void cubeb_crash() CLANG_ANALYZER_NORETURN;
}
#endif
typedef struct {
char const * name;
unsigned int const channels;
cubeb_channel_layout const layout;
} cubeb_layout_map;
extern cubeb_layout_map const CUBEB_CHANNEL_LAYOUT_MAPS[CUBEB_LAYOUT_MAX];
struct cubeb_ops {
int (* init)(cubeb ** context, char const * context_name);
char const * (* get_backend_id)(cubeb * context);
@ -43,6 +51,7 @@ struct cubeb_ops {
cubeb_stream_params params,
uint32_t * latency_ms);
int (* get_preferred_sample_rate)(cubeb * context, uint32_t * rate);
int (* get_preferred_channel_layout)(cubeb * context, cubeb_channel_layout * layout);
int (* enumerate_devices)(cubeb * context, cubeb_device_type type,
cubeb_device_collection ** collection);
void (* destroy)(cubeb * context);

View File

@ -55,7 +55,6 @@ int audiotrack_init(cubeb ** context, char const * context_name);
int kai_init(cubeb ** context, char const * context_name);
#endif
static int
validate_stream_params(cubeb_stream_params * input_stream_params,
cubeb_stream_params * output_stream_params)
@ -96,8 +95,6 @@ validate_stream_params(cubeb_stream_params * input_stream_params,
return CUBEB_ERROR_INVALID_FORMAT;
}
static int
validate_latency(int latency)
{
@ -218,6 +215,20 @@ cubeb_get_preferred_sample_rate(cubeb * context, uint32_t * rate)
return context->ops->get_preferred_sample_rate(context, rate);
}
int
cubeb_get_preferred_channel_layout(cubeb * context, cubeb_channel_layout * layout)
{
if (!context || !layout) {
return CUBEB_ERROR_INVALID_PARAMETER;
}
if (!context->ops->get_preferred_channel_layout) {
return CUBEB_ERROR_NOT_SUPPORTED;
}
return context->ops->get_preferred_channel_layout(context, layout);
}
void
cubeb_destroy(cubeb * context)
{
@ -565,4 +576,3 @@ cubeb_crash()
abort();
*((volatile int *) NULL) = 0;
}

View File

@ -82,7 +82,7 @@ struct cubeb_stream {
cubeb_data_callback data_callback;
cubeb_state_callback state_callback;
void * user_ptr;
snd_pcm_uframes_t write_position;
snd_pcm_uframes_t stream_position;
snd_pcm_uframes_t last_position;
snd_pcm_uframes_t buffer_size;
cubeb_stream_params params;
@ -107,6 +107,12 @@ struct cubeb_stream {
being logically active and playing. */
struct timeval last_activity;
float volume;
char * buffer;
snd_pcm_uframes_t bufframes;
snd_pcm_stream_t stream_type;
struct cubeb_stream * other_stream;
};
static int
@ -234,6 +240,14 @@ set_timeout(struct timeval * timeout, unsigned int ms)
timeout->tv_usec += (ms % 1000) * 1000;
}
static void
stream_buffer_decrement(cubeb_stream * stm, long count)
{
char * bufremains = stm->buffer + snd_pcm_frames_to_bytes(stm->pcm, count);
memmove(stm->buffer, bufremains, snd_pcm_frames_to_bytes(stm->pcm, stm->bufframes - count));
stm->bufframes -= count;
}
static void
alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
{
@ -249,92 +263,173 @@ alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
}
static enum stream_state
alsa_refill_stream(cubeb_stream * stm)
alsa_process_stream(cubeb_stream * stm)
{
unsigned short revents;
snd_pcm_sframes_t avail;
long got;
void * p;
int draining;
draining = 0;
pthread_mutex_lock(&stm->mutex);
/* Call _poll_descriptors_revents() even if we don't use it
to let underlying plugins clear null events. Otherwise poll()
may wake up again and again, producing unnecessary CPU usage. */
snd_pcm_poll_descriptors_revents(stm->pcm, stm->fds, stm->nfds, &revents);
avail = snd_pcm_avail_update(stm->pcm);
if (avail < 0) {
snd_pcm_recover(stm->pcm, avail, 1);
avail = snd_pcm_avail_update(stm->pcm);
}
/* Failed to recover from an xrun, this stream must be broken. */
if (avail < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
return ERROR;
}
/* This should never happen. */
if ((unsigned int) avail > stm->buffer_size) {
avail = stm->buffer_size;
}
/* poll(2) claims this stream is active, so there should be some space
available to write. If avail is still zero here, the stream must be in
a funky state, bail and wait for another wakeup. */
/* Got null event? Bail and wait for another wakeup. */
if (avail == 0) {
pthread_mutex_unlock(&stm->mutex);
return RUNNING;
}
p = calloc(1, snd_pcm_frames_to_bytes(stm->pcm, avail));
assert(p);
pthread_mutex_unlock(&stm->mutex);
got = stm->data_callback(stm, stm->user_ptr, NULL, p, avail);
pthread_mutex_lock(&stm->mutex);
if (got < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
free(p);
return ERROR;
/* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time. */
if ((unsigned int) avail > stm->buffer_size) {
avail = stm->buffer_size;
}
if (got > 0) {
/* Capture: Read available frames */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) {
snd_pcm_sframes_t got;
if (avail + stm->bufframes > stm->buffer_size) {
/* Buffer overflow. Skip and overwrite with new data. */
stm->bufframes = 0;
// TODO: should it be marked as DRAINING?
}
got = snd_pcm_readi(stm->pcm, stm->buffer+stm->bufframes, avail);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
stm->stream_position += got;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Capture: Pass read frames to callback function */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 &&
(!stm->other_stream || stm->other_stream->bufframes < stm->other_stream->buffer_size)) {
long wrote = stm->bufframes;
struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer + stm->other_stream->bufframes : NULL;
/* Correct write size to the other stream available space */
if (stm->other_stream && wrote > stm->other_stream->buffer_size - stm->other_stream->bufframes) {
wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer, other_buffer, wrote);
pthread_mutex_lock(&stm->mutex);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
if (stm->other_stream) {
stm->other_stream->bufframes += wrote;
}
}
}
/* Playback: Don't have enough data? Let's ask for more. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > stm->bufframes &&
(!stm->other_stream || stm->other_stream->bufframes > 0)) {
long got = avail - stm->bufframes;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL;
char * buftail = stm->buffer + snd_pcm_frames_to_bytes(stm->pcm, stm->bufframes);
/* Correct read size to the other stream available frames */
if (stm->other_stream && got > stm->other_stream->bufframes) {
got = stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got);
pthread_mutex_lock(&stm->mutex);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
if (stm->other_stream) {
stream_buffer_decrement(stm->other_stream, got);
}
}
}
/* Playback: Still don't have enough data? Add some silence. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > stm->bufframes) {
long drain_frames = avail - stm->bufframes;
double drain_time = (double) drain_frames / stm->params.rate;
char * buftail = stm->buffer + snd_pcm_frames_to_bytes(stm->pcm, stm->bufframes);
memset(buftail, 0, snd_pcm_frames_to_bytes(stm->pcm, drain_frames));
stm->bufframes = avail;
/* Mark as draining, unless we're waiting for capture */
if (!stm->other_stream || stm->other_stream->bufframes > 0) {
set_timeout(&stm->drain_timeout, drain_time * 1000);
draining = 1;
}
}
/* Playback: Have enough data and no errors. Let's write it out. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) {
snd_pcm_sframes_t wrote;
if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
float * b = (float *) p;
for (uint32_t i = 0; i < got * stm->params.channels; i++) {
float * b = (float *) stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
} else {
short * b = (short *) p;
for (uint32_t i = 0; i < got * stm->params.channels; i++) {
short * b = (short *) stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
}
wrote = snd_pcm_writei(stm->pcm, p, got);
wrote = snd_pcm_writei(stm->pcm, stm->buffer, avail);
if (wrote < 0) {
snd_pcm_recover(stm->pcm, wrote, 1);
wrote = snd_pcm_writei(stm->pcm, p, got);
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
stm->stream_position += wrote;
gettimeofday(&stm->last_activity, NULL);
}
assert(wrote >= 0 && wrote == got);
stm->write_position += wrote;
gettimeofday(&stm->last_activity, NULL);
}
if (got != avail) {
long buffer_fill = stm->buffer_size - (avail - got);
double buffer_time = (double) buffer_fill / stm->params.rate;
/* Fill the remaining buffer with silence to guarantee one full period
has been written. */
snd_pcm_writei(stm->pcm, (char *) p + got, avail - got);
set_timeout(&stm->drain_timeout, buffer_time * 1000);
draining = 1;
}
free(p);
/* Got some error? Let's try to recover the stream. */
if (avail < 0) {
avail = snd_pcm_recover(stm->pcm, avail, 0);
/* Capture pcm must be started after initial setup/recover */
if (avail >= 0 &&
stm->stream_type == SND_PCM_STREAM_CAPTURE &&
snd_pcm_state(stm->pcm) == SND_PCM_STATE_PREPARED) {
avail = snd_pcm_start(stm->pcm);
}
}
/* Failed to recover, this stream must be broken. */
if (avail < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
return ERROR;
}
pthread_mutex_unlock(&stm->mutex);
return draining ? DRAINING : RUNNING;
}
@ -390,7 +485,7 @@ alsa_run(cubeb * ctx)
if (stm && stm->state == RUNNING && stm->fds && any_revents(stm->fds, stm->nfds)) {
alsa_set_stream_state(stm, PROCESSING);
pthread_mutex_unlock(&ctx->mutex);
state = alsa_refill_stream(stm);
state = alsa_process_stream(stm);
pthread_mutex_lock(&ctx->mutex);
alsa_set_stream_state(stm, state);
}
@ -576,15 +671,15 @@ init_local_config_with_workaround(char const * pcm_name)
}
static int
alsa_locked_pcm_open(snd_pcm_t ** pcm, snd_pcm_stream_t stream, snd_config_t * local_config)
alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name, snd_pcm_stream_t stream, snd_config_t * local_config)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
if (local_config) {
r = snd_pcm_open_lconf(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK, local_config);
r = snd_pcm_open_lconf(pcm, pcm_name, stream, SND_PCM_NONBLOCK, local_config);
} else {
r = snd_pcm_open(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK);
r = snd_pcm_open(pcm, pcm_name, stream, SND_PCM_NONBLOCK);
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
@ -707,7 +802,7 @@ alsa_init(cubeb ** context, char const * context_name)
/* Open a dummy PCM to force the configuration space to be evaluated so that
init_local_config_with_workaround can find and modify the default node. */
r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, NULL);
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, NULL);
if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
@ -717,7 +812,7 @@ alsa_init(cubeb ** context, char const * context_name)
pthread_mutex_unlock(&cubeb_alsa_mutex);
if (ctx->local_config) {
ctx->is_pa = 1;
r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, ctx->local_config);
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, ctx->local_config);
/* If we got a local_config, we found a PA PCM. If opening a PCM with that
config fails with EINVAL, the PA PCM is too old for this workaround. */
if (r == -EINVAL) {
@ -774,14 +869,14 @@ alsa_destroy(cubeb * ctx)
static void alsa_stream_destroy(cubeb_stream * stm);
static int
alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
cubeb_devid input_device,
cubeb_stream_params * input_stream_params,
cubeb_devid output_device,
cubeb_stream_params * output_stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback, cubeb_state_callback state_callback,
void * user_ptr)
alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
snd_pcm_stream_t stream_type,
cubeb_devid deviceid,
cubeb_stream_params * stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback,
cubeb_state_callback state_callback,
void * user_ptr)
{
(void)stream_name;
cubeb_stream * stm;
@ -789,23 +884,13 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
snd_pcm_format_t format;
snd_pcm_uframes_t period_size;
int latency_us = 0;
char const * pcm_name = deviceid ? (char const *) deviceid : CUBEB_ALSA_PCM_NAME;
assert(ctx && stream);
if (input_stream_params) {
/* Capture support not yet implemented. */
return CUBEB_ERROR_NOT_SUPPORTED;
}
if (input_device || output_device) {
/* Device selection not yet implemented. */
return CUBEB_ERROR_DEVICE_UNAVAILABLE;
}
*stream = NULL;
switch (output_stream_params->format) {
switch (stream_params->format) {
case CUBEB_SAMPLE_S16LE:
format = SND_PCM_FORMAT_S16_LE;
break;
@ -837,14 +922,18 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
stm->data_callback = data_callback;
stm->state_callback = state_callback;
stm->user_ptr = user_ptr;
stm->params = *output_stream_params;
stm->params = *stream_params;
stm->state = INACTIVE;
stm->volume = 1.0;
stm->buffer = NULL;
stm->bufframes = 0;
stm->stream_type = stream_type;
stm->other_stream = NULL;
r = pthread_mutex_init(&stm->mutex, NULL);
assert(r == 0);
r = alsa_locked_pcm_open(&stm->pcm, SND_PCM_STREAM_PLAYBACK, ctx->local_config);
r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type, ctx->local_config);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
@ -874,6 +963,11 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
r = snd_pcm_get_params(stm->pcm, &stm->buffer_size, &period_size);
assert(r == 0);
/* Double internal buffer size to have enough space when waiting for the other side of duplex connection */
stm->buffer_size *= 2;
stm->buffer = calloc(1, snd_pcm_frames_to_bytes(stm->pcm, stm->buffer_size));
assert(stm->buffer);
stm->nfds = snd_pcm_poll_descriptors_count(stm->pcm);
assert(stm->nfds > 0);
@ -895,6 +989,45 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
return CUBEB_OK;
}
static int
alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
cubeb_devid input_device,
cubeb_stream_params * input_stream_params,
cubeb_devid output_device,
cubeb_stream_params * output_stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback, cubeb_state_callback state_callback,
void * user_ptr)
{
int result = CUBEB_OK;
cubeb_stream * instm = NULL, * outstm = NULL;
if (result == CUBEB_OK && input_stream_params) {
result = alsa_stream_init_single(ctx, &instm, stream_name, SND_PCM_STREAM_CAPTURE,
input_device, input_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && output_stream_params) {
result = alsa_stream_init_single(ctx, &outstm, stream_name, SND_PCM_STREAM_PLAYBACK,
output_device, output_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && input_stream_params && output_stream_params) {
instm->other_stream = outstm;
outstm->other_stream = instm;
}
if (result != CUBEB_OK && instm) {
alsa_stream_destroy(instm);
}
*stream = outstm ? outstm : instm;
return result;
}
static void
alsa_stream_destroy(cubeb_stream * stm)
{
@ -907,6 +1040,11 @@ alsa_stream_destroy(cubeb_stream * stm)
ctx = stm->context;
if (stm->other_stream) {
stm->other_stream->other_stream = NULL; // to stop infinite recursion
alsa_stream_destroy(stm->other_stream);
}
pthread_mutex_lock(&stm->mutex);
if (stm->pcm) {
if (stm->state == DRAINING) {
@ -929,6 +1067,8 @@ alsa_stream_destroy(cubeb_stream * stm)
ctx->active_streams -= 1;
pthread_mutex_unlock(&ctx->mutex);
free(stm->buffer);
free(stm);
}
@ -952,6 +1092,8 @@ alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
return CUBEB_ERROR;
}
assert(stm);
r = snd_pcm_hw_params_any(stm->pcm, hw_params);
if (r < 0) {
return CUBEB_ERROR;
@ -1029,7 +1171,18 @@ alsa_stream_start(cubeb_stream * stm)
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_start(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&stm->mutex);
/* Capture pcm must be started after initial setup/recover */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE &&
snd_pcm_state(stm->pcm) == SND_PCM_STATE_PREPARED) {
snd_pcm_start(stm->pcm);
}
snd_pcm_pause(stm->pcm, 0);
gettimeofday(&stm->last_activity, NULL);
pthread_mutex_unlock(&stm->mutex);
@ -1054,6 +1207,12 @@ alsa_stream_stop(cubeb_stream * stm)
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_stop(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&ctx->mutex);
while (stm->state == PROCESSING) {
r = pthread_cond_wait(&stm->cond, &ctx->mutex);
@ -1090,8 +1249,8 @@ alsa_stream_get_position(cubeb_stream * stm, uint64_t * position)
assert(delay >= 0);
*position = 0;
if (stm->write_position >= (snd_pcm_uframes_t) delay) {
*position = stm->write_position - delay;
if (stm->stream_position >= (snd_pcm_uframes_t) delay) {
*position = stm->stream_position - delay;
}
stm->last_position = *position;
@ -1126,13 +1285,63 @@ alsa_stream_set_volume(cubeb_stream * stm, float volume)
return CUBEB_OK;
}
static int
alsa_enumerate_devices(cubeb * context, cubeb_device_type type,
cubeb_device_collection ** collection)
{
if (!context)
return CUBEB_ERROR;
uint32_t rate, max_channels;
int r;
r = alsa_get_preferred_sample_rate(context, &rate);
if (r != CUBEB_OK) {
return CUBEB_ERROR;
}
r = alsa_get_max_channel_count(context, &max_channels);
if (r != CUBEB_OK) {
return CUBEB_ERROR;
}
*collection = (cubeb_device_collection *) calloc(1, sizeof(cubeb_device_collection) + 1*sizeof(cubeb_device_info *));
assert(*collection);
char const * a_name = "default";
(*collection)->device[0] = (cubeb_device_info *) calloc(1, sizeof(cubeb_device_info));
assert((*collection)->device[0]);
(*collection)->device[0]->device_id = strdup(a_name);
(*collection)->device[0]->devid = (*collection)->device[0]->device_id;
(*collection)->device[0]->friendly_name = strdup(a_name);
(*collection)->device[0]->group_id = strdup(a_name);
(*collection)->device[0]->vendor_name = strdup(a_name);
(*collection)->device[0]->type = type;
(*collection)->device[0]->state = CUBEB_DEVICE_STATE_ENABLED;
(*collection)->device[0]->preferred = CUBEB_DEVICE_PREF_ALL;
(*collection)->device[0]->format = CUBEB_DEVICE_FMT_S16NE;
(*collection)->device[0]->default_format = CUBEB_DEVICE_FMT_S16NE;
(*collection)->device[0]->max_channels = max_channels;
(*collection)->device[0]->min_rate = rate;
(*collection)->device[0]->max_rate = rate;
(*collection)->device[0]->default_rate = rate;
(*collection)->device[0]->latency_lo = 0;
(*collection)->device[0]->latency_hi = 0;
(*collection)->count = 1;
return CUBEB_OK;
}
static struct cubeb_ops const alsa_ops = {
.init = alsa_init,
.get_backend_id = alsa_get_backend_id,
.get_max_channel_count = alsa_get_max_channel_count,
.get_min_latency = alsa_get_min_latency,
.get_preferred_sample_rate = alsa_get_preferred_sample_rate,
.enumerate_devices = NULL,
.get_preferred_channel_layout = NULL,
.enumerate_devices = alsa_enumerate_devices,
.destroy = alsa_destroy,
.stream_init = alsa_stream_init,
.stream_destroy = alsa_stream_destroy,

View File

@ -421,6 +421,7 @@ static struct cubeb_ops const audiotrack_ops = {
.get_max_channel_count = audiotrack_get_max_channel_count,
.get_min_latency = audiotrack_get_min_latency,
.get_preferred_sample_rate = audiotrack_get_preferred_sample_rate,
.get_preferred_channel_layout = NULL,
.enumerate_devices = NULL,
.destroy = audiotrack_destroy,
.stream_init = audiotrack_stream_init,

View File

@ -40,9 +40,11 @@ typedef UInt32 AudioFormatFlags;
#define AU_OUT_BUS 0
#define AU_IN_BUS 1
#define PRINT_ERROR_CODE(str, r) do { \
LOG("System call failed: %s (rv: %d)", str, r); \
} while(0)
#define PRINT_ERROR_CODE(str, r) do { \
LOG("System call failed: %s (rv: %d)", str, (int) r); \
} while(0)
const char * DISPATCH_QUEUE_LABEL = "org.mozilla.cubeb";
/* Testing empirically, some headsets report a minimal latency that is very
* low, but this does not work in practice. Lie and say the minimum is 256
@ -52,8 +54,8 @@ const uint32_t SAFE_MAX_LATENCY_FRAMES = 512;
void audiounit_stream_stop_internal(cubeb_stream * stm);
void audiounit_stream_start_internal(cubeb_stream * stm);
static void close_audiounit_stream(cubeb_stream * stm);
static int setup_audiounit_stream(cubeb_stream * stm);
static void audiounit_close_stream(cubeb_stream *stm);
static int audiounit_setup_stream(cubeb_stream *stm);
extern cubeb_ops const audiounit_ops;
@ -61,11 +63,14 @@ struct cubeb {
cubeb_ops const * ops = &audiounit_ops;
owned_critical_section mutex;
std::atomic<int> active_streams{ 0 };
uint32_t global_latency_frames = 0;
cubeb_device_collection_changed_callback collection_changed_callback = nullptr;
void * collection_changed_user_ptr = nullptr;
/* Differentiate input from output devices. */
cubeb_device_type collection_changed_devtype = CUBEB_DEVICE_TYPE_UNKNOWN;
std::vector<AudioObjectID> devtype_device_array;
// The queue is asynchronously deallocated once all references to it are released
dispatch_queue_t serial_queue = dispatch_queue_create(DISPATCH_QUEUE_LABEL, DISPATCH_QUEUE_SERIAL);
};
struct auto_array_wrapper {
@ -133,8 +138,8 @@ struct cubeb_stream {
cubeb_state_callback state_callback = nullptr;
cubeb_device_changed_callback device_changed_callback = nullptr;
/* Stream creation parameters */
cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
bool is_default_input;
AudioDeviceID input_device = 0;
AudioDeviceID output_device = 0;
@ -177,6 +182,7 @@ struct cubeb_stream {
std::atomic<int> output_callback_in_a_row{ 0 };
/* This is true if a device change callback is currently running. */
std::atomic<bool> switching_device{ false };
std::atomic<bool> buffer_size_change_state{ false };
};
bool has_input(cubeb_stream * stm)
@ -227,6 +233,14 @@ audiotimestamp_to_latency(AudioTimeStamp const * tstamp, cubeb_stream * stream)
return ((pres - now) * stream->output_desc.mSampleRate) / 1000000000LL;
}
static void
audiounit_set_global_latency(cubeb_stream * stm, uint32_t latency_frames)
{
stm->mutex.assert_current_thread_owns();
assert(stm->context->active_streams == 1);
stm->context->global_latency_frames = latency_frames;
}
static void
audiounit_make_silent(AudioBuffer * ioData)
{
@ -267,11 +281,12 @@ audiounit_render_input(cubeb_stream * stm,
stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData,
input_frames * stm->input_desc.mChannelsPerFrame);
LOGV("(%p) input: buffers %d, size %d, channels %d, frames %d.",
stm, input_buffer_list.mNumberBuffers,
input_buffer_list.mBuffers[0].mDataByteSize,
input_buffer_list.mBuffers[0].mNumberChannels,
input_frames);
LOGV("(%p) input: buffers %u, size %u, channels %u, frames %d.",
stm,
(unsigned int) input_buffer_list.mNumberBuffers,
(unsigned int) input_buffer_list.mBuffers[0].mDataByteSize,
(unsigned int) input_buffer_list.mBuffers[0].mNumberChannels,
(unsigned int) input_frames);
/* Advance input frame counter. */
assert(input_frames > 0);
@ -289,7 +304,6 @@ audiounit_input_callback(void * user_ptr,
AudioBufferList * /* bufs */)
{
cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr);
long outframes;
assert(stm->input_unit != NULL);
assert(AU_IN_BUS == bus);
@ -323,15 +337,15 @@ audiounit_input_callback(void * user_ptr,
Resampler will deliver input buffer in the correct rate. */
assert(input_frames <= stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame);
long total_input_frames = stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame;
outframes = cubeb_resampler_fill(stm->resampler.get(),
stm->input_linear_buffer->data(),
&total_input_frames,
NULL,
0);
long outframes = cubeb_resampler_fill(stm->resampler.get(),
stm->input_linear_buffer->data(),
&total_input_frames,
NULL,
0);
// Reset input buffer
stm->input_linear_buffer->clear();
if (outframes < 0 || outframes != input_frames) {
if (outframes < 0 || (UInt32) outframes != input_frames) {
stm->shutdown = true;
return noErr;
}
@ -375,12 +389,14 @@ audiounit_output_callback(void * user_ptr,
stm->output_callback_in_a_row++;
LOGV("(%p) output: buffers %d, size %d, channels %d, frames %d.",
stm, outBufferList->mNumberBuffers,
outBufferList->mBuffers[0].mDataByteSize,
outBufferList->mBuffers[0].mNumberChannels, output_frames);
LOGV("(%p) output: buffers %u, size %u, channels %u, frames %u.",
stm,
(unsigned int) outBufferList->mNumberBuffers,
(unsigned int) outBufferList->mBuffers[0].mDataByteSize,
(unsigned int) outBufferList->mBuffers[0].mNumberChannels,
(unsigned int) output_frames);
long outframes = 0, input_frames = 0;
long input_frames = 0;
void * output_buffer = NULL, * input_buffer = NULL;
if (stm->shutdown) {
@ -419,11 +435,11 @@ audiounit_output_callback(void * user_ptr,
}
/* Call user callback through resampler. */
outframes = cubeb_resampler_fill(stm->resampler.get(),
input_buffer,
input_buffer ? &input_frames : NULL,
output_buffer,
output_frames);
long outframes = cubeb_resampler_fill(stm->resampler.get(),
input_buffer,
input_buffer ? &input_frames : NULL,
output_buffer,
output_frames);
if (input_buffer) {
stm->input_linear_buffer->pop(input_frames * stm->input_desc.mChannelsPerFrame);
@ -435,7 +451,7 @@ audiounit_output_callback(void * user_ptr,
}
size_t outbpf = stm->output_desc.mBytesPerFrame;
stm->draining = outframes < output_frames;
stm->draining = (UInt32) outframes < output_frames;
stm->frames_played = stm->frames_queued;
stm->frames_queued += outframes;
@ -533,37 +549,62 @@ audiounit_get_input_device_id(AudioDeviceID * device_id)
return CUBEB_OK;
}
static int
audiounit_reinit_stream(cubeb_stream * stm, bool is_started)
{
if (is_started) {
audiounit_stream_stop_internal(stm);
}
{
auto_lock lock(stm->mutex);
audiounit_close_stream(stm);
if (audiounit_setup_stream(stm) != CUBEB_OK) {
LOG("(%p) Stream reinit failed.", stm);
return CUBEB_ERROR;
}
// Reset input frames to force new stream pre-buffer
// silence if needed, check `is_extra_input_needed()`
stm->frames_read = 0;
// If the stream was running, start it again.
if (is_started) {
audiounit_stream_start_internal(stm);
}
}
return CUBEB_OK;
}
static OSStatus
audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_count,
const AudioObjectPropertyAddress * addresses,
void * user)
{
cubeb_stream * stm = (cubeb_stream*) user;
int rv;
bool was_running = false;
stm->switching_device = true;
// Note if the stream was running or not
was_running = !stm->shutdown;
bool was_running = !stm->shutdown;
LOG("(%p) Audio device changed, %d events.", stm, address_count);
LOG("(%p) Audio device changed, %u events.", stm, (unsigned int) address_count);
for (UInt32 i = 0; i < address_count; i++) {
switch(addresses[i].mSelector) {
case kAudioHardwarePropertyDefaultOutputDevice: {
LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultOutputDevice", i);
LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultOutputDevice", (unsigned int) i);
// Allow restart to choose the new default
stm->output_device = 0;
}
break;
case kAudioHardwarePropertyDefaultInputDevice: {
LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultInputDevice", i);
LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultInputDevice", (unsigned int) i);
// Allow restart to choose the new default
stm->input_device = 0;
}
break;
case kAudioDevicePropertyDeviceIsAlive: {
LOG("Event[%d] - mSelector == kAudioDevicePropertyDeviceIsAlive", i);
LOG("Event[%u] - mSelector == kAudioDevicePropertyDeviceIsAlive", (unsigned int) i);
// If this is the default input device ignore the event,
// kAudioHardwarePropertyDefaultInputDevice will take care of the switch
if (stm->is_default_input) {
@ -575,7 +616,7 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun
}
break;
case kAudioDevicePropertyDataSource:
LOG("Event[%d] - mSelector == kAudioHardwarePropertyDataSource", i);
LOG("Event[%u] - mSelector == kAudioHardwarePropertyDataSource", (unsigned int) i);
break;
}
}
@ -596,28 +637,15 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun
}
}
// This means the callback won't be called again.
audiounit_stream_stop_internal(stm);
{
auto_lock lock(stm->mutex);
close_audiounit_stream(stm);
rv = setup_audiounit_stream(stm);
if (rv != CUBEB_OK) {
LOG("(%p) Could not reopen a stream after switching.", stm);
// Use a new thread, through the queue, to avoid deadlock when calling
// Get/SetProperties method from inside notify callback
dispatch_async(stm->context->serial_queue, ^() {
if (audiounit_reinit_stream(stm, was_running) != CUBEB_OK) {
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
return noErr;
LOG("(%p) Could not reopen the stream after switching.", stm);
}
stm->frames_read = 0;
// If the stream was running, start it again.
if (was_running) {
audiounit_stream_start_internal(stm);
}
}
stm->switching_device = false;
stm->switching_device = false;
});
return noErr;
}
@ -1099,8 +1127,7 @@ audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity)
}
static uint32_t
audiounit_clamp_latency(cubeb_stream * stm,
uint32_t latency_frames)
audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames)
{
// For the 1st stream set anything within safe min-max
assert(stm->context->active_streams > 0);
@ -1163,16 +1190,364 @@ audiounit_clamp_latency(cubeb_stream * stm,
SAFE_MIN_LATENCY_FRAMES);
}
static int
setup_audiounit_stream(cubeb_stream * stm)
/*
* Change buffer size is prone to deadlock thus we change it
* following the steps:
* - register a listener for the buffer size property
* - change the property
* - wait until the listener is executed
* - property has changed, remove the listener
* */
static void
buffer_size_changed_callback(void * inClientData,
AudioUnit inUnit,
AudioUnitPropertyID inPropertyID,
AudioUnitScope inScope,
AudioUnitElement inElement)
{
stm->mutex.assert_current_thread_owns();
cubeb_stream * stm = (cubeb_stream *)inClientData;
int r;
AudioUnit au = inUnit;
AudioUnitScope au_scope = kAudioUnitScope_Input;
AudioUnitElement au_element = inElement;
const char * au_type = "output";
if (au == stm->input_unit) {
au_scope = kAudioUnitScope_Output;
au_type = "input";
}
switch (inPropertyID) {
case kAudioDevicePropertyBufferFrameSize: {
if (inScope != au_scope) {
break;
}
UInt32 new_buffer_size;
UInt32 outSize = sizeof(UInt32);
OSStatus r = AudioUnitGetProperty(au,
kAudioDevicePropertyBufferFrameSize,
au_scope,
au_element,
&new_buffer_size,
&outSize);
if (r != noErr) {
LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: Cannot get current buffer size", stm);
} else {
LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: New %s buffer size = %d for scope %d", stm,
au_type, new_buffer_size, inScope);
}
stm->buffer_size_change_state = true;
break;
}
}
}
enum set_buffer_size_side {
INPUT,
OUTPUT,
};
static int
audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buffer_size_side set_side)
{
AudioUnit au = stm->output_unit;
AudioUnitScope au_scope = kAudioUnitScope_Input;
AudioUnitElement au_element = AU_OUT_BUS;
const char * au_type = "output";
if (set_side == INPUT) {
au = stm->input_unit;
au_scope = kAudioUnitScope_Output;
au_element = AU_IN_BUS;
au_type = "input";
}
uint32_t buffer_frames = 0;
UInt32 size = sizeof(buffer_frames);
int r = AudioUnitGetProperty(au,
kAudioDevicePropertyBufferFrameSize,
au_scope,
au_element,
&buffer_frames,
&size);
if (r != noErr) {
if (set_side == INPUT) {
PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize", r);
} else {
PRINT_ERROR_CODE("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize", r);
}
return CUBEB_ERROR;
}
if (new_size_frames == buffer_frames) {
LOG("(%p) No need to update %s buffer size already %u frames", stm, au_type, buffer_frames);
return CUBEB_OK;
}
r = AudioUnitAddPropertyListener(au,
kAudioDevicePropertyBufferFrameSize,
buffer_size_changed_callback,
stm);
if (r != noErr) {
if (set_side == INPUT) {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r);
} else {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r);
}
return CUBEB_ERROR;
}
stm->buffer_size_change_state = false;
r = AudioUnitSetProperty(au,
kAudioDevicePropertyBufferFrameSize,
au_scope,
au_element,
&new_size_frames,
sizeof(new_size_frames));
if (r != noErr) {
if (set_side == INPUT) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioDevicePropertyBufferFrameSize", r);
} else {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioDevicePropertyBufferFrameSize", r);
}
r = AudioUnitRemovePropertyListenerWithUserData(au,
kAudioDevicePropertyBufferFrameSize,
buffer_size_changed_callback,
stm);
if (r != noErr) {
if (set_side == INPUT) {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r);
} else {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r);
}
}
return CUBEB_ERROR;
}
int count = 0;
while (!stm->buffer_size_change_state && count++ < 30) {
struct timespec req, rem;
req.tv_sec = 0;
req.tv_nsec = 100000000L; // 0.1 sec
if (nanosleep(&req , &rem) < 0 ) {
LOG("(%p) Warning: nanosleep call failed or interrupted. Remaining time %ld nano secs \n", stm, rem.tv_nsec);
}
LOG("(%p) audiounit_set_buffer_size : wait count = %d", stm, count);
}
r = AudioUnitRemovePropertyListenerWithUserData(au,
kAudioDevicePropertyBufferFrameSize,
buffer_size_changed_callback,
stm);
if (r != noErr) {
if (set_side == INPUT) {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r);
} else {
PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r);
}
return CUBEB_ERROR;
}
if (!stm->buffer_size_change_state && count >= 30) {
LOG("(%p) Error, did not get buffer size change callback ...", stm);
return CUBEB_ERROR;
}
LOG("(%p) %s buffer size changed to %u frames.", stm, au_type, new_size_frames);
return CUBEB_OK;
}
static int
audiounit_configure_input(cubeb_stream * stm)
{
int r = 0;
UInt32 size;
AURenderCallbackStruct aurcbs_in;
LOG("(%p) Opening input side: rate %u, channels %u, format %d, latency in frames %u.",
stm, stm->input_stream_params.rate, stm->input_stream_params.channels,
stm->input_stream_params.format, stm->latency_frames);
/* Get input device sample rate. */
AudioStreamBasicDescription input_hw_desc;
size = sizeof(AudioStreamBasicDescription);
r = AudioUnitGetProperty(stm->input_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
AU_IN_BUS,
&input_hw_desc,
&size);
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
stm->input_hw_rate = input_hw_desc.mSampleRate;
LOG("(%p) Input device sampling rate: %.2f", stm, stm->input_hw_rate);
/* Set format description according to the input params. */
r = audio_stream_desc_init(&stm->input_desc, &stm->input_stream_params);
if (r != CUBEB_OK) {
LOG("(%p) Setting format description for input failed.", stm);
return r;
}
// Use latency to set buffer size
stm->input_buffer_frames = stm->latency_frames;
r = audiounit_set_buffer_size(stm, stm->input_buffer_frames, INPUT);
if (r != CUBEB_OK) {
LOG("(%p) Error in change input buffer size.", stm);
return CUBEB_ERROR;
}
AudioStreamBasicDescription src_desc = stm->input_desc;
/* Input AudioUnit must be configured with device's sample rate.
we will resample inside input callback. */
src_desc.mSampleRate = stm->input_hw_rate;
r = AudioUnitSetProperty(stm->input_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
AU_IN_BUS,
&src_desc,
sizeof(AudioStreamBasicDescription));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
/* Frames per buffer in the input callback. */
r = AudioUnitSetProperty(stm->input_unit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
AU_IN_BUS,
&stm->input_buffer_frames,
sizeof(UInt32));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice", r);
return CUBEB_ERROR;
}
// Input only capacity
unsigned int array_capacity = 1;
if (has_output(stm)) {
// Full-duplex increase capacity
array_capacity = 8;
}
if (audiounit_init_input_linear_buffer(stm, array_capacity) != CUBEB_OK) {
return CUBEB_ERROR;
}
assert(stm->input_unit != NULL);
aurcbs_in.inputProc = audiounit_input_callback;
aurcbs_in.inputProcRefCon = stm;
r = AudioUnitSetProperty(stm->input_unit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
AU_OUT_BUS,
&aurcbs_in,
sizeof(aurcbs_in));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback", r);
return CUBEB_ERROR;
}
LOG("(%p) Input audiounit init successfully.", stm);
return CUBEB_OK;
}
static int
audiounit_configure_output(cubeb_stream * stm)
{
int r;
AURenderCallbackStruct aurcbs_out;
UInt32 size;
LOG("(%p) Opening output side: rate %u, channels %u, format %d, latency in frames %u.",
stm, stm->output_stream_params.rate, stm->output_stream_params.channels,
stm->output_stream_params.format, stm->latency_frames);
r = audio_stream_desc_init(&stm->output_desc, &stm->output_stream_params);
if (r != CUBEB_OK) {
LOG("(%p) Could not initialize the audio stream description.", stm);
return r;
}
/* Get output device sample rate. */
AudioStreamBasicDescription output_hw_desc;
size = sizeof(AudioStreamBasicDescription);
memset(&output_hw_desc, 0, size);
r = AudioUnitGetProperty(stm->output_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
AU_OUT_BUS,
&output_hw_desc,
&size);
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitGetProperty/output/tkAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
stm->output_hw_rate = output_hw_desc.mSampleRate;
LOG("(%p) Output device sampling rate: %.2f", stm, output_hw_desc.mSampleRate);
r = AudioUnitSetProperty(stm->output_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
AU_OUT_BUS,
&stm->output_desc,
sizeof(AudioStreamBasicDescription));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
r = audiounit_set_buffer_size(stm, stm->latency_frames, OUTPUT);
if (r != CUBEB_OK) {
LOG("(%p) Error in change output buffer size.", stm);
return CUBEB_ERROR;
}
/* Frames per buffer in the input callback. */
r = AudioUnitSetProperty(stm->output_unit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
AU_OUT_BUS,
&stm->latency_frames,
sizeof(UInt32));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice", r);
return CUBEB_ERROR;
}
assert(stm->output_unit != NULL);
aurcbs_out.inputProc = audiounit_output_callback;
aurcbs_out.inputProcRefCon = stm;
r = AudioUnitSetProperty(stm->output_unit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
AU_OUT_BUS,
&aurcbs_out,
sizeof(aurcbs_out));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback", r);
return CUBEB_ERROR;
}
LOG("(%p) Output audiounit init successfully.", stm);
return CUBEB_OK;
}
static int
audiounit_setup_stream(cubeb_stream * stm)
{
stm->mutex.assert_current_thread_owns();
int r = 0;
if (has_input(stm)) {
r = audiounit_create_unit(&stm->input_unit, true,
&stm->input_stream_params,
@ -1193,170 +1568,36 @@ setup_audiounit_stream(cubeb_stream * stm)
}
}
/* Latency cannot change if another stream is operating in parallel. In this case
* latecy is set to the other stream value. */
if (stm->context->active_streams > 1) {
LOG("(%p) More than one active stream, use global latency.", stm);
stm->latency_frames = stm->context->global_latency_frames;
} else {
/* Silently clamp the latency down to the platform default, because we
* synthetize the clock from the callbacks, and we want the clock to update
* often. */
stm->latency_frames = audiounit_clamp_latency(stm, stm->latency_frames);
assert(stm->latency_frames); // Ungly error check
audiounit_set_global_latency(stm, stm->latency_frames);
}
/* Setup Input Stream! */
if (has_input(stm)) {
LOG("(%p) Opening input side: rate %u, channels %u, format %d, latency in frames %u.",
stm, stm->input_stream_params.rate, stm->input_stream_params.channels,
stm->input_stream_params.format, stm->latency_frames);
/* Get input device sample rate. */
AudioStreamBasicDescription input_hw_desc;
size = sizeof(AudioStreamBasicDescription);
r = AudioUnitGetProperty(stm->input_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
AU_IN_BUS,
&input_hw_desc,
&size);
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
stm->input_hw_rate = input_hw_desc.mSampleRate;
LOG("(%p) Input device sampling rate: %.2f", stm, stm->input_hw_rate);
/* Set format description according to the input params. */
r = audio_stream_desc_init(&stm->input_desc, &stm->input_stream_params);
r = audiounit_configure_input(stm);
if (r != CUBEB_OK) {
LOG("(%p) Setting format description for input failed.", stm);
LOG("(%p) Configure audiounit input failed.", stm);
return r;
}
// Use latency to set buffer size
stm->input_buffer_frames = stm->latency_frames;
LOG("(%p) Input buffer frame count %u.", stm, unsigned(stm->input_buffer_frames));
r = AudioUnitSetProperty(stm->input_unit,
kAudioDevicePropertyBufferFrameSize,
kAudioUnitScope_Output,
AU_IN_BUS,
&stm->input_buffer_frames,
sizeof(UInt32));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioDevicePropertyBufferFrameSize", r);
return CUBEB_ERROR;
}
AudioStreamBasicDescription src_desc = stm->input_desc;
/* Input AudioUnit must be configured with device's sample rate.
we will resample inside input callback. */
src_desc.mSampleRate = stm->input_hw_rate;
r = AudioUnitSetProperty(stm->input_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
AU_IN_BUS,
&src_desc,
sizeof(AudioStreamBasicDescription));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
/* Frames per buffer in the input callback. */
r = AudioUnitSetProperty(stm->input_unit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Output,
AU_IN_BUS,
&stm->input_buffer_frames,
sizeof(UInt32));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice", r);
return CUBEB_ERROR;
}
// Input only capacity
unsigned int array_capacity = 1;
if (has_output(stm)) {
// Full-duplex increase capacity
array_capacity = 8;
}
if (audiounit_init_input_linear_buffer(stm, array_capacity) != CUBEB_OK) {
return CUBEB_ERROR;
}
assert(stm->input_unit != NULL);
aurcbs_in.inputProc = audiounit_input_callback;
aurcbs_in.inputProcRefCon = stm;
r = AudioUnitSetProperty(stm->input_unit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
AU_OUT_BUS,
&aurcbs_in,
sizeof(aurcbs_in));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback", r);
return CUBEB_ERROR;
}
LOG("(%p) Input audiounit init successfully.", stm);
}
/* Setup Output Stream! */
if (has_output(stm)) {
LOG("(%p) Opening output side: rate %u, channels %u, format %d, latency in frames %u.",
stm, stm->output_stream_params.rate, stm->output_stream_params.channels,
stm->output_stream_params.format, stm->latency_frames);
r = audio_stream_desc_init(&stm->output_desc, &stm->output_stream_params);
r = audiounit_configure_output(stm);
if (r != CUBEB_OK) {
LOG("(%p) Could not initialize the audio stream description.", stm);
LOG("(%p) Configure audiounit output failed.", stm);
return r;
}
/* Get output device sample rate. */
AudioStreamBasicDescription output_hw_desc;
size = sizeof(AudioStreamBasicDescription);
memset(&output_hw_desc, 0, size);
r = AudioUnitGetProperty(stm->output_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
AU_OUT_BUS,
&output_hw_desc,
&size);
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitGetProperty/output/tkAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
stm->output_hw_rate = output_hw_desc.mSampleRate;
LOG("(%p) Output device sampling rate: %.2f", stm, output_hw_desc.mSampleRate);
r = AudioUnitSetProperty(stm->output_unit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
AU_OUT_BUS,
&stm->output_desc,
sizeof(AudioStreamBasicDescription));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat", r);
return CUBEB_ERROR;
}
// Use latency to calculate buffer size
uint32_t output_buffer_frames = stm->latency_frames;
LOG("(%p) Output buffer frame count %u.", stm, output_buffer_frames);
r = AudioUnitSetProperty(stm->output_unit,
kAudioDevicePropertyBufferFrameSize,
kAudioUnitScope_Input,
AU_OUT_BUS,
&output_buffer_frames,
sizeof(output_buffer_frames));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioDevicePropertyBufferFrameSize", r);
return CUBEB_ERROR;
}
assert(stm->output_unit != NULL);
aurcbs_out.inputProc = audiounit_output_callback;
aurcbs_out.inputProcRefCon = stm;
r = AudioUnitSetProperty(stm->output_unit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
AU_OUT_BUS,
&aurcbs_out,
sizeof(aurcbs_out));
if (r != noErr) {
PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback", r);
return CUBEB_ERROR;
}
LOG("(%p) Output audiounit init successfully.", stm);
}
// Setting the latency doesn't work well for USB headsets (eg. plantronics).
@ -1486,14 +1727,12 @@ audiounit_stream_init(cubeb * context,
assert(context);
*stream = NULL;
assert(latency_frames > 0);
if ((input_device && !input_stream_params) ||
(output_device && !output_stream_params)) {
return CUBEB_ERROR_INVALID_PARAMETER;
}
context->active_streams += 1;
stm.reset(new cubeb_stream(context));
/* These could be different in the future if we have both
@ -1501,6 +1740,7 @@ audiounit_stream_init(cubeb * context,
stm->data_callback = data_callback;
stm->state_callback = state_callback;
stm->user_ptr = user_ptr;
stm->latency_frames = latency_frames;
if (input_stream_params) {
stm->input_stream_params = *input_stream_params;
stm->input_device = reinterpret_cast<uintptr_t>(input_device);
@ -1512,18 +1752,14 @@ audiounit_stream_init(cubeb * context,
stm->output_device = reinterpret_cast<uintptr_t>(output_device);
}
/* Silently clamp the latency down to the platform default, because we
* synthetize the clock from the callbacks, and we want the clock to update
* often. */
stm->latency_frames = audiounit_clamp_latency(stm.get(), latency_frames);
assert(latency_frames > 0);
auto_lock context_lock(context->mutex);
{
// It's not critical to lock here, because no other thread has been started
// yet, but it allows to assert that the lock has been taken in
// `setup_audiounit_stream`.
// `audiounit_setup_stream`.
context->active_streams += 1;
auto_lock lock(stm->mutex);
r = setup_audiounit_stream(stm.get());
r = audiounit_setup_stream(stm.get());
}
if (r != CUBEB_OK) {
@ -1543,7 +1779,7 @@ audiounit_stream_init(cubeb * context,
}
static void
close_audiounit_stream(cubeb_stream * stm)
audiounit_close_stream(cubeb_stream *stm)
{
stm->mutex.assert_current_thread_owns();
if (stm->input_unit) {
@ -1566,11 +1802,12 @@ audiounit_stream_destroy(cubeb_stream * stm)
{
stm->shutdown = true;
auto_lock context_locl(stm->context->mutex);
audiounit_stream_stop_internal(stm);
{
auto_lock lock(stm->mutex);
close_audiounit_stream(stm);
audiounit_close_stream(stm);
}
#if !TARGET_OS_IPHONE
@ -1583,6 +1820,7 @@ audiounit_stream_destroy(cubeb_stream * stm)
assert(stm->context->active_streams >= 1);
stm->context->active_streams -= 1;
LOG("Cubeb stream (%p) destroyed successful.", stm);
delete stm;
}
@ -1606,6 +1844,7 @@ audiounit_stream_start(cubeb_stream * stm)
stm->shutdown = false;
stm->draining = false;
auto_lock context_locl(stm->context->mutex);
audiounit_stream_start_internal(stm);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
@ -1633,6 +1872,7 @@ audiounit_stream_stop(cubeb_stream * stm)
{
stm->shutdown = true;
auto_lock context_locl(stm->context->mutex);
audiounit_stream_stop_internal(stm);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
@ -2294,6 +2534,7 @@ cubeb_ops const audiounit_ops = {
/*.get_max_channel_count =*/ audiounit_get_max_channel_count,
/*.get_min_latency =*/ audiounit_get_min_latency,
/*.get_preferred_sample_rate =*/ audiounit_get_preferred_sample_rate,
/*.get_preferred_channel_layout =*/ nullptr,
/*.enumerate_devices =*/ audiounit_enumerate_devices,
/*.destroy =*/ audiounit_destroy,
/*.stream_init =*/ audiounit_stream_init,

View File

@ -114,6 +114,7 @@ static struct cubeb_ops const cbjack_ops = {
.get_max_channel_count = cbjack_get_max_channel_count,
.get_min_latency = cbjack_get_min_latency,
.get_preferred_sample_rate = cbjack_get_preferred_sample_rate,
.get_preferred_channel_layout = NULL,
.enumerate_devices = cbjack_enumerate_devices,
.destroy = cbjack_destroy,
.stream_init = cbjack_stream_init,
@ -420,7 +421,6 @@ cbjack_process(jack_nframes_t nframes, void * arg)
return 0;
}
static void
cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, float ** in, float ** bufs_out, jack_nframes_t nframes)
{
@ -433,7 +433,6 @@ cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, float ** in, fl
long done_frames = 0;
long input_frames_count = (in != NULL) ? nframes : 0;
done_frames = cubeb_resampler_fill(stream->resampler,
inptr,
&input_frames_count,
@ -934,7 +933,6 @@ cbjack_stream_set_volume(cubeb_stream * stm, float volume)
return CUBEB_OK;
}
static int
cbjack_stream_get_current_device(cubeb_stream * stm, cubeb_device ** const device)
{

View File

@ -0,0 +1,348 @@
/*
* Copyright © 2016 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#include <cassert>
#include "cubeb-internal.h"
#include "cubeb_mixer.h"
cubeb_layout_map const CUBEB_CHANNEL_LAYOUT_MAPS[CUBEB_LAYOUT_MAX] = {
{ "undefined", 0, CUBEB_LAYOUT_UNDEFINED },
{ "dual mono", 2, CUBEB_LAYOUT_DUAL_MONO },
{ "dual mono lfe", 3, CUBEB_LAYOUT_DUAL_MONO_LFE },
{ "mono", 1, CUBEB_LAYOUT_MONO },
{ "mono lfe", 2, CUBEB_LAYOUT_MONO_LFE },
{ "stereo", 2, CUBEB_LAYOUT_STEREO },
{ "stereo lfe", 3, CUBEB_LAYOUT_STEREO_LFE },
{ "3f", 3, CUBEB_LAYOUT_3F },
{ "3f lfe", 4, CUBEB_LAYOUT_3F_LFE },
{ "2f1", 3, CUBEB_LAYOUT_2F1 },
{ "2f1 lfe", 4, CUBEB_LAYOUT_2F1_LFE },
{ "3f1", 4, CUBEB_LAYOUT_3F1 },
{ "3f1 lfe", 5, CUBEB_LAYOUT_3F1_LFE },
{ "2f2", 4, CUBEB_LAYOUT_2F2 },
{ "2f2 lfe", 5, CUBEB_LAYOUT_2F2_LFE },
{ "3f2", 5, CUBEB_LAYOUT_3F2 },
{ "3f2 lfe", 6, CUBEB_LAYOUT_3F2_LFE },
{ "3f3r lfe", 7, CUBEB_LAYOUT_3F3R_LFE },
{ "3f4 lfe", 8, CUBEB_LAYOUT_3F4_LFE }
};
static int const CHANNEL_ORDER_TO_INDEX[CUBEB_LAYOUT_MAX][CHANNEL_MAX] = {
// M | L | R | C | LS | RS | RLS | RC | RRS | LFE
{ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // UNDEFINED
{ -1, 0, 1, -1, -1, -1, -1, -1, -1, -1 }, // DUAL_MONO
{ -1, 0, 1, -1, -1, -1, -1, -1, -1, 2 }, // DUAL_MONO_LFE
{ 0, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // MONO
{ 0, -1, -1, -1, -1, -1, -1, -1, -1, 1 }, // MONO_LFE
{ -1, 0, 1, -1, -1, -1, -1, -1, -1, -1 }, // STEREO
{ -1, 0, 1, -1, -1, -1, -1, -1, -1, 2 }, // STEREO_LFE
{ -1, 0, 1, 2, -1, -1, -1, -1, -1, -1 }, // 3F
{ -1, 0, 1, 2, -1, -1, -1, -1, -1, 3 }, // 3F_LFE
{ -1, 0, 1, -1, -1, -1, -1, 2, -1, -1 }, // 2F1
{ -1, 0, 1, -1, -1, -1, -1, 3, -1, 2 }, // 2F1_LFE
{ -1, 0, 1, 2, -1, -1, -1, 3, -1, -1 }, // 3F1
{ -1, 0, 1, 2, -1, -1, -1, 4, -1, 3 }, // 3F1_LFE
{ -1, 0, 1, -1, 2, 3, -1, -1, -1, -1 }, // 2F2
{ -1, 0, 1, -1, 3, 4, -1, -1, -1, 2 }, // 2F2_LFE
{ -1, 0, 1, 2, 3, 4, -1, -1, -1, -1 }, // 3F2
{ -1, 0, 1, 2, 4, 5, -1, -1, -1, 3 }, // 3F2_LFE
{ -1, 0, 1, 2, 5, 6, -1, 4, -1, 3 }, // 3F3R_LFE
{ -1, 0, 1, 2, 6, 7, 4, -1, 5, 3 }, // 3F4_LFE
};
// The downmix matrix from TABLE 2 in the ITU-R BS.775-3[1] defines a way to
// convert 3F2 input data to 1F, 2F, 3F, 2F1, 3F1, 2F2 output data. We extend it
// to convert 3F2-LFE input data to 1F, 2F, 3F, 2F1, 3F1, 2F2 and their LFEs
// output data.
// [1] https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.775-3-201208-I!!PDF-E.pdf
// Number of converted layouts: 1F, 2F, 3F, 2F1, 3F1, 2F2 and their LFEs.
unsigned int const SUPPORTED_LAYOUT_NUM = 12;
// Number of input channel for downmix conversion.
unsigned int const INPUT_CHANNEL_NUM = 6; // 3F2-LFE
// Max number of possible output channels.
unsigned int const MAX_OUTPUT_CHANNEL_NUM = 5; // 2F2-LFE or 3F1-LFE
float const INV_SQRT_2 = 0.707106f; // 1/sqrt(2)
// Each array contains coefficients that will be multiplied with
// { L, R, C, LFE, LS, RS } channels respectively.
static float const DOWNMIX_MATRIX_3F2_LFE[SUPPORTED_LAYOUT_NUM][MAX_OUTPUT_CHANNEL_NUM][INPUT_CHANNEL_NUM] =
{
// 1F Mono
{
{ INV_SQRT_2, INV_SQRT_2, 1, 0, 0.5, 0.5 }, // M
},
// 1F Mono-LFE
{
{ INV_SQRT_2, INV_SQRT_2, 1, 0, 0.5, 0.5 }, // M
{ 0, 0, 0, 1, 0, 0 } // LFE
},
// 2F Stereo
{
{ 1, 0, INV_SQRT_2, 0, INV_SQRT_2, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, INV_SQRT_2 } // R
},
// 2F Stereo-LFE
{
{ 1, 0, INV_SQRT_2, 0, INV_SQRT_2, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, INV_SQRT_2 }, // R
{ 0, 0, 0, 1, 0, 0 } // LFE
},
// 3F
{
{ 1, 0, 0, 0, INV_SQRT_2, 0 }, // L
{ 0, 1, 0, 0, 0, INV_SQRT_2 }, // R
{ 0, 0, 1, 0, 0, 0 } // C
},
// 3F-LFE
{
{ 1, 0, 0, 0, INV_SQRT_2, 0 }, // L
{ 0, 1, 0, 0, 0, INV_SQRT_2 }, // R
{ 0, 0, 1, 0, 0, 0 }, // C
{ 0, 0, 0, 1, 0, 0 } // LFE
},
// 2F1
{
{ 1, 0, INV_SQRT_2, 0, 0, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, 0 }, // R
{ 0, 0, 0, 0, INV_SQRT_2, INV_SQRT_2 } // S
},
// 2F1-LFE
{
{ 1, 0, INV_SQRT_2, 0, 0, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, 0 }, // R
{ 0, 0, 0, 1, 0, 0 }, // LFE
{ 0, 0, 0, 0, INV_SQRT_2, INV_SQRT_2 } // S
},
// 3F1
{
{ 1, 0, 0, 0, 0, 0 }, // L
{ 0, 1, 0, 0, 0, 0 }, // R
{ 0, 0, 1, 0, 0, 0 }, // C
{ 0, 0, 0, 0, INV_SQRT_2, INV_SQRT_2 } // S
},
// 3F1-LFE
{
{ 1, 0, 0, 0, 0, 0 }, // L
{ 0, 1, 0, 0, 0, 0 }, // R
{ 0, 0, 1, 0, 0, 0 }, // C
{ 0, 0, 0, 1, 0, 0 }, // LFE
{ 0, 0, 0, 0, INV_SQRT_2, INV_SQRT_2 } // S
},
// 2F2
{
{ 1, 0, INV_SQRT_2, 0, 0, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, 0 }, // R
{ 0, 0, 0, 0, 1, 0 }, // LS
{ 0, 0, 0, 0, 0, 1 } // RS
},
// 2F2-LFE
{
{ 1, 0, INV_SQRT_2, 0, 0, 0 }, // L
{ 0, 1, INV_SQRT_2, 0, 0, 0 }, // R
{ 0, 0, 0, 1, 0, 0 }, // LFE
{ 0, 0, 0, 0, 1, 0 }, // LS
{ 0, 0, 0, 0, 0, 1 } // RS
}
};
/* Convert audio data from 3F2(-LFE) to 1F, 2F, 3F, 2F1, 3F1, 2F2 and their LFEs. */
template<typename T>
bool
downmix_3f2(T const * const in, unsigned long inframes, T * out, cubeb_channel_layout in_layout, cubeb_channel_layout out_layout)
{
if ((in_layout != CUBEB_LAYOUT_3F2 && in_layout != CUBEB_LAYOUT_3F2_LFE) ||
out_layout < CUBEB_LAYOUT_MONO || out_layout > CUBEB_LAYOUT_2F2_LFE) {
return false;
}
unsigned int in_channels = CUBEB_CHANNEL_LAYOUT_MAPS[in_layout].channels;
unsigned int out_channels = CUBEB_CHANNEL_LAYOUT_MAPS[out_layout].channels;
// Conversion from 3F2 to 2F2-LFE or 3F1-LFE is allowed, so we use '<=' instead of '<'.
assert(out_channels <= in_channels);
long out_index = 0;
auto & downmix_matrix = DOWNMIX_MATRIX_3F2_LFE[out_layout - CUBEB_LAYOUT_MONO]; // The matrix is started from mono.
for (unsigned long i = 0; i < inframes * in_channels; i += in_channels) {
for (unsigned int j = 0; j < out_channels; ++j) {
out[out_index + j] = 0; // Clear its value.
for (unsigned int k = 0 ; k < INPUT_CHANNEL_NUM ; ++k) {
// 3F2-LFE has 6 channels: L, R, C, LFE, LS, RS, while 3F2 has only 5
// channels: L, R, C, LS, RS. Thus, we need to append 0 to LFE(index 3)
// to simulate a 3F2-LFE data when input layout is 3F2.
T data = (in_layout == CUBEB_LAYOUT_3F2_LFE) ? in[i + k] : (k == 3) ? 0 : in[i + ((k < 3) ? k : k - 1)];
out[out_index + j] += downmix_matrix[j][k] * data;
}
}
out_index += out_channels;
}
return true;
}
/* Map the audio data by channel name. */
template<class T>
bool
mix_remap(T const * const in, unsigned long inframes, T * out, cubeb_channel_layout in_layout, cubeb_channel_layout out_layout) {
assert(in_layout != out_layout);
unsigned int in_channels = CUBEB_CHANNEL_LAYOUT_MAPS[in_layout].channels;
unsigned int out_channels = CUBEB_CHANNEL_LAYOUT_MAPS[out_layout].channels;
uint32_t in_layout_mask = 0;
for (unsigned int i = 0 ; i < in_channels ; ++i) {
in_layout_mask |= 1 << CHANNEL_INDEX_TO_ORDER[in_layout][i];
}
uint32_t out_layout_mask = 0;
for (unsigned int i = 0 ; i < out_channels ; ++i) {
out_layout_mask |= 1 << CHANNEL_INDEX_TO_ORDER[out_layout][i];
}
// If there is no matched channel, then do nothing.
if (!(out_layout_mask & in_layout_mask)) {
return false;
}
long out_index = 0;
for (unsigned long i = 0; i < inframes * in_channels; i += in_channels) {
for (unsigned int j = 0; j < out_channels; ++j) {
cubeb_channel channel = CHANNEL_INDEX_TO_ORDER[out_layout][j];
uint32_t channel_mask = 1 << channel;
int channel_index = CHANNEL_ORDER_TO_INDEX[in_layout][channel];
if (in_layout_mask & channel_mask) {
assert(channel_index != -1);
out[out_index + j] = in[i + channel_index];
} else {
assert(channel_index == -1);
out[out_index + j] = 0;
}
}
out_index += out_channels;
}
return true;
}
/* Drop the extra channels beyond the provided output channels. */
template<typename T>
void
downmix_fallback(T const * const in, unsigned long inframes, T * out, unsigned int in_channels, unsigned int out_channels)
{
assert(in_channels >= out_channels);
long out_index = 0;
for (unsigned long i = 0; i < inframes * in_channels; i += in_channels) {
for (unsigned int j = 0; j < out_channels; ++j) {
out[out_index + j] = in[i + j];
}
out_index += out_channels;
}
}
template<typename T>
void
cubeb_downmix(T const * const in, long inframes, T * out,
unsigned int in_channels, unsigned int out_channels,
cubeb_channel_layout in_layout, cubeb_channel_layout out_layout)
{
assert(in_channels >= out_channels && in_layout != CUBEB_LAYOUT_UNDEFINED);
// If the channel number is different from the layout's setting or it's not a
// valid audio 5.1 downmix, then we use fallback downmix mechanism.
if (out_channels == CUBEB_CHANNEL_LAYOUT_MAPS[out_layout].channels &&
in_channels == CUBEB_CHANNEL_LAYOUT_MAPS[in_layout].channels) {
if (downmix_3f2(in, inframes, out, in_layout, out_layout)) {
return;
}
if (mix_remap(in, inframes, out, in_layout, out_layout)) {
return;
}
}
downmix_fallback(in, inframes, out, in_channels, out_channels);
}
/* Upmix function, copies a mono channel into L and R. */
template<typename T>
void
mono_to_stereo(T const * in, long insamples, T * out, unsigned int out_channels)
{
for (long i = 0, j = 0; i < insamples; ++i, j += out_channels) {
out[j] = out[j + 1] = in[i];
}
}
template<typename T>
void
cubeb_upmix(T const * in, long inframes, T * out,
unsigned int in_channels, unsigned int out_channels)
{
assert(out_channels >= in_channels && in_channels > 0);
/* Either way, if we have 2 or more channels, the first two are L and R. */
/* If we are playing a mono stream over stereo speakers, copy the data over. */
if (in_channels == 1 && out_channels >= 2) {
mono_to_stereo(in, inframes, out, out_channels);
} else {
/* Copy through. */
for (unsigned int i = 0, o = 0; i < inframes * in_channels;
i += in_channels, o += out_channels) {
for (unsigned int j = 0; j < in_channels; ++j) {
out[o + j] = in[i + j];
}
}
}
/* Check if more channels. */
if (out_channels <= 2) {
return;
}
/* Put silence in remaining channels. */
for (long i = 0, o = 0; i < inframes; ++i, o += out_channels) {
for (unsigned int j = 2; j < out_channels; ++j) {
out[o + j] = 0.0;
}
}
}
bool
cubeb_should_upmix(cubeb_stream_params const * stream, cubeb_stream_params const * mixer)
{
return mixer->channels > stream->channels;
}
bool
cubeb_should_downmix(cubeb_stream_params const * stream, cubeb_stream_params const * mixer)
{
if (mixer->channels > stream->channels || mixer->layout == stream->layout) {
return false;
}
return mixer->channels < stream->channels ||
// When mixer.channels == stream.channels
mixer->layout == CUBEB_LAYOUT_UNDEFINED || // fallback downmix
(stream->layout == CUBEB_LAYOUT_3F2 && // 3f2 downmix
(mixer->layout == CUBEB_LAYOUT_2F2_LFE ||
mixer->layout == CUBEB_LAYOUT_3F1_LFE));
}
void
cubeb_downmix_float(float * const in, long inframes, float * out,
unsigned int in_channels, unsigned int out_channels,
cubeb_channel_layout in_layout, cubeb_channel_layout out_layout)
{
cubeb_downmix(in, inframes, out, in_channels, out_channels, in_layout, out_layout);
}
void
cubeb_upmix_float(float * const in, long inframes, float * out,
unsigned int in_channels, unsigned int out_channels)
{
cubeb_upmix(in, inframes, out, in_channels, out_channels);
}

View File

@ -0,0 +1,70 @@
/*
* Copyright © 2016 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#ifndef CUBEB_MIXER
#define CUBEB_MIXER
#include "cubeb/cubeb.h" // for cubeb_channel_layout ,CUBEB_CHANNEL_LAYOUT_MAPS and cubeb_stream_params.
#include <stdbool.h>
#if defined(__cplusplus)
extern "C" {
#endif
typedef enum {
CHANNEL_INVALID = -1,
CHANNEL_MONO = 0,
CHANNEL_LEFT,
CHANNEL_RIGHT,
CHANNEL_CENTER,
CHANNEL_LS,
CHANNEL_RS,
CHANNEL_RLS,
CHANNEL_RCENTER,
CHANNEL_RRS,
CHANNEL_LFE,
CHANNEL_MAX // Max number of supported channels.
} cubeb_channel;
static cubeb_channel const CHANNEL_INDEX_TO_ORDER[CUBEB_LAYOUT_MAX][CHANNEL_MAX] = {
{ CHANNEL_INVALID }, // UNDEFINED
{ CHANNEL_LEFT, CHANNEL_RIGHT }, // DUAL_MONO
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_LFE }, // DUAL_MONO_LFE
{ CHANNEL_MONO }, // MONO
{ CHANNEL_MONO, CHANNEL_LFE }, // MONO_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT }, // STEREO
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_LFE }, // STEREO_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER }, // 3F
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE }, // 3F_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_RCENTER }, // 2F1
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_LFE, CHANNEL_RCENTER }, // 2F1_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_RCENTER }, // 3F1
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_RCENTER }, // 3F1_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_LS, CHANNEL_RS }, // 2F2
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_LFE, CHANNEL_LS, CHANNEL_RS }, // 2F2_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LS, CHANNEL_RS }, // 3F2
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_LS, CHANNEL_RS }, // 3F2_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_RCENTER, CHANNEL_LS, CHANNEL_RS }, // 3F3R_LFE
{ CHANNEL_LEFT, CHANNEL_RIGHT, CHANNEL_CENTER, CHANNEL_LFE, CHANNEL_RLS, CHANNEL_RRS, CHANNEL_LS, CHANNEL_RS } // 3F4_LFE
};
bool cubeb_should_upmix(cubeb_stream_params const * stream, cubeb_stream_params const * mixer);
bool cubeb_should_downmix(cubeb_stream_params const * stream, cubeb_stream_params const * mixer);
void cubeb_downmix_float(float * const in, long inframes, float * out,
unsigned int in_channels, unsigned int out_channels,
cubeb_channel_layout in_layout, cubeb_channel_layout out_layout);
void cubeb_upmix_float(float * const in, long inframes, float * out,
unsigned int in_channels, unsigned int out_channels);
#if defined(__cplusplus)
}
#endif
#endif // CUBEB_MIXER

View File

@ -1740,6 +1740,7 @@ static struct cubeb_ops const opensl_ops = {
.get_max_channel_count = opensl_get_max_channel_count,
.get_min_latency = opensl_get_min_latency,
.get_preferred_sample_rate = opensl_get_preferred_sample_rate,
.get_preferred_channel_layout = NULL,
.enumerate_devices = NULL,
.destroy = opensl_destroy,
.stream_init = opensl_stream_init,

View File

@ -12,6 +12,7 @@
#include <string.h>
#include "cubeb/cubeb.h"
#include "cubeb-internal.h"
#include "cubeb_mixer.h"
#include <stdio.h>
#ifdef DISABLE_LIBPULSE_DLOPEN
@ -20,7 +21,7 @@
#define WRAP(x) cubeb_##x
#define LIBPULSE_API_VISIT(X) \
X(pa_channel_map_can_balance) \
X(pa_channel_map_init_auto) \
X(pa_channel_map_init) \
X(pa_context_connect) \
X(pa_context_disconnect) \
X(pa_context_drain) \
@ -468,6 +469,113 @@ stream_update_timing_info(cubeb_stream * stm)
return r;
}
static pa_channel_position_t
cubeb_channel_to_pa_channel(cubeb_channel channel)
{
assert(channel != CHANNEL_INVALID);
// This variable may be used for multiple times, so we should avoid to
// allocate it in stack, or it will be created and removed repeatedly.
// Use static to allocate this local variable in data space instead of stack.
static pa_channel_position_t map[CHANNEL_MAX] = {
// PA_CHANNEL_POSITION_INVALID, // CHANNEL_INVALID
PA_CHANNEL_POSITION_MONO, // CHANNEL_MONO
PA_CHANNEL_POSITION_FRONT_LEFT, // CHANNEL_LEFT
PA_CHANNEL_POSITION_FRONT_RIGHT, // CHANNEL_RIGHT
PA_CHANNEL_POSITION_FRONT_CENTER, // CHANNEL_CENTER
PA_CHANNEL_POSITION_SIDE_LEFT, // CHANNEL_LS
PA_CHANNEL_POSITION_SIDE_RIGHT, // CHANNEL_RS
PA_CHANNEL_POSITION_REAR_LEFT, // CHANNEL_RLS
PA_CHANNEL_POSITION_REAR_CENTER, // CHANNEL_RCENTER
PA_CHANNEL_POSITION_REAR_RIGHT, // CHANNEL_RRS
PA_CHANNEL_POSITION_LFE // CHANNEL_LFE
};
return map[channel];
}
static cubeb_channel
pa_channel_to_cubeb_channel(pa_channel_position_t channel)
{
assert(channel != PA_CHANNEL_POSITION_INVALID);
switch(channel) {
case PA_CHANNEL_POSITION_MONO: return CHANNEL_MONO;
case PA_CHANNEL_POSITION_FRONT_LEFT: return CHANNEL_LEFT;
case PA_CHANNEL_POSITION_FRONT_RIGHT: return CHANNEL_RIGHT;
case PA_CHANNEL_POSITION_FRONT_CENTER: return CHANNEL_CENTER;
case PA_CHANNEL_POSITION_SIDE_LEFT: return CHANNEL_LS;
case PA_CHANNEL_POSITION_SIDE_RIGHT: return CHANNEL_RS;
case PA_CHANNEL_POSITION_REAR_LEFT: return CHANNEL_RLS;
case PA_CHANNEL_POSITION_REAR_CENTER: return CHANNEL_RCENTER;
case PA_CHANNEL_POSITION_REAR_RIGHT: return CHANNEL_RRS;
case PA_CHANNEL_POSITION_LFE: return CHANNEL_LFE;
default: return CHANNEL_INVALID;
}
}
static void
layout_to_channel_map(cubeb_channel_layout layout, pa_channel_map * cm)
{
assert(cm && layout != CUBEB_LAYOUT_UNDEFINED);
WRAP(pa_channel_map_init)(cm);
cm->channels = CUBEB_CHANNEL_LAYOUT_MAPS[layout].channels;
for (uint8_t i = 0 ; i < cm->channels ; ++i) {
cm->map[i] = cubeb_channel_to_pa_channel(CHANNEL_INDEX_TO_ORDER[layout][i]);
}
}
// DUAL_MONO(_LFE) is same as STEREO(_LFE).
#define MASK_MONO (1 << CHANNEL_MONO)
#define MASK_MONO_LFE (MASK_MONO | (1 << CHANNEL_LFE))
#define MASK_STEREO ((1 << CHANNEL_LEFT) | (1 << CHANNEL_RIGHT))
#define MASK_STEREO_LFE (MASK_STEREO | (1 << CHANNEL_LFE))
#define MASK_3F (MASK_STEREO | (1 << CHANNEL_CENTER))
#define MASK_3F_LFE (MASK_3F | (1 << CHANNEL_LFE))
#define MASK_2F1 (MASK_STEREO | (1 << CHANNEL_RCENTER))
#define MASK_2F1_LFE (MASK_2F1 | (1 << CHANNEL_LFE))
#define MASK_3F1 (MASK_3F | (1 << CHANNEL_RCENTER))
#define MASK_3F1_LFE (MASK_3F1 | (1 << CHANNEL_LFE))
#define MASK_2F2 (MASK_STEREO | (1 << CHANNEL_LS) | (1 << CHANNEL_RS))
#define MASK_2F2_LFE (MASK_2F2 | (1 << CHANNEL_LFE))
#define MASK_3F2 (MASK_2F2 | (1 << CHANNEL_CENTER))
#define MASK_3F2_LFE (MASK_3F2 | (1 << CHANNEL_LFE))
#define MASK_3F3R_LFE (MASK_3F2_LFE | (1 << CHANNEL_RCENTER))
#define MASK_3F4_LFE (MASK_3F2_LFE | (1 << CHANNEL_RLS) | (1 << CHANNEL_RRS))
static cubeb_channel_layout
channel_map_to_layout(pa_channel_map * cm)
{
uint32_t channel_mask = 0;
for (uint8_t i = 0 ; i < cm->channels ; ++i) {
cubeb_channel channel = pa_channel_to_cubeb_channel(cm->map[i]);
if (channel == CHANNEL_INVALID) {
return CUBEB_LAYOUT_UNDEFINED;
}
channel_mask |= 1 << channel;
}
switch(channel_mask) {
case MASK_MONO: return CUBEB_LAYOUT_MONO;
case MASK_MONO_LFE: return CUBEB_LAYOUT_MONO_LFE;
case MASK_STEREO: return CUBEB_LAYOUT_STEREO;
case MASK_STEREO_LFE: return CUBEB_LAYOUT_STEREO_LFE;
case MASK_3F: return CUBEB_LAYOUT_3F;
case MASK_3F_LFE: return CUBEB_LAYOUT_3F_LFE;
case MASK_2F1: return CUBEB_LAYOUT_2F1;
case MASK_2F1_LFE: return CUBEB_LAYOUT_2F1_LFE;
case MASK_3F1: return CUBEB_LAYOUT_3F1;
case MASK_3F1_LFE: return CUBEB_LAYOUT_3F1_LFE;
case MASK_2F2: return CUBEB_LAYOUT_2F2;
case MASK_2F2_LFE: return CUBEB_LAYOUT_2F2_LFE;
case MASK_3F2: return CUBEB_LAYOUT_3F2;
case MASK_3F2_LFE: return CUBEB_LAYOUT_3F2_LFE;
case MASK_3F3R_LFE: return CUBEB_LAYOUT_3F3R_LFE;
case MASK_3F4_LFE: return CUBEB_LAYOUT_3F4_LFE;
default: return CUBEB_LAYOUT_UNDEFINED;
}
}
static void pulse_context_destroy(cubeb * ctx);
static void pulse_destroy(cubeb * ctx);
@ -596,6 +704,23 @@ pulse_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
return CUBEB_OK;
}
static int
pulse_get_preferred_channel_layout(cubeb * ctx, cubeb_channel_layout * layout)
{
assert(ctx && layout);
(void)ctx;
WRAP(pa_threaded_mainloop_lock)(ctx->mainloop);
while (!ctx->default_sink_info) {
WRAP(pa_threaded_mainloop_wait)(ctx->mainloop);
}
WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop);
*layout = channel_map_to_layout(&ctx->default_sink_info->channel_map);
return CUBEB_OK;
}
static int
pulse_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames)
{
@ -672,7 +797,8 @@ create_pa_stream(cubeb_stream * stm,
cubeb_stream_params * stream_params,
char const * stream_name)
{
assert(stm && stream_params);
assert(stm && stream_params && stream_params->layout != CUBEB_LAYOUT_UNDEFINED &&
CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].channels == stream_params->channels);
*pa_stm = NULL;
pa_sample_spec ss;
ss.format = to_pulse_format(stream_params->format);
@ -681,7 +807,10 @@ create_pa_stream(cubeb_stream * stm,
ss.rate = stream_params->rate;
ss.channels = stream_params->channels;
*pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL);
pa_channel_map cm;
layout_to_channel_map(stream_params->layout, &cm);
*pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm);
return (*pa_stm == NULL) ? CUBEB_ERROR : CUBEB_OK;
}
@ -1410,6 +1539,7 @@ static struct cubeb_ops const pulse_ops = {
.get_max_channel_count = pulse_get_max_channel_count,
.get_min_latency = pulse_get_min_latency,
.get_preferred_sample_rate = pulse_get_preferred_sample_rate,
.get_preferred_channel_layout = pulse_get_preferred_channel_layout,
.enumerate_devices = pulse_enumerate_devices,
.destroy = pulse_destroy,
.stream_init = pulse_stream_init,

View File

@ -124,7 +124,6 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor>
T * out_unprocessed = nullptr;
long output_frames_before_processing = 0;
/* fill directly the input buffer of the output processor to save a copy */
output_frames_before_processing =
output_processor->input_needed_for_output(output_frames_needed);
@ -174,7 +173,6 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor>
return (*input_frames_count) * (got / resampled_frame_count);
}
template<typename T, typename InputProcessor, typename OutputProcessor>
long
cubeb_resampler_speex<T, InputProcessor, OutputProcessor>

View File

@ -366,6 +366,7 @@ static struct cubeb_ops const sndio_ops = {
.get_max_channel_count = sndio_get_max_channel_count,
.get_min_latency = sndio_get_min_latency,
.get_preferred_sample_rate = sndio_get_preferred_sample_rate,
.get_preferred_channel_layout = NULL,
.enumerate_devices = NULL,
.destroy = sndio_destroy,
.stream_init = sndio_stream_init,

View File

@ -46,6 +46,63 @@ void PodZero(T * destination, size_t count)
memset(destination, 0, count * sizeof(T));
}
namespace {
template<typename T, typename Trait>
void Copy(T * destination, const T * source, size_t count, Trait)
{
for (size_t i = 0; i < count; i++) {
destination[i] = source[i];
}
}
template<typename T>
void Copy(T * destination, const T * source, size_t count, std::true_type)
{
PodCopy(destination, source, count);
}
}
/**
* This allows copying a number of elements from a `source` pointer to a
* `destination` pointer, using `memcpy` if it is safe to do so, or a loop that
* calls the constructors and destructors otherwise.
*/
template<typename T>
void Copy(T * destination, const T * source, size_t count)
{
assert(destination && source);
Copy(destination, source, count, typename std::is_trivial<T>::type());
}
namespace {
template<typename T, typename Trait>
void ConstructDefault(T * destination, size_t count, Trait)
{
for (size_t i = 0; i < count; i++) {
destination[i] = T();
}
}
template<typename T>
void ConstructDefault(T * destination,
size_t count, std::true_type)
{
PodZero(destination, count);
}
}
/**
* This allows zeroing (using memset) or default-constructing a number of
* elements calling the constructors and destructors if necessary.
*/
template<typename T>
void ConstructDefault(T * destination, size_t count)
{
assert(destination);
ConstructDefault(destination, count,
typename std::is_arithmetic<T>::type());
}
template<typename T>
class auto_array
{

View File

@ -4,6 +4,11 @@
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
// Explicitly define NTDDI_VERSION rather than letting the value be derived
// from _WIN32_WINNT because we depend on values defined for XP SP2 and WS03
// SP1.
#define _WIN32_WINNT 0x0502
#define NTDDI_VERSION 0x05020100
#define NOMINMAX
#include <initguid.h>
@ -27,6 +32,7 @@
#include "cubeb/cubeb.h"
#include "cubeb-internal.h"
#include "cubeb_mixer.h"
#include "cubeb_resampler.h"
#include "cubeb_utils.h"
@ -198,12 +204,12 @@ struct cubeb_stream {
/* Mixer pameters. We need to convert the input stream to this
samplerate/channel layout, as WASAPI does not resample nor upmix
itself. */
cubeb_stream_params input_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params input_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
/* Stream parameters. This is what the client requested,
* and what will be presented in the callback. */
cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0 };
cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED };
/* The input and output device, or NULL for default. */
std::unique_ptr<const wchar_t[]> input_device;
std::unique_ptr<const wchar_t[]> output_device;
@ -392,21 +398,89 @@ bool has_output(cubeb_stream * stm)
return stm->output_stream_params.rate != 0;
}
bool should_upmix(cubeb_stream_params & stream, cubeb_stream_params & mixer)
{
return mixer.channels > stream.channels;
}
bool should_downmix(cubeb_stream_params & stream, cubeb_stream_params & mixer)
{
return mixer.channels < stream.channels;
}
double stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, cubeb_stream_params & mixer)
{
return double(stream.rate) / mixer.rate;
}
/* Convert the channel layout into the corresponding KSAUDIO_CHANNEL_CONFIG.
See more: https://msdn.microsoft.com/en-us/library/windows/hardware/ff537083(v=vs.85).aspx */
#define MASK_DUAL_MONO (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT)
#define MASK_DUAL_MONO_LFE (MASK_DUAL_MONO | SPEAKER_LOW_FREQUENCY)
#define MASK_MONO (KSAUDIO_SPEAKER_MONO)
#define MASK_MONO_LFE (MASK_MONO | SPEAKER_LOW_FREQUENCY)
#define MASK_STEREO (KSAUDIO_SPEAKER_STEREO)
#define MASK_STEREO_LFE (MASK_STEREO | SPEAKER_LOW_FREQUENCY)
#define MASK_3F (MASK_STEREO | SPEAKER_FRONT_CENTER)
#define MASK_3F_LFE (MASK_3F | SPEAKER_LOW_FREQUENCY)
#define MASK_2F1 (MASK_STEREO | SPEAKER_BACK_CENTER)
#define MASK_2F1_LFE (MASK_2F1 | SPEAKER_LOW_FREQUENCY)
#define MASK_3F1 (KSAUDIO_SPEAKER_SURROUND)
#define MASK_3F1_LFE (MASK_3F1 | SPEAKER_LOW_FREQUENCY)
#define MASK_2F2 (MASK_STEREO | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT)
#define MASK_2F2_LFE (MASK_2F2 | SPEAKER_LOW_FREQUENCY)
#define MASK_3F2 (MASK_3F | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT)
#define MASK_3F2_LFE (KSAUDIO_SPEAKER_5POINT1_SURROUND)
#define MASK_3F3R_LFE (MASK_3F2_LFE | SPEAKER_BACK_CENTER)
#define MASK_3F4_LFE (KSAUDIO_SPEAKER_7POINT1_SURROUND)
static DWORD
channel_layout_to_mask(cubeb_channel_layout layout)
{
XASSERT(layout > CUBEB_LAYOUT_UNDEFINED && layout < CUBEB_LAYOUT_MAX &&
"This mask conversion is not allowed.");
// This variable may be used for multiple times, so we should avoid to
// allocate it in stack, or it will be created and removed repeatedly.
// Use static to allocate this local variable in data space instead of stack.
static DWORD map[CUBEB_LAYOUT_MAX] = {
0, // CUBEB_LAYOUT_UNDEFINED (this won't be used.)
MASK_DUAL_MONO, // CUBEB_LAYOUT_DUAL_MONO
MASK_DUAL_MONO_LFE, // CUBEB_LAYOUT_DUAL_MONO_LFE
MASK_MONO, // CUBEB_LAYOUT_MONO
MASK_MONO_LFE, // CUBEB_LAYOUT_MONO_LFE
MASK_STEREO, // CUBEB_LAYOUT_STEREO
MASK_STEREO_LFE, // CUBEB_LAYOUT_STEREO_LFE
MASK_3F, // CUBEB_LAYOUT_3F
MASK_3F_LFE, // CUBEB_LAYOUT_3F_LFE
MASK_2F1, // CUBEB_LAYOUT_2F1
MASK_2F1_LFE, // CUBEB_LAYOUT_2F1_LFE
MASK_3F1, // CUBEB_LAYOUT_3F1
MASK_3F1_LFE, // CUBEB_LAYOUT_3F1_LFE
MASK_2F2, // CUBEB_LAYOUT_2F2
MASK_2F2_LFE, // CUBEB_LAYOUT_2F2_LFE
MASK_3F2, // CUBEB_LAYOUT_3F2
MASK_3F2_LFE, // CUBEB_LAYOUT_3F2_LFE
MASK_3F3R_LFE, // CUBEB_LAYOUT_3F3R_LFE
MASK_3F4_LFE, // CUBEB_LAYOUT_3F4_LFE
};
return map[layout];
}
cubeb_channel_layout
mask_to_channel_layout(DWORD mask)
{
switch (mask) {
// MASK_DUAL_MONO(_LFE) is same as STEREO(_LFE), so we skip it.
case MASK_MONO: return CUBEB_LAYOUT_MONO;
case MASK_MONO_LFE: return CUBEB_LAYOUT_MONO_LFE;
case MASK_STEREO: return CUBEB_LAYOUT_STEREO;
case MASK_STEREO_LFE: return CUBEB_LAYOUT_STEREO_LFE;
case MASK_3F: return CUBEB_LAYOUT_3F;
case MASK_3F_LFE: return CUBEB_LAYOUT_3F_LFE;
case MASK_2F1: return CUBEB_LAYOUT_2F1;
case MASK_2F1_LFE: return CUBEB_LAYOUT_2F1_LFE;
case MASK_3F1: return CUBEB_LAYOUT_3F1;
case MASK_3F1_LFE: return CUBEB_LAYOUT_3F1_LFE;
case MASK_2F2: return CUBEB_LAYOUT_2F2;
case MASK_2F2_LFE: return CUBEB_LAYOUT_2F2_LFE;
case MASK_3F2: return CUBEB_LAYOUT_3F2;
case MASK_3F2_LFE: return CUBEB_LAYOUT_3F2_LFE;
case MASK_3F3R_LFE: return CUBEB_LAYOUT_3F3R_LFE;
case MASK_3F4_LFE: return CUBEB_LAYOUT_3F4_LFE;
default: return CUBEB_LAYOUT_UNDEFINED;
}
}
uint32_t
get_rate(cubeb_stream * stm)
@ -439,71 +513,13 @@ frames_to_hns(cubeb_stream * stm, uint32_t frames)
return frames * 1000 / get_rate(stm);
}
/* Upmix function, copies a mono channel into L and R */
template<typename T>
void
mono_to_stereo(T * in, long insamples, T * out, int32_t out_channels)
{
for (int i = 0, j = 0; i < insamples; ++i, j += out_channels) {
out[j] = out[j + 1] = in[i];
}
}
template<typename T>
void
upmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels)
{
XASSERT(out_channels >= in_channels && in_channels > 0);
/* Either way, if we have 2 or more channels, the first two are L and R. */
/* If we are playing a mono stream over stereo speakers, copy the data over. */
if (in_channels == 1 && out_channels >= 2) {
mono_to_stereo(in, inframes, out, out_channels);
} else {
/* Copy through. */
for (int i = 0, o = 0; i < inframes * in_channels;
i += in_channels, o += out_channels) {
for (int j = 0; j < in_channels; ++j) {
out[o + j] = in[i + j];
}
}
}
/* Check if more channels. */
if (out_channels <= 2) {
return;
}
/* Put silence in remaining channels. */
for (long i = 0, o = 0; i < inframes; ++i, o += out_channels) {
for (int j = 2; j < out_channels; ++j) {
out[o + j] = 0.0;
}
}
}
template<typename T>
void
downmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels)
{
XASSERT(in_channels >= out_channels);
/* We could use a downmix matrix here, applying mixing weight based on the
channel, but directsound and winmm simply drop the channels that cannot be
rendered by the hardware, so we do the same for consistency. */
long out_index = 0;
for (long i = 0; i < inframes * in_channels; i += in_channels) {
for (int j = 0; j < out_channels; ++j) {
out[out_index + j] = in[i + j];
}
out_index += out_channels;
}
}
/* This returns the size of a frame in the stream, before the eventual upmix
occurs. */
static size_t
frames_to_bytes_before_mix(cubeb_stream * stm, size_t frames)
{
// This is called only when we has a output client.
XASSERT(has_output(stm));
size_t stream_frame_size = stm->output_stream_params.channels * sizeof(float);
return stream_frame_size * frames;
}
@ -519,8 +535,8 @@ refill(cubeb_stream * stm, float * input_buffer, long input_frames_count,
avoid a copy. */
float * dest = nullptr;
if (has_output(stm)) {
if (should_upmix(stm->output_stream_params, stm->output_mix_params) ||
should_downmix(stm->output_stream_params, stm->output_mix_params)) {
if (cubeb_should_upmix(&stm->output_stream_params, &stm->output_mix_params) ||
cubeb_should_downmix(&stm->output_stream_params, &stm->output_mix_params)) {
dest = stm->mix_buffer.data();
} else {
dest = output_buffer;
@ -551,12 +567,13 @@ refill(cubeb_stream * stm, float * input_buffer, long input_frames_count,
XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm));
if (has_output(stm)) {
if (should_upmix(stm->output_stream_params, stm->output_mix_params)) {
upmix(dest, out_frames, output_buffer,
stm->output_stream_params.channels, stm->output_mix_params.channels);
} else if (should_downmix(stm->output_stream_params, stm->output_mix_params)) {
downmix(dest, out_frames, output_buffer,
stm->output_stream_params.channels, stm->output_mix_params.channels);
if (cubeb_should_upmix(&stm->output_stream_params, &stm->output_mix_params)) {
cubeb_upmix_float(dest, out_frames, output_buffer,
stm->output_stream_params.channels, stm->output_mix_params.channels);
} else if (cubeb_should_downmix(&stm->output_stream_params, &stm->output_mix_params)) {
cubeb_downmix_float(dest, out_frames, output_buffer,
stm->output_stream_params.channels, stm->output_mix_params.channels,
stm->output_stream_params.layout, stm->output_mix_params.layout);
}
}
@ -615,23 +632,25 @@ bool get_input_buffer(cubeb_stream * stm)
LOG("insert silence: ps=%u", packet_size);
stm->linear_input_buffer.push_silence(packet_size * stm->input_stream_params.channels);
} else {
if (should_upmix(stm->input_mix_params, stm->input_stream_params)) {
if (cubeb_should_upmix(&stm->input_mix_params, &stm->input_stream_params)) {
bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() +
packet_size * stm->input_stream_params.channels);
XASSERT(ok);
upmix(reinterpret_cast<float*>(input_packet), packet_size,
stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
stm->input_mix_params.channels,
stm->input_stream_params.channels);
cubeb_upmix_float(reinterpret_cast<float*>(input_packet), packet_size,
stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
stm->input_mix_params.channels,
stm->input_stream_params.channels);
stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels);
} else if (should_downmix(stm->input_mix_params, stm->input_stream_params)) {
} else if (cubeb_should_downmix(&stm->input_mix_params, &stm->input_stream_params)) {
bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() +
packet_size * stm->input_stream_params.channels);
XASSERT(ok);
downmix(reinterpret_cast<float*>(input_packet), packet_size,
stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
stm->input_mix_params.channels,
stm->input_stream_params.channels);
cubeb_downmix_float(reinterpret_cast<float*>(input_packet), packet_size,
stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
stm->input_mix_params.channels,
stm->input_stream_params.channels,
stm->input_mix_params.layout,
stm->input_stream_params.layout);
stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels);
} else {
stm->linear_input_buffer.push(reinterpret_cast<float*>(input_packet),
@ -781,7 +800,8 @@ refill_callback_input(cubeb_stream * stm)
nullptr,
0);
consumed_all_buffer = read == stm->linear_input_buffer.length();
XASSERT(read >= 0);
consumed_all_buffer = (unsigned long) read == stm->linear_input_buffer.length();
stm->linear_input_buffer.clear();
@ -817,7 +837,7 @@ refill_callback_output(cubeb_stream * stm)
output_frames, got);
XASSERT(got >= 0);
XASSERT(got == output_frames || stm->draining);
XASSERT((unsigned long) got == output_frames || stm->draining);
hr = stm->render_client->ReleaseBuffer(got, 0);
if (FAILED(hr)) {
@ -825,7 +845,7 @@ refill_callback_output(cubeb_stream * stm)
return false;
}
return got == output_frames || stm->draining;
return (unsigned long) got == output_frames || stm->draining;
}
static unsigned int __stdcall
@ -860,7 +880,6 @@ wasapi_stream_render_loop(LPVOID stream)
LOG("Unable to use mmcss to bump the render thread priority: %lx", GetLastError());
}
/* WaitForMultipleObjects timeout can trigger in cases where we don't want to
treat it as a timeout, such as across a system sleep/wake cycle. Trigger
the timeout error handling only when the timeout_limit is reached, which is
@ -1211,13 +1230,18 @@ bool stop_and_join_render_thread(cubeb_stream * stm)
rv = false;
}
LOG("Closing thread.");
CloseHandle(stm->thread);
stm->thread = NULL;
// Only attempts to close and null out the thread and event if the
// WaitForSingleObject above succeeded, so that calling this function again
// attemps to clean up the thread and event each time.
if (rv) {
LOG("Closing thread.");
CloseHandle(stm->thread);
stm->thread = NULL;
CloseHandle(stm->shutdown_event);
stm->shutdown_event = 0;
CloseHandle(stm->shutdown_event);
stm->shutdown_event = 0;
}
return rv;
}
@ -1359,6 +1383,44 @@ wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
return CUBEB_OK;
}
int
wasapi_get_preferred_channel_layout(cubeb * context, cubeb_channel_layout * layout)
{
HRESULT hr;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
if (FAILED(hr)) {
return CUBEB_ERROR;
}
com_ptr<IAudioClient> client;
hr = device->Activate(__uuidof(IAudioClient),
CLSCTX_INPROC_SERVER,
NULL, client.receive_vpp());
if (FAILED(hr)) {
return CUBEB_ERROR;
}
WAVEFORMATEX * tmp = nullptr;
hr = client->GetMixFormat(&tmp);
if (FAILED(hr)) {
return CUBEB_ERROR;
}
com_heap_ptr<WAVEFORMATEX> mix_format(tmp);
WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
*layout = mask_to_channel_layout(format_pcm->dwChannelMask);
LOG("Preferred channel layout: %s", CUBEB_CHANNEL_LAYOUT_MAPS[*layout].name);
return CUBEB_OK;
}
void wasapi_stream_destroy(cubeb_stream * stm);
/* Based on the mix format and the stream format, try to find a way to play
@ -1366,12 +1428,7 @@ void wasapi_stream_destroy(cubeb_stream * stm);
static void
handle_channel_layout(cubeb_stream * stm, com_heap_ptr<WAVEFORMATEX> & mix_format, const cubeb_stream_params * stream_params)
{
/* Common case: the hardware is stereo. Up-mixing and down-mixing will be
handled in the callback. */
if (mix_format->nChannels <= 2) {
return;
}
XASSERT(stream_params->layout != CUBEB_LAYOUT_UNDEFINED);
/* The docs say that GetMixFormat is always of type WAVEFORMATEXTENSIBLE [1],
so the reinterpret_cast below should be safe. In practice, this is not
true, and we just want to bail out and let the rest of the code find a good
@ -1386,19 +1443,10 @@ handle_channel_layout(cubeb_stream * stm, com_heap_ptr<WAVEFORMATEX> & mix_form
/* Stash a copy of the original mix format in case we need to restore it later. */
WAVEFORMATEXTENSIBLE hw_mix_format = *format_pcm;
/* The hardware is in surround mode, we want to only use front left and front
right. Try that, and check if it works. */
switch (stream_params->channels) {
case 1: /* Mono */
format_pcm->dwChannelMask = KSAUDIO_SPEAKER_MONO;
break;
case 2: /* Stereo */
format_pcm->dwChannelMask = KSAUDIO_SPEAKER_STEREO;
break;
default:
XASSERT(false && "Channel layout not supported.");
break;
}
/* Get the channel mask by the channel layout.
If the layout is not supported, we will get a closest settings below. */
format_pcm->dwChannelMask = channel_layout_to_mask(stream_params->layout);
mix_format->nChannels = stream_params->channels;
mix_format->nBlockAlign = mix_format->wBitsPerSample * mix_format->nChannels / 8;
mix_format->nAvgBytesPerSec = mix_format->nSamplesPerSec * mix_format->nBlockAlign;
@ -1499,16 +1547,39 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm,
}
com_heap_ptr<WAVEFORMATEX> mix_format(tmp);
handle_channel_layout(stm, mix_format, stream_params);
/* Set channel layout only when there're more than two channels. Otherwise,
* use the default setting retrieved from the stream format of the audio
* engine's internal processing by GetMixFormat. */
if (mix_format->nChannels > 2) {
/* Currently, we only support mono and stereo for capture stream. */
if (direction == eCapture) {
XASSERT(false && "Multichannel recording is not supported.");
}
handle_channel_layout(stm, mix_format, stream_params);
}
/* Shared mode WASAPI always supports float32 sample format, so this
* is safe. */
mix_params->format = CUBEB_SAMPLE_FLOAT32NE;
mix_params->rate = mix_format->nSamplesPerSec;
mix_params->channels = mix_format->nChannels;
LOG("Setup requested=[f=%d r=%u c=%u] mix=[f=%d r=%u c=%u]",
WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
mix_params->layout = mask_to_channel_layout(format_pcm->dwChannelMask);
if (mix_params->layout == CUBEB_LAYOUT_UNDEFINED) {
LOG("Output using undefined layout!\n");
} else if (mix_format->nChannels != CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].channels) {
// The CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].channels may be
// different from the mix_params->channels. 6 channel ouput with stereo
// layout is acceptable in Windows. If this happens, it should not downmix
// audio according to layout.
LOG("Channel count is different from the layout standard!\n");
}
LOG("Setup requested=[f=%d r=%u c=%u l=%s] mix=[f=%d r=%u c=%u l=%s]",
stream_params->format, stream_params->rate, stream_params->channels,
mix_params->format, mix_params->rate, mix_params->channels);
CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].name,
mix_params->format, mix_params->rate, mix_params->channels,
CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].name);
hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
@ -1530,8 +1601,8 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm,
}
// Input is up/down mixed when depacketized in get_input_buffer.
if (has_output(stm) &&
(should_upmix(*stream_params, *mix_params) ||
should_downmix(*stream_params, *mix_params))) {
(cubeb_should_upmix(stream_params, mix_params) ||
cubeb_should_downmix(stream_params, mix_params))) {
stm->mix_buffer.resize(frames_to_bytes_before_mix(stm, *buffer_frame_count));
}
@ -1712,10 +1783,14 @@ wasapi_stream_init(cubeb * context, cubeb_stream ** stream,
if (input_stream_params) {
stm->input_stream_params = *input_stream_params;
stm->input_device = utf8_to_wstr(reinterpret_cast<char const *>(input_device));
// Make sure the layout matches the channel count.
XASSERT(stm->input_stream_params.channels == CUBEB_CHANNEL_LAYOUT_MAPS[stm->input_stream_params.layout].channels);
}
if (output_stream_params) {
stm->output_stream_params = *output_stream_params;
stm->output_device = utf8_to_wstr(reinterpret_cast<char const *>(output_device));
// Make sure the layout matches the channel count.
XASSERT(stm->output_stream_params.channels == CUBEB_CHANNEL_LAYOUT_MAPS[stm->output_stream_params.layout].channels);
}
stm->latency = latency_frames;
@ -1921,7 +1996,6 @@ int wasapi_stream_stop(cubeb_stream * stm)
}
}
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
}
@ -2252,6 +2326,7 @@ cubeb_ops const wasapi_ops = {
/*.get_max_channel_count =*/ wasapi_get_max_channel_count,
/*.get_min_latency =*/ wasapi_get_min_latency,
/*.get_preferred_sample_rate =*/ wasapi_get_preferred_sample_rate,
/*.get_preferred_channel_layout =*/ wasapi_get_preferred_channel_layout,
/*.enumerate_devices =*/ wasapi_enumerate_devices,
/*.destroy =*/ wasapi_destroy,
/*.stream_init =*/ wasapi_stream_init,

View File

@ -512,7 +512,6 @@ winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n
return CUBEB_ERROR;
}
for (i = 0; i < NBUFS; ++i) {
WAVEHDR * hdr = &stm->buffers[i];
@ -769,7 +768,6 @@ winmm_calculate_device_rate(cubeb_device_info * info, DWORD formats)
}
}
#define MM_S16_MASK (WAVE_FORMAT_1M16 | WAVE_FORMAT_1S16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4M16 | \
WAVE_FORMAT_4S16 | WAVE_FORMAT_48M16 | WAVE_FORMAT_48S16 | WAVE_FORMAT_96M16 | WAVE_FORMAT_96S16)
static int
@ -1050,6 +1048,7 @@ static struct cubeb_ops const winmm_ops = {
/*.get_max_channel_count=*/ winmm_get_max_channel_count,
/*.get_min_latency=*/ winmm_get_min_latency,
/*.get_preferred_sample_rate =*/ winmm_get_preferred_sample_rate,
/*.get_preferred_channel_layout =*/ NULL,
/*.enumerate_devices =*/ winmm_enumerate_devices,
/*.destroy =*/ winmm_destroy,
/*.stream_init =*/ winmm_stream_init,

View File

@ -10,6 +10,7 @@ Library('cubeb')
SOURCES += [
'cubeb.c',
'cubeb_mixer.cpp',
'cubeb_panner.cpp'
]

View File

@ -31,6 +31,8 @@ cp $1/src/cubeb_utils_unix.h src
cp $1/src/cubeb_utils_win.h src
cp $1/src/cubeb_wasapi.cpp src
cp $1/src/cubeb_winmm.c src
cp $1/src/cubeb_mixer.h src
cp $1/src/cubeb_mixer.cpp src
cp $1/test/common.h gtest
cp $1/test/test_audio.cpp gtest
cp $1/test/test_devices.cpp gtest
@ -42,6 +44,7 @@ cp $1/test/test_ring_array.cpp gtest
cp $1/test/test_sanity.cpp gtest
cp $1/test/test_tone.cpp gtest
cp $1/test/test_utils.cpp gtest
cp $1/test/test_mixer.cpp gtest
if [ -d $1/.git ]; then
rev=$(cd $1 && git rev-parse --verify HEAD)