// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Tests PPB_MediaStreamAudioTrack interface. #include "ppapi/tests/test_media_stream_audio_track.h" // For MSVC. #define _USE_MATH_DEFINES #include #include #include #include "ppapi/c/private/ppb_testing_private.h" #include "ppapi/cpp/audio_buffer.h" #include "ppapi/cpp/completion_callback.h" #include "ppapi/cpp/instance.h" #include "ppapi/cpp/var.h" #include "ppapi/tests/test_utils.h" #include "ppapi/tests/testing_instance.h" REGISTER_TEST_CASE(MediaStreamAudioTrack); namespace { // Real constants defined in // content/renderer/pepper/pepper_media_stream_audio_track_host.cc. const int32_t kMaxNumberOfBuffers = 1000; const int32_t kMinDuration = 10; const int32_t kMaxDuration = 10000; const int32_t kTimes = 3; const char kJSCode[] = "function gotStream(stream) {" " test_stream = stream;" " var track = stream.getAudioTracks()[0];" " var plugin = document.getElementById('plugin');" " plugin.postMessage(track);" "}" "var constraints = {" " audio: true," " video: false," "};" "navigator.getUserMedia = " " navigator.getUserMedia || navigator.webkitGetUserMedia;" "navigator.getUserMedia(constraints," " gotStream, function() {});"; const char kSineJSCode[] = // Create oscillators for the left and right channels. Use a sine wave, // which is the easiest to calculate expected values. The oscillator output // is low-pass filtered (as per spec) making comparison hard. "var context = new AudioContext();" "var l_osc = context.createOscillator();" "l_osc.type = \"sine\";" "l_osc.frequency.value = 25;" "var r_osc = context.createOscillator();" "r_osc.type = \"sine\";" "r_osc.frequency.value = 100;" // Combine the left and right channels. "var merger = context.createChannelMerger(2);" "merger.channelInterpretation = \"discrete\";" "l_osc.connect(merger, 0, 0);" "r_osc.connect(merger, 0, 1);" "var dest_stream = context.createMediaStreamDestination();" "merger.connect(dest_stream);" // Dump the generated waveform to a MediaStream output. "l_osc.start();" "r_osc.start();" "var track = dest_stream.stream.getAudioTracks()[0];" "var plugin = document.getElementById('plugin');" "plugin.postMessage(track);"; // Helper to check if the |sample_rate| is listed in PP_AudioBuffer_SampleRate // enum. bool IsSampleRateValid(PP_AudioBuffer_SampleRate sample_rate) { switch (sample_rate) { case PP_AUDIOBUFFER_SAMPLERATE_8000: case PP_AUDIOBUFFER_SAMPLERATE_16000: case PP_AUDIOBUFFER_SAMPLERATE_22050: case PP_AUDIOBUFFER_SAMPLERATE_32000: case PP_AUDIOBUFFER_SAMPLERATE_44100: case PP_AUDIOBUFFER_SAMPLERATE_48000: case PP_AUDIOBUFFER_SAMPLERATE_96000: case PP_AUDIOBUFFER_SAMPLERATE_192000: return true; default: return false; } } } // namespace TestMediaStreamAudioTrack::TestMediaStreamAudioTrack(TestingInstance* instance) : TestCase(instance), event_(instance_->pp_instance()) { } bool TestMediaStreamAudioTrack::Init() { return true; } TestMediaStreamAudioTrack::~TestMediaStreamAudioTrack() { } void TestMediaStreamAudioTrack::RunTests(const std::string& filter) { RUN_TEST(Create, filter); RUN_TEST(GetBuffer, filter); RUN_TEST(Configure, filter); RUN_TEST(ConfigureClose, filter); RUN_TEST(VerifyWaveform, filter); } void TestMediaStreamAudioTrack::HandleMessage(const pp::Var& message) { if (message.is_resource()) { audio_track_ = pp::MediaStreamAudioTrack(message.AsResource()); } event_.Signal(); } std::string TestMediaStreamAudioTrack::TestCreate() { // Create a track. instance_->EvalScript(kJSCode); event_.Wait(); event_.Reset(); ASSERT_FALSE(audio_track_.is_null()); ASSERT_FALSE(audio_track_.HasEnded()); ASSERT_FALSE(audio_track_.GetId().empty()); // Close the track. audio_track_.Close(); ASSERT_TRUE(audio_track_.HasEnded()); audio_track_ = pp::MediaStreamAudioTrack(); PASS(); } std::string TestMediaStreamAudioTrack::TestGetBuffer() { // Create a track. instance_->EvalScript(kJSCode); event_.Wait(); event_.Reset(); ASSERT_FALSE(audio_track_.is_null()); ASSERT_FALSE(audio_track_.HasEnded()); ASSERT_FALSE(audio_track_.GetId().empty()); PP_TimeDelta timestamp = 0.0; // Get |kTimes| buffers. for (int i = 0; i < kTimes; ++i) { TestCompletionCallbackWithOutput cc( instance_->pp_instance(), false); cc.WaitForResult(audio_track_.GetBuffer(cc.GetCallback())); ASSERT_EQ(PP_OK, cc.result()); pp::AudioBuffer buffer = cc.output(); ASSERT_FALSE(buffer.is_null()); ASSERT_TRUE(IsSampleRateValid(buffer.GetSampleRate())); ASSERT_EQ(buffer.GetSampleSize(), PP_AUDIOBUFFER_SAMPLESIZE_16_BITS); ASSERT_GE(buffer.GetTimestamp(), timestamp); timestamp = buffer.GetTimestamp(); ASSERT_GT(buffer.GetDataBufferSize(), 0U); ASSERT_TRUE(buffer.GetDataBuffer() != NULL); audio_track_.RecycleBuffer(buffer); // A recycled buffer should be invalidated. ASSERT_EQ(buffer.GetSampleRate(), PP_AUDIOBUFFER_SAMPLERATE_UNKNOWN); ASSERT_EQ(buffer.GetSampleSize(), PP_AUDIOBUFFER_SAMPLESIZE_UNKNOWN); ASSERT_EQ(buffer.GetDataBufferSize(), 0U); ASSERT_TRUE(buffer.GetDataBuffer() == NULL); } // Close the track. audio_track_.Close(); ASSERT_TRUE(audio_track_.HasEnded()); audio_track_ = pp::MediaStreamAudioTrack(); PASS(); } std::string TestMediaStreamAudioTrack::CheckConfigure( int32_t attrib_list[], int32_t expected_result) { TestCompletionCallback cc_configure(instance_->pp_instance(), false); cc_configure.WaitForResult( audio_track_.Configure(attrib_list, cc_configure.GetCallback())); ASSERT_EQ(expected_result, cc_configure.result()); PASS(); } std::string TestMediaStreamAudioTrack::CheckGetBuffer( int times, int expected_duration) { PP_TimeDelta timestamp = 0.0; for (int j = 0; j < times; ++j) { TestCompletionCallbackWithOutput cc_get_buffer( instance_->pp_instance(), false); cc_get_buffer.WaitForResult( audio_track_.GetBuffer(cc_get_buffer.GetCallback())); ASSERT_EQ(PP_OK, cc_get_buffer.result()); pp::AudioBuffer buffer = cc_get_buffer.output(); ASSERT_FALSE(buffer.is_null()); ASSERT_TRUE(IsSampleRateValid(buffer.GetSampleRate())); ASSERT_EQ(buffer.GetSampleSize(), PP_AUDIOBUFFER_SAMPLESIZE_16_BITS); ASSERT_GE(buffer.GetTimestamp(), timestamp); timestamp = buffer.GetTimestamp(); ASSERT_TRUE(buffer.GetDataBuffer() != NULL); if (expected_duration > 0) { uint32_t buffer_size = buffer.GetDataBufferSize(); uint32_t channels = buffer.GetNumberOfChannels(); uint32_t sample_rate = buffer.GetSampleRate(); uint32_t bytes_per_frame = channels * 2; int32_t duration = expected_duration; ASSERT_EQ(buffer_size % bytes_per_frame, 0U); ASSERT_EQ(buffer_size, (duration * sample_rate * bytes_per_frame) / 1000); } else { ASSERT_GT(buffer.GetDataBufferSize(), 0U); } audio_track_.RecycleBuffer(buffer); } PASS(); } std::string TestMediaStreamAudioTrack::TestConfigure() { // Create a track. instance_->EvalScript(kJSCode); event_.Wait(); event_.Reset(); ASSERT_FALSE(audio_track_.is_null()); ASSERT_FALSE(audio_track_.HasEnded()); ASSERT_FALSE(audio_track_.GetId().empty()); // Perform a |Configure()| with no attributes. This ends up making an IPC // call, but the host implementation has a fast-path when there are no changes // to the configuration. This test is intended to hit that fast-path and make // sure it works correctly. { int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, PP_OK)); } // Configure number of buffers. struct { int32_t buffers; int32_t expect_result; } buffers[] = { { 8, PP_OK }, { 100, PP_OK }, { kMaxNumberOfBuffers, PP_OK }, { -1, PP_ERROR_BADARGUMENT }, { kMaxNumberOfBuffers + 1, PP_OK }, // Clipped to max value. { 0, PP_OK }, // Use default. }; for (size_t i = 0; i < sizeof(buffers) / sizeof(buffers[0]); ++i) { int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_BUFFERS, buffers[i].buffers, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, buffers[i].expect_result)); // Get some buffers. This should also succeed when configure fails. ASSERT_SUBTEST_SUCCESS(CheckGetBuffer(kTimes, -1)); } // Configure buffer duration. struct { int32_t duration; int32_t expect_result; } durations[] = { { kMinDuration, PP_OK }, { 123, PP_OK }, { kMinDuration - 1, PP_ERROR_BADARGUMENT }, { kMaxDuration + 1, PP_ERROR_BADARGUMENT }, }; for (size_t i = 0; i < sizeof(durations) / sizeof(durations[0]); ++i) { int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_DURATION, durations[i].duration, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, durations[i].expect_result)); // Get some buffers. This always works, but the buffer size will vary. int duration = durations[i].expect_result == PP_OK ? durations[i].duration : -1; ASSERT_SUBTEST_SUCCESS(CheckGetBuffer(kTimes, duration)); } // Test kMaxDuration separately since each GetBuffer will take 10 seconds. { int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_DURATION, kMaxDuration, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, PP_OK)); } // Reset the duration to prevent the next part from taking 10 seconds. { int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_DURATION, kMinDuration, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, PP_OK)); } // Configure should fail while plugin holds buffers. { TestCompletionCallbackWithOutput cc_get_buffer( instance_->pp_instance(), false); cc_get_buffer.WaitForResult( audio_track_.GetBuffer(cc_get_buffer.GetCallback())); ASSERT_EQ(PP_OK, cc_get_buffer.result()); pp::AudioBuffer buffer = cc_get_buffer.output(); int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_BUFFERS, 0, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; TestCompletionCallback cc_configure(instance_->pp_instance(), false); cc_configure.WaitForResult( audio_track_.Configure(attrib_list, cc_configure.GetCallback())); ASSERT_EQ(PP_ERROR_INPROGRESS, cc_configure.result()); audio_track_.RecycleBuffer(buffer); } // Close the track. audio_track_.Close(); ASSERT_TRUE(audio_track_.HasEnded()); audio_track_ = pp::MediaStreamAudioTrack(); PASS(); } std::string TestMediaStreamAudioTrack::TestConfigureClose() { // Create a track. instance_->EvalScript(kJSCode); event_.Wait(); event_.Reset(); ASSERT_FALSE(audio_track_.is_null()); ASSERT_FALSE(audio_track_.HasEnded()); ASSERT_FALSE(audio_track_.GetId().empty()); // Configure the audio track and close it immediately. The Configure() call // should complete. int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_BUFFERS, 10, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; TestCompletionCallback cc_configure(instance_->pp_instance(), false); int32_t result = audio_track_.Configure(attrib_list, cc_configure.GetCallback()); ASSERT_EQ(PP_OK_COMPLETIONPENDING, result); audio_track_.Close(); cc_configure.WaitForResult(result); result = cc_configure.result(); // Unfortunately, we can't control whether the configure succeeds or is // aborted. ASSERT_TRUE(result == PP_OK || result == PP_ERROR_ABORTED); PASS(); } uint32_t CalculateWaveStartingTime(int16_t sample, int16_t next_sample, uint32_t period) { int16_t slope = next_sample - sample; double angle = asin(sample / (double)INT16_MAX); if (slope < 0) { angle = M_PI - angle; } if (angle < 0) { angle += 2 * M_PI; } return round(angle * period / (2 * M_PI)); } std::string TestMediaStreamAudioTrack::TestVerifyWaveform() { // Create a track. instance_->EvalScript(kSineJSCode); event_.Wait(); event_.Reset(); ASSERT_FALSE(audio_track_.is_null()); ASSERT_FALSE(audio_track_.HasEnded()); ASSERT_FALSE(audio_track_.GetId().empty()); // Use a weird buffer length and number of buffers. const int32_t kBufferSize = 13; const int32_t kNumBuffers = 3; const uint32_t kChannels = 2; const uint32_t kFreqLeft = 25; const uint32_t kFreqRight = 100; int32_t attrib_list[] = { PP_MEDIASTREAMAUDIOTRACK_ATTRIB_DURATION, kBufferSize, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_BUFFERS, kNumBuffers, PP_MEDIASTREAMAUDIOTRACK_ATTRIB_NONE, }; ASSERT_SUBTEST_SUCCESS(CheckConfigure(attrib_list, PP_OK)); // Get kNumBuffers buffers and verify they conform to the expected waveform. PP_TimeDelta timestamp = 0.0; int sample_time = 0; uint32_t left_start = 0; uint32_t right_start = 0; for (int j = 0; j < kNumBuffers; ++j) { TestCompletionCallbackWithOutput cc_get_buffer( instance_->pp_instance(), false); cc_get_buffer.WaitForResult( audio_track_.GetBuffer(cc_get_buffer.GetCallback())); ASSERT_EQ(PP_OK, cc_get_buffer.result()); pp::AudioBuffer buffer = cc_get_buffer.output(); ASSERT_FALSE(buffer.is_null()); ASSERT_TRUE(IsSampleRateValid(buffer.GetSampleRate())); ASSERT_EQ(buffer.GetSampleSize(), PP_AUDIOBUFFER_SAMPLESIZE_16_BITS); ASSERT_EQ(buffer.GetNumberOfChannels(), kChannels); ASSERT_GE(buffer.GetTimestamp(), timestamp); timestamp = buffer.GetTimestamp(); uint32_t buffer_size = buffer.GetDataBufferSize(); uint32_t sample_rate = buffer.GetSampleRate(); uint32_t num_samples = buffer.GetNumberOfSamples(); uint32_t bytes_per_frame = kChannels * 2; ASSERT_EQ(num_samples, (kChannels * kBufferSize * sample_rate) / 1000); ASSERT_EQ(buffer_size % bytes_per_frame, 0U); ASSERT_EQ(buffer_size, num_samples * 2); // Period of sine wave, in samples. uint32_t left_period = sample_rate / kFreqLeft; uint32_t right_period = sample_rate / kFreqRight; int16_t* data_buffer = static_cast(buffer.GetDataBuffer()); ASSERT_TRUE(data_buffer != NULL); if (j == 0) { // The generated wave doesn't necessarily start at 0, so compensate for // this. left_start = CalculateWaveStartingTime(data_buffer[0], data_buffer[2], left_period); right_start = CalculateWaveStartingTime(data_buffer[1], data_buffer[3], right_period); } for (uint32_t sample = 0; sample < num_samples; sample += 2, sample_time++) { int16_t left = data_buffer[sample]; int16_t right = data_buffer[sample + 1]; double angle = (2.0 * M_PI * ((sample_time + left_start) % left_period)) / left_period; int16_t expected = INT16_MAX * sin(angle); // Account for off-by-one errors due to rounding. ASSERT_GE(left, std::max(expected, INT16_MIN + 1) - 1); ASSERT_LE(left, std::min(expected, INT16_MAX - 1) + 1); angle = (2 * M_PI * ((sample_time + right_start) % right_period)) / right_period; expected = INT16_MAX * sin(angle); ASSERT_GE(right, std::max(expected, INT16_MIN + 1) - 1); ASSERT_LE(right, std::min(expected, INT16_MAX - 1) + 1); } audio_track_.RecycleBuffer(buffer); } PASS(); }