From 1691d1805310c448c19808b1308cd84eec54d14a Mon Sep 17 00:00:00 2001 From: Bryce Van Dyk Date: Tue, 5 Dec 2017 13:11:01 -0500 Subject: [PATCH] Bug 1432869 - Update cubeb from upstream to 4c18a84. r=achronop This brings across the WASAPI loopback stream functionality recently implemented in cubeb. The README_MOZILLA is reverted to its previous format. Previously a couple of patches were highlighted as needing application, however these have now been applied upstream. MozReview-Commit-ID: CV6FHWXZBK0 --HG-- extra : rebase_source : 2dff33fcff6960d1d5b7d8dfaf35023b9c44d16e --- media/libcubeb/README_MOZILLA | 10 +- media/libcubeb/gtest/test_audio.cpp | 2 + media/libcubeb/gtest/test_devices.cpp | 1 + media/libcubeb/gtest/test_duplex.cpp | 2 + media/libcubeb/gtest/test_latency.cpp | 3 +- media/libcubeb/gtest/test_loopback.cpp | 575 ++++++++++++++++++ media/libcubeb/gtest/test_mixer.cpp | 6 +- .../libcubeb/gtest/test_overload_callback.cpp | 1 + media/libcubeb/gtest/test_record.cpp | 1 + media/libcubeb/gtest/test_resampler.cpp | 1 + media/libcubeb/gtest/test_sanity.cpp | 11 + media/libcubeb/gtest/test_tone.cpp | 1 + media/libcubeb/include/cubeb.h | 16 +- media/libcubeb/src/cubeb_alsa.c | 4 + media/libcubeb/src/cubeb_audiounit.cpp | 10 +- media/libcubeb/src/cubeb_jack.cpp | 6 + media/libcubeb/src/cubeb_opensl.c | 5 + media/libcubeb/src/cubeb_pulse.c | 3 + media/libcubeb/src/cubeb_sndio.c | 8 + media/libcubeb/src/cubeb_wasapi.cpp | 165 +++-- media/libcubeb/src/cubeb_winmm.c | 5 + 21 files changed, 776 insertions(+), 60 deletions(-) create mode 100644 media/libcubeb/gtest/test_loopback.cpp diff --git a/media/libcubeb/README_MOZILLA b/media/libcubeb/README_MOZILLA index fafdcd4c968e..1e3e0ad1e230 100644 --- a/media/libcubeb/README_MOZILLA +++ b/media/libcubeb/README_MOZILLA @@ -1,12 +1,8 @@ The source from this directory was copied from the cubeb git repository using the update.sh script. The only changes -made were those applied by update.sh, the addition of -Makefile.in build files for the Mozilla build system, -and the following patches, which may be overwritten when -included upstream. -https://github.com/kinetiknz/cubeb/pull/398/commits/c8e66dee61a35e6a6d54e3630e1668bdbd6984b4 -https://github.com/kinetiknz/cubeb/pull/398/commits/2ed979bc891cf1a7822799947a357d4d3b625964 +made were those applied by update.sh and the addition of +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 2b98e3d3279322ce0764ae2b9f28a9ea9000418b (2018-01-19 10:17:28 +1300) +The git commit ID used was 4c18a84a4573a23a1c39ccf6c70f1a6c328cb110 (2018-01-23 08:50:28 +1300) diff --git a/media/libcubeb/gtest/test_audio.cpp b/media/libcubeb/gtest/test_audio.cpp index 5548b34ab5bd..df513669517d 100644 --- a/media/libcubeb/gtest/test_audio.cpp +++ b/media/libcubeb/gtest/test_audio.cpp @@ -123,6 +123,7 @@ int run_test(int num_channels, layout_info layout, int sampling_rate, int is_flo params.rate = sampling_rate; params.channels = num_channels; params.layout = layout.layout; + params.prefs = CUBEB_STREAM_PREF_NONE; synth_state synth(params.channels, params.rate); @@ -171,6 +172,7 @@ int run_panning_volume_test(int is_float) params.rate = 44100; params.channels = 2; params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; synth_state synth(params.channels, params.rate); diff --git a/media/libcubeb/gtest/test_devices.cpp b/media/libcubeb/gtest/test_devices.cpp index 93c869248399..10c14c3f74f5 100644 --- a/media/libcubeb/gtest/test_devices.cpp +++ b/media/libcubeb/gtest/test_devices.cpp @@ -190,6 +190,7 @@ TEST(cubeb, enumerate_devices) input_params.rate = output_params.rate = 48000; input_params.channels = output_params.channels = 1; input_params.layout = output_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, &output_params, diff --git a/media/libcubeb/gtest/test_duplex.cpp b/media/libcubeb/gtest/test_duplex.cpp index 427b503929a7..09315ba4b5c5 100644 --- a/media/libcubeb/gtest/test_duplex.cpp +++ b/media/libcubeb/gtest/test_duplex.cpp @@ -101,10 +101,12 @@ TEST(cubeb, duplex) input_params.rate = 48000; input_params.channels = 1; input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_NONE; output_params.format = STREAM_FORMAT; output_params.rate = 48000; output_params.channels = 2; output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; diff --git a/media/libcubeb/gtest/test_latency.cpp b/media/libcubeb/gtest/test_latency.cpp index 6af39d598875..f4aa6b07c8da 100644 --- a/media/libcubeb/gtest/test_latency.cpp +++ b/media/libcubeb/gtest/test_latency.cpp @@ -34,7 +34,8 @@ TEST(cubeb, latency) CUBEB_SAMPLE_FLOAT32NE, preferred_rate, max_channels, - CUBEB_LAYOUT_UNDEFINED + CUBEB_LAYOUT_UNDEFINED, + CUBEB_STREAM_PREF_NONE }; r = cubeb_get_min_latency(ctx, ¶ms, &latency_frames); ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); diff --git a/media/libcubeb/gtest/test_loopback.cpp b/media/libcubeb/gtest/test_loopback.cpp new file mode 100644 index 000000000000..4fe513708fa6 --- /dev/null +++ b/media/libcubeb/gtest/test_loopback.cpp @@ -0,0 +1,575 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + + /* libcubeb api/function test. Requests a loopback device and checks that + output is being looped back to input. NOTE: Usage of output devices while + performing this test will cause flakey results! */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include +#include +#include +#include +#include +#include +#include +#include "cubeb/cubeb.h" +#include "common.h" + +const uint32_t SAMPLE_FREQUENCY = 48000; +const uint32_t TONE_FREQUENCY = 440; +const double OUTPUT_AMPLITUDE = 0.25; +const uint32_t NUM_FRAMES_TO_OUTPUT = SAMPLE_FREQUENCY / 20; /* play ~50ms of samples */ + +template T ConvertSampleToOutput(double input); +template<> float ConvertSampleToOutput(double input) { return float(input); } +template<> short ConvertSampleToOutput(double input) { return short(input * 32767.0f); } + +template double ConvertSampleFromOutput(T sample); +template<> double ConvertSampleFromOutput(float sample) { return double(sample); } +template<> double ConvertSampleFromOutput(short sample) { return double(sample / 32767.0); } + +/* Simple cross correlation to help find phase shift. Not a performant impl */ +std::vector cross_correlate(std::vector & f, + std::vector & g, + size_t signal_length) +{ + /* the length we sweep our window through to find the cross correlation */ + size_t sweep_length = f.size() - signal_length + 1; + std::vector correlation; + correlation.reserve(sweep_length); + for (size_t i = 0; i < sweep_length; i++) { + double accumulator = 0.0; + for (size_t j = 0; j < signal_length; j++) { + accumulator += f.at(j) * g.at(i + j); + } + correlation.push_back(accumulator); + } + return correlation; +} + +/* best effort discovery of phase shift between output and (looped) input*/ +size_t find_phase(std::vector & output_frames, + std::vector & input_frames, + size_t signal_length) +{ + std::vector correlation = cross_correlate(output_frames, input_frames, signal_length); + size_t phase = 0; + double max_correlation = correlation.at(0); + for (size_t i = 1; i < correlation.size(); i++) { + if (correlation.at(i) > max_correlation) { + max_correlation = correlation.at(i); + phase = i; + } + } + return phase; +} + +std::vector normalize_frames(std::vector & frames) { + double max = abs(*std::max_element(frames.begin(), frames.end(), + [](double a, double b) { return abs(a) < abs(b); })); + std::vector normalized_frames; + normalized_frames.reserve(frames.size()); + for (const double frame : frames) { + normalized_frames.push_back(frame / max); + } + return normalized_frames; +} + +/* heuristic comparison of aligned output and input signals, gets flaky if TONE_FREQUENCY is too high */ +void compare_signals(std::vector & output_frames, + std::vector & input_frames) +{ + ASSERT_EQ(output_frames.size(), input_frames.size()) << "#Output frames != #input frames"; + size_t num_frames = output_frames.size(); + std::vector normalized_output_frames = normalize_frames(output_frames); + std::vector normalized_input_frames = normalize_frames(input_frames); + + /* calculate mean absolute errors */ + /* mean absolute errors between output and input */ + double io_mas = 0.0; + /* mean absolute errors between output and silence */ + double output_silence_mas = 0.0; + /* mean absolute errors between input and silence */ + double input_silence_mas = 0.0; + for (size_t i = 0; i < num_frames; i++) { + io_mas += abs(normalized_output_frames.at(i) - normalized_input_frames.at(i)); + output_silence_mas += abs(normalized_output_frames.at(i)); + input_silence_mas += abs(normalized_input_frames.at(i)); + } + io_mas /= num_frames; + output_silence_mas /= num_frames; + input_silence_mas /= num_frames; + + ASSERT_LT(io_mas, output_silence_mas) + << "Error between output and input should be less than output and silence!"; + ASSERT_LT(io_mas, input_silence_mas) + << "Error between output and input should be less than output and silence!"; + + /* make sure extrema are in (roughly) correct location */ + /* number of maxima + minama expected in the frames*/ + const long NUM_EXTREMA = 2 * TONE_FREQUENCY * NUM_FRAMES_TO_OUTPUT / SAMPLE_FREQUENCY; + /* expected index of first maxima */ + const long FIRST_MAXIMUM_INDEX = SAMPLE_FREQUENCY / TONE_FREQUENCY / 4; + /* Threshold we expect all maxima and minima to be above or below. Ideally + the extrema would be 1 or -1, but particularly at the start of loopback + the values seen can be significantly lower. */ + const double THRESHOLD = 0.5; + + for (size_t i = 0; i < NUM_EXTREMA; i++) { + bool is_maximum = i % 2 == 0; + /* expected offset to current extreme: i * stide between extrema */ + size_t offset = i * SAMPLE_FREQUENCY / TONE_FREQUENCY / 2; + if (is_maximum) { + ASSERT_GT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), THRESHOLD) + << "Output frames have unexpected missing maximum!"; + ASSERT_GT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), THRESHOLD) + << "Input frames have unexpected missing maximum!"; + } else { + ASSERT_LT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), -THRESHOLD) + << "Output frames have unexpected missing minimum!"; + ASSERT_LT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), -THRESHOLD) + << "Input frames have unexpected missing minimum!"; + } + } +} + +struct user_state_loopback { + std::mutex user_state_mutex; + long position = 0; + /* track output */ + std::vector output_frames; + /* track input */ + std::vector input_frames; +}; + +template +long data_cb_loop_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ib = (T *) inputbuffer; + T * ob = (T *) outputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = sin(2 * M_PI*(i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput(tone); + u->output_frames.push_back(tone); + /* store any looped back output, may be silence */ + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + u->position += nframes; + + return nframes; +} + +template +long data_cb_loop_input_only(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ib = (T *) inputbuffer; + + if (outputbuffer != NULL) { + // Can't assert as it needs to return, so expect to fail instead + EXPECT_EQ(outputbuffer, (void *) NULL) << "outputbuffer should be null in input only callback"; + return CUBEB_ERROR; + } + + if (stream == NULL || inputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + for (int i = 0; i < nframes; i++) { + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + return nframes; +} + +template +long data_cb_playback(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *) user; + T * ob = (T *) outputbuffer; + + if (stream == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = sin(2 * M_PI*(i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput(tone); + u->output_frames.push_back(tone); + } + + u->position += nframes; + + return nframes; +} + +void state_cb_loop(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +void run_loopback_duplex_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: duplex stream"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup a duplex stream with loopback */ + r = cubeb_stream_init(ctx, &stream, "Cubeb loopback", + NULL, &input_params, NULL, &output_params, latency_frames, + is_float ? data_cb_loop_duplex : data_cb_loop_duplex, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(150); + cubeb_stream_stop(stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_EQ(output_frames.size(), input_frames.size()) + << "#Output frames != #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_duplex) +{ + run_loopback_duplex_test(true); + run_loopback_duplex_test(false); +} + +void run_loopback_separate_streams_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + NULL, &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", + NULL, NULL, NULL, &output_params, latency_frames, + is_float ? data_cb_playback : data_cb_playback, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(150); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_separate_streams) +{ + run_loopback_separate_streams_test(true); + run_loopback_separate_streams_test(false); +} + +void run_loopback_silence_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream_params input_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: record silence"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &input_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + NULL, &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + delay(50); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & input_frames = user_data->input_frames; + + /* expect to have at least ~50ms of frames */ + ASSERT_GE(input_frames.size(), SAMPLE_FREQUENCY / 20); + double EPISILON = 0.000001; + /* frames should be 0.0, but use epsilon to avoid possible issues with impls + that may use ~0.0 silence values. */ + for (double frame : input_frames) { + ASSERT_LT(abs(frame), EPISILON); + } +} + +TEST(cubeb, loopback_silence) +{ + run_loopback_silence_test(true); + run_loopback_silence_test(false); +} + +void run_loopback_device_selection_test(bool is_float) +{ + cubeb * ctx; + cubeb_device_collection collection; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: device selection, separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr + cleanup_cubeb_at_exit(ctx, cubeb_destroy); + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Device enumeration not supported" + " for this backend, skipping this test.\n"); + return; + } + + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + /* get first preferred output device id */ + std::string device_id; + for (size_t i = 0; i < collection.count; i++) { + if (collection.device[i].preferred) { + device_id = collection.device[i].device_id; + break; + } + } + cubeb_device_collection_destroy(ctx, &collection); + if (device_id.empty()) { + fprintf(stderr, "Could not find preferred device, aborting test.\n"); + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + + std::unique_ptr user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + device_id.c_str(), &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only : data_cb_loop_input_only, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", + NULL, NULL, device_id.c_str(), &output_params, latency_frames, + is_float ? data_cb_playback : data_cb_playback, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(150); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease sanitization tools */ + std::lock_guard lock(user_data->user_state_mutex); + std::vector & output_frames = user_data->output_frames; + std::vector & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_output_frames(output_frames_signal_start, output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector trimmed_input_frames(input_frames_signal_start, input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_device_selection) +{ + run_loopback_device_selection_test(true); + run_loopback_device_selection_test(false); +} diff --git a/media/libcubeb/gtest/test_mixer.cpp b/media/libcubeb/gtest/test_mixer.cpp index fc9efefc551c..b7daea9a9a4a 100644 --- a/media/libcubeb/gtest/test_mixer.cpp +++ b/media/libcubeb/gtest/test_mixer.cpp @@ -114,7 +114,8 @@ downmix_test(float const * data, cubeb_channel_layout in_layout, cubeb_channel_l STREAM_FORMAT, STREAM_FREQUENCY, layout_infos[in_layout].channels, - in_layout + in_layout, + CUBEB_STREAM_PREF_NONE }; cubeb_stream_params out_params = { @@ -124,7 +125,8 @@ downmix_test(float const * data, cubeb_channel_layout in_layout, cubeb_channel_l // 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 + out_layout, + CUBEB_STREAM_PREF_NONE }; if (!cubeb_should_downmix(&in_params, &out_params)) { diff --git a/media/libcubeb/gtest/test_overload_callback.cpp b/media/libcubeb/gtest/test_overload_callback.cpp index 73982ef67819..05c01e848838 100644 --- a/media/libcubeb/gtest/test_overload_callback.cpp +++ b/media/libcubeb/gtest/test_overload_callback.cpp @@ -68,6 +68,7 @@ TEST(cubeb, overload_callback) output_params.rate = 48000; output_params.channels = 2; output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); ASSERT_EQ(r, CUBEB_OK); diff --git a/media/libcubeb/gtest/test_record.cpp b/media/libcubeb/gtest/test_record.cpp index b25eb8abd18c..635beeebdc48 100644 --- a/media/libcubeb/gtest/test_record.cpp +++ b/media/libcubeb/gtest/test_record.cpp @@ -94,6 +94,7 @@ TEST(cubeb, record) params.rate = SAMPLE_FREQUENCY; params.channels = 1; params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "Cubeb record (mono)", NULL, ¶ms, NULL, nullptr, 4096, data_cb_record, state_cb_record, &stream_state); diff --git a/media/libcubeb/gtest/test_resampler.cpp b/media/libcubeb/gtest/test_resampler.cpp index 1f5fb8f9e14c..a5c335f94a91 100644 --- a/media/libcubeb/gtest/test_resampler.cpp +++ b/media/libcubeb/gtest/test_resampler.cpp @@ -333,6 +333,7 @@ void test_resampler_duplex(uint32_t input_channels, uint32_t output_channels, input_params.rate = input_rate; state.output_rate = output_params.rate = output_rate; state.target_rate = target_rate; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; long got; cubeb_resampler * resampler = diff --git a/media/libcubeb/gtest/test_sanity.cpp b/media/libcubeb/gtest/test_sanity.cpp index db441d416488..d3546d6f0cbb 100644 --- a/media/libcubeb/gtest/test_sanity.cpp +++ b/media/libcubeb/gtest/test_sanity.cpp @@ -125,6 +125,8 @@ TEST(cubeb, context_variables) params.format = STREAM_FORMAT; params.rate = STREAM_RATE; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + r = cubeb_get_min_latency(ctx, ¶ms, &value); ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); if (r == CUBEB_OK) { @@ -160,6 +162,7 @@ TEST(cubeb, init_destroy_stream) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_data_callback, test_state_callback, &dummy); @@ -186,6 +189,7 @@ TEST(cubeb, init_destroy_multiple_streams) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; for (i = 0; i < ARRAY_LENGTH(stream); ++i) { r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, @@ -216,6 +220,7 @@ TEST(cubeb, configure_stream) params.rate = STREAM_RATE; params.channels = 2; // panning params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_data_callback, test_state_callback, &dummy); @@ -247,6 +252,7 @@ TEST(cubeb, configure_stream_undefined_layout) params.rate = STREAM_RATE; params.channels = 2; // panning params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_data_callback, test_state_callback, &dummy); @@ -282,6 +288,7 @@ test_init_start_stop_destroy_multiple_streams(int early, int delay_ms) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; for (i = 0; i < ARRAY_LENGTH(stream); ++i) { r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, @@ -366,6 +373,7 @@ TEST(cubeb, init_destroy_multiple_contexts_and_streams) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { r = common_init(&ctx[i], "test_sanity"); @@ -404,6 +412,7 @@ TEST(cubeb, basic_stream_operations) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_data_callback, test_state_callback, &dummy); @@ -452,6 +461,7 @@ TEST(cubeb, stream_position) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_data_callback, test_state_callback, &dummy); @@ -585,6 +595,7 @@ TEST(cubeb, drain) params.rate = STREAM_RATE; params.channels = STREAM_CHANNELS; params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, STREAM_LATENCY, test_drain_data_callback, test_drain_state_callback, &dummy); diff --git a/media/libcubeb/gtest/test_tone.cpp b/media/libcubeb/gtest/test_tone.cpp index 3cfc0b012865..09bc2a754d25 100644 --- a/media/libcubeb/gtest/test_tone.cpp +++ b/media/libcubeb/gtest/test_tone.cpp @@ -95,6 +95,7 @@ TEST(cubeb, tone) params.rate = SAMPLE_FREQUENCY; params.channels = 1; params.layout = CUBEB_LAYOUT_MONO; + params.prefs = CUBEB_STREAM_PREF_NONE; std::unique_ptr user_data(new cb_user_data()); ASSERT_TRUE(!!user_data) << "Error allocating user data"; diff --git a/media/libcubeb/include/cubeb.h b/media/libcubeb/include/cubeb.h index 6d3f195cfb4e..64f3a8d0f2e2 100644 --- a/media/libcubeb/include/cubeb.h +++ b/media/libcubeb/include/cubeb.h @@ -53,16 +53,18 @@ extern "C" { output_params.format = CUBEB_SAMPLE_FLOAT32NE; output_params.rate = rate; output_params.channels = 2; + output_params.prefs = CUBEB_STREAM_PREF_NONE; cubeb_stream_params input_params; input_params.format = CUBEB_SAMPLE_FLOAT32NE; input_params.rate = rate; input_params.channels = 1; + input_params.prefs = CUBEB_STREAM_PREF_NONE; cubeb_stream * stm; rv = cubeb_stream_init(app_ctx, &stm, "Example Stream 1", - NULL, input_params, - NULL, output_params, + NULL, &input_params, + NULL, &output_params, latency_frames, data_cb, state_cb, NULL); @@ -213,6 +215,15 @@ typedef enum { CUBEB_LAYOUT_MAX } cubeb_channel_layout; +/** Miscellaneous stream preferences. */ +typedef enum { + CUBEB_STREAM_PREF_NONE = 0x00, /**< No stream preferences are requested. */ + CUBEB_STREAM_PREF_LOOPBACK = 0x01 /**< Request a loopback stream. Should be + specified on the input params and an + output device to loopback from should + be passed in place of an input device. */ +} cubeb_stream_prefs; + /** Stream format initialization parameters. */ typedef struct { cubeb_sample_format format; /**< Requested sample format. One of @@ -220,6 +231,7 @@ typedef struct { uint32_t rate; /**< Requested sample rate. Valid range is [1000, 192000]. */ uint32_t channels; /**< Requested channel count. Valid range is [1, 8]. */ cubeb_channel_layout layout; /**< Requested channel layout. This must be consistent with the provided channels. */ + cubeb_stream_prefs prefs; /**< Requested preferences. */ } cubeb_stream_params; /** Audio device description */ diff --git a/media/libcubeb/src/cubeb_alsa.c b/media/libcubeb/src/cubeb_alsa.c index 8cb37ee96c77..f77d2522a862 100644 --- a/media/libcubeb/src/cubeb_alsa.c +++ b/media/libcubeb/src/cubeb_alsa.c @@ -890,6 +890,10 @@ alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream, char const * stream *stream = NULL; + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + switch (stream_params->format) { case CUBEB_SAMPLE_S16LE: format = SND_PCM_FORMAT_S16_LE; diff --git a/media/libcubeb/src/cubeb_audiounit.cpp b/media/libcubeb/src/cubeb_audiounit.cpp index c1e95ef7d7aa..0e137eda8b50 100644 --- a/media/libcubeb/src/cubeb_audiounit.cpp +++ b/media/libcubeb/src/cubeb_audiounit.cpp @@ -135,8 +135,8 @@ struct cubeb_stream { cubeb_device_changed_callback device_changed_callback = nullptr; owned_critical_section device_changed_callback_lock; /* Stream creation parameters */ - 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 }; + cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; device_info input_device; device_info output_device; /* User pointer of data_callback */ @@ -2360,6 +2360,12 @@ audiounit_setup_stream(cubeb_stream * stm) { stm->mutex.assert_current_thread_owns(); + if ((stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) || + (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK)) { + LOG("(%p) Loopback not supported for audiounit.", stm); + return CUBEB_ERROR_NOT_SUPPORTED; + } + int r = 0; device_info in_dev_info = stm->input_device; diff --git a/media/libcubeb/src/cubeb_jack.cpp b/media/libcubeb/src/cubeb_jack.cpp index 3b9574a3e8c1..f1a2d7847470 100644 --- a/media/libcubeb/src/cubeb_jack.cpp +++ b/media/libcubeb/src/cubeb_jack.cpp @@ -743,6 +743,12 @@ cbjack_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_ if (input_device || output_device) return CUBEB_ERROR_NOT_SUPPORTED; + // Loopback is unsupported + if ((input_stream_params && (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK)) || + (output_stream_params && (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK))) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + *stream = NULL; // Find a free stream. diff --git a/media/libcubeb/src/cubeb_opensl.c b/media/libcubeb/src/cubeb_opensl.c index 903906d046ed..4eaedbb888c4 100644 --- a/media/libcubeb/src/cubeb_opensl.c +++ b/media/libcubeb/src/cubeb_opensl.c @@ -1291,6 +1291,11 @@ opensl_validate_stream_param(cubeb_stream_params * stream_params) (stream_params->channels < 1 || stream_params->channels > 32))) { return CUBEB_ERROR_INVALID_FORMAT; } + if ((stream_params && + (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK))) { + LOG("Loopback is not supported"); + return CUBEB_ERROR_NOT_SUPPORTED; + } return CUBEB_OK; } diff --git a/media/libcubeb/src/cubeb_pulse.c b/media/libcubeb/src/cubeb_pulse.c index 19c2a84c590e..31ec534c5136 100644 --- a/media/libcubeb/src/cubeb_pulse.c +++ b/media/libcubeb/src/cubeb_pulse.c @@ -807,6 +807,9 @@ create_pa_stream(cubeb_stream * stm, (stream_params->layout == CUBEB_LAYOUT_UNDEFINED || (stream_params->layout != CUBEB_LAYOUT_UNDEFINED && CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].channels == stream_params->channels)))); + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; + } *pa_stm = NULL; pa_sample_spec ss; ss.format = to_pulse_format(stream_params->format); diff --git a/media/libcubeb/src/cubeb_sndio.c b/media/libcubeb/src/cubeb_sndio.c index c470a294c1fe..1699677e54fa 100644 --- a/media/libcubeb/src/cubeb_sndio.c +++ b/media/libcubeb/src/cubeb_sndio.c @@ -279,11 +279,19 @@ sndio_stream_init(cubeb * context, memset(s, 0, sizeof(cubeb_stream)); s->mode = 0; if (input_stream_params) { + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } s->mode |= SIO_REC; format = input_stream_params->format; rate = input_stream_params->rate; } if (output_stream_params) { + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } s->mode |= SIO_PLAY; format = output_stream_params->format; rate = output_stream_params->rate; diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp index 0efc6b192800..0de14348ae4f 100644 --- a/media/libcubeb/src/cubeb_wasapi.cpp +++ b/media/libcubeb/src/cubeb_wasapi.cpp @@ -193,12 +193,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_LAYOUT_UNDEFINED }; - cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED }; + cubeb_stream_params input_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; /* 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_LAYOUT_UNDEFINED }; - cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED }; + cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; /* The input and output device, or NULL for default. */ std::unique_ptr input_device; std::unique_ptr output_device; @@ -207,6 +207,10 @@ struct cubeb_stream { cubeb_state_callback state_callback = nullptr; cubeb_data_callback data_callback = nullptr; wasapi_refill_callback refill_callback = nullptr; + /* True when a loopback device is requested with no output device. In this + case a dummy output device is opened to drive the loopback, but should not + be exposed. */ + bool has_dummy_output = false; void * user_ptr = nullptr; /* Lifetime considerations: - client, render_client, audio_clock and audio_stream_volume are interface @@ -564,9 +568,9 @@ refill(cubeb_stream * stm, void * input_buffer, long input_frames_count, { XASSERT(!stm->draining); /* If we need to upmix after resampling, resample into the mix buffer to - avoid a copy. */ + avoid a copy. Avoid exposing output if it is a dummy stream. */ void * dest = nullptr; - if (has_output(stm)) { + if (has_output(stm) && !stm->has_dummy_output) { if (cubeb_should_mix(&stm->output_stream_params, &stm->output_mix_params)) { dest = stm->mix_buffer.data(); } else { @@ -595,9 +599,10 @@ refill(cubeb_stream * stm, void * input_buffer, long input_frames_count, /* If this is not true, there will be glitches. It is alright to have produced less frames if we are draining, though. */ - XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm)); + XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm) || stm->has_dummy_output); - if (has_output(stm) && cubeb_should_mix(&stm->output_stream_params, &stm->output_mix_params)) { + // We don't bother mixing dummy output as it will be silenced, otherwise mix output if needed + if (!stm->has_dummy_output && has_output(stm) && cubeb_should_mix(&stm->output_stream_params, &stm->output_mix_params)) { XASSERT(dest == stm->mix_buffer.data()); unsigned long dest_len = out_frames * stm->output_stream_params.channels; XASSERT(dest_len <= stm->mix_buffer.size() / stm->bytes_per_sample); @@ -612,22 +617,12 @@ refill(cubeb_stream * stm, void * input_buffer, long input_frames_count, /* This helper grabs all the frames available from a capture client, put them in * linear_input_buffer. linear_input_buffer should be cleared before the - * callback exits. */ + * callback exits. This helper does not work with exclusive mode streams. */ bool get_input_buffer(cubeb_stream * stm) { - HRESULT hr; - UINT32 padding_in; - XASSERT(has_input(stm)); - hr = stm->input_client->GetCurrentPadding(&padding_in); - if (FAILED(hr)) { - LOG("Failed to get padding"); - return false; - } - XASSERT(padding_in <= stm->input_buffer_frame_count); - UINT32 total_available_input = padding_in; - + HRESULT hr; BYTE * input_packet = NULL; DWORD flags; UINT64 dev_pos; @@ -635,17 +630,18 @@ bool get_input_buffer(cubeb_stream * stm) /* Get input packets until we have captured enough frames, and put them in a * contiguous buffer. */ uint32_t offset = 0; - while (offset != total_available_input) { - hr = stm->capture_client->GetNextPacketSize(&next); + // If the input stream is event driven we should only ever expect to read a + // single packet each time. However, if we're pulling from the stream we may + // need to grab multiple packets worth of frames that have accumulated (so + // need a loop). + for (hr = stm->capture_client->GetNextPacketSize(&next); + next > 0; + hr = stm->capture_client->GetNextPacketSize(&next)) { + if (FAILED(hr)) { LOG("cannot get next packet size: %lx", hr); return false; } - /* This can happen if the capture stream has stopped. Just return in this - * case. */ - if (!next) { - break; - } UINT32 packet_size; hr = stm->capture_client->GetBuffer(&input_packet, @@ -658,6 +654,21 @@ bool get_input_buffer(cubeb_stream * stm) return false; } XASSERT(packet_size == next); + // We do not explicitly handle the AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY + // flag. There a two primary (non exhaustive) scenarios we anticipate this + // flag being set in: + // - The first GetBuffer after Start has this flag undefined. In this + // case the flag may be set but is meaningless and can be ignored. + // - If a glitch is introduced into the input. This should not happen + // for event based inputs, and should be mitigated by using a dummy + // stream to drive input in the case of input only loopback. Without + // a dummy output, input only loopback would glitch on silence. However, + // the dummy input should push silence to the loopback and prevent + // discontinuities. See https://blogs.msdn.microsoft.com/matthew_van_eerde/2008/12/16/sample-wasapi-loopback-capture-record-what-you-hear/ + // As the first scenario can be ignored, and we anticipate the second + // scenario is mitigated, we ignore the flag. + // For more info: https://msdn.microsoft.com/en-us/library/windows/desktop/dd370859(v=vs.85).aspx, + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd371458(v=vs.85).aspx if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { LOG("insert silence: ps=%u", packet_size); stm->linear_input_buffer->push_silence(packet_size * stm->input_stream_params.channels); @@ -687,8 +698,7 @@ bool get_input_buffer(cubeb_stream * stm) offset += packet_size; } - XASSERT(stm->linear_input_buffer->length() >= total_available_input && - offset == total_available_input); + XASSERT(stm->linear_input_buffer->length() >= offset); return true; } @@ -774,18 +784,36 @@ refill_callback_duplex(cubeb_stream * stm) return false; } - ALOGV("Duplex callback: input frames: %Iu, output frames: %Iu", - input_frames, output_frames); + if (stm->has_dummy_output) { + ALOGV("Duplex callback (dummy output): input frames: %Iu, output frames: %Iu", + input_frames, output_frames); - refill(stm, - stm->linear_input_buffer->data(), - input_frames, - output_buffer, - output_frames); + // We don't want to expose the dummy output to the callback so don't pass + // the output buffer (it will be released later with silence in it) + refill(stm, + stm->linear_input_buffer->data(), + input_frames, + nullptr, + 0); + } else { + ALOGV("Duplex callback: input frames: %Iu, output frames: %Iu", + input_frames, output_frames); + + refill(stm, + stm->linear_input_buffer->data(), + input_frames, + output_buffer, + output_frames); + } stm->linear_input_buffer->clear(); - hr = stm->render_client->ReleaseBuffer(output_frames, 0); + if (stm->has_dummy_output) { + // If output is a dummy output, make sure it's silent + hr = stm->render_client->ReleaseBuffer(output_frames, AUDCLNT_BUFFERFLAGS_SILENT); + } else { + hr = stm->render_client->ReleaseBuffer(output_frames, 0); + } if (FAILED(hr)) { LOG("failed to release buffer: %lx", hr); return false; @@ -1517,6 +1545,11 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, { com_ptr device; HRESULT hr; + bool is_loopback = stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK; + if (is_loopback && direction != eCapture) { + LOG("Loopback pref can only be used with capture streams!\n"); + return CUBEB_ERROR; + } stm->stream_reset_lock.assert_current_thread_owns(); bool try_again = false; @@ -1530,9 +1563,16 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, return CUBEB_ERROR; } } else { - hr = get_default_endpoint(device, direction); + // If caller has requested loopback but not specified a device, look for + // the default render device. Otherwise look for the default device + // appropriate to the direction. + hr = get_default_endpoint(device, is_loopback ? eRender : direction); if (FAILED(hr)) { - LOG("Could not get default %s endpoint, error: %lx\n", DIRECTION_NAME, hr); + if (is_loopback) { + LOG("Could not get default render endpoint for loopback, error: %lx\n", hr); + } else { + LOG("Could not get default %s endpoint, error: %lx\n", DIRECTION_NAME, hr); + } return CUBEB_ERROR; } } @@ -1603,9 +1643,18 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, mix_params->format, mix_params->rate, mix_params->channels, CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].name); + DWORD flags = AUDCLNT_STREAMFLAGS_NOPERSIST; + + // Check if a loopback device should be requested. Note that event callbacks + // do not work with loopback devices, so only request these if not looping. + if (is_loopback) { + flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; + } else { + flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + } + hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - AUDCLNT_STREAMFLAGS_NOPERSIST, + flags, frames_to_hns(stm, stm->latency), 0, mix_format.get(), @@ -1626,11 +1675,14 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, stm->mix_buffer.resize(frames_to_bytes_before_mix(stm, *buffer_frame_count)); } - hr = audio_client->SetEventHandle(event); - if (FAILED(hr)) { - LOG("Could set the event handle for the %s client %lx.", - DIRECTION_NAME, hr); - return CUBEB_ERROR; + // Events are used if not looping back + if (!is_loopback) { + hr = audio_client->SetEventHandle(event); + if (FAILED(hr)) { + LOG("Could set the event handle for the %s client %lx.", + DIRECTION_NAME, hr); + return CUBEB_ERROR; + } } hr = audio_client->GetService(riid, render_or_capture_client.receive_vpp()); @@ -1691,6 +1743,27 @@ int setup_wasapi_stream(cubeb_stream * stm) silent_buffer_count); } + // If we don't have an output device but are requesting a loopback device, + // we attempt to open that same device in output mode in order to drive the + // loopback via the output events. + stm->has_dummy_output = false; + if (!has_output(stm) && stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) { + stm->output_stream_params.rate = stm->input_stream_params.rate; + stm->output_stream_params.channels = stm->input_stream_params.channels; + stm->output_stream_params.layout = stm->input_stream_params.layout; + if (stm->input_device) { + size_t len = wcslen(stm->input_device.get()); + std::unique_ptr tmp(new wchar_t[len + 1]); + if (wcsncpy_s(tmp.get(), len + 1, stm->input_device.get(), len) != 0) { + LOG("Failed to copy device identifier while copying input stream" + " configuration to output stream configuration to drive loopback."); + return CUBEB_ERROR; + } + stm->output_device = move(tmp); + } + stm->has_dummy_output = true; + } + if (has_output(stm)) { LOG("(%p) Setup render: device=%p", stm, stm->output_device.get()); rv = setup_wasapi_stream_one_side(stm, diff --git a/media/libcubeb/src/cubeb_winmm.c b/media/libcubeb/src/cubeb_winmm.c index b5aab51e8316..e62eed640ec6 100644 --- a/media/libcubeb/src/cubeb_winmm.c +++ b/media/libcubeb/src/cubeb_winmm.c @@ -414,6 +414,11 @@ winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n return CUBEB_ERROR_DEVICE_UNAVAILABLE; } + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + /* Loopback is not supported */ + return CUBEB_ERROR_NOT_SUPPORTED; + } + *stream = NULL; memset(&wfx, 0, sizeof(wfx));