Chromium Code Reviews| Index: examples/audio_play_test/play_wav.cc |
| diff --git a/examples/audio_play_test/play_wav.cc b/examples/audio_play_test/play_wav.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..599ffd20039c8f23662bf019d73cc39e714b31fa |
| --- /dev/null |
| +++ b/examples/audio_play_test/play_wav.cc |
| @@ -0,0 +1,493 @@ |
| +// Copyright 2015 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. |
| + |
| +#include <memory> |
| + |
| +#include "base/bind.h" |
| +#include "base/logging.h" |
| +#include "mojo/public/c/system/main.h" |
| +#include "mojo/public/cpp/application/application_delegate.h" |
| +#include "mojo/public/cpp/application/application_impl.h" |
| +#include "mojo/public/cpp/application/application_runner.h" |
| +#include "mojo/public/cpp/system/data_pipe.h" |
| +#include "mojo/public/cpp/utility/run_loop.h" |
| +#include "mojo/services/media/audio/interfaces/audio_server.mojom.h" |
| +#include "mojo/services/media/audio/interfaces/audio_track.mojom.h" |
| +#include "mojo/services/media/common/cpp/circular_buffer_media_pipe_adapter.h" |
| +#include "mojo/services/media/common/cpp/linear_transform.h" |
| +#include "mojo/services/media/common/cpp/local_time.h" |
| +#include "mojo/services/network/interfaces/network_service.mojom.h" |
| +#include "mojo/services/network/interfaces/url_loader.mojom.h" |
| + |
| +#define PACKED __attribute__((packed)) |
| + |
| +namespace mojo { |
| +namespace media { |
| +namespace audio { |
| +namespace examples { |
| + |
| +#if 0 |
| +static constexpr const char* TEST_FILE = |
| + "http://localhost/test_content/piano2.wav"; |
| +#else |
| +static constexpr const char* TEST_FILE = |
| + "http://www.thesoundarchive.com/starwars/swvader04.wav"; |
|
jeffbrown
2015/11/04 19:34:44
Is this copyrighted material?
johngro
2015/11/05 00:25:16
The URL? I don't think that you can copyright a U
|
| +#endif |
| +static constexpr uint32_t BUF_DEPTH_USEC = 500000; |
| +static constexpr uint32_t BUF_LO_WATER_USEC = 400000; |
| +static constexpr uint32_t BUF_HI_WATER_USEC = 450000; |
| +static constexpr uint32_t CHUNK_SIZE_USEC = 10000; |
| + |
| +class PlayWAVApp : public ApplicationDelegate { |
| + public: |
| + ~PlayWAVApp() override { Quit(); } |
| + |
| + // ApplicationDelegate |
| + void Initialize(ApplicationImpl* app) override; |
| + void Quit() override; |
| + |
| + private: |
| + using AudioPipePtr = std::unique_ptr<CircularBufferMediaPipeAdapter>; |
| + using AudioPacket = CircularBufferMediaPipeAdapter::MappedPacket; |
| + using PacketCbk = MediaPipe::SendPacketCallback; |
| + |
| + // TODO(johngro): endianness! |
| + struct PACKED RIFFChunkHeader { |
| + uint32_t four_cc; |
| + uint32_t length; |
| + }; |
| + |
| + struct PACKED WAVHeader { |
| + uint32_t wave_four_cc; |
| + uint32_t fmt_four_cc; |
| + uint32_t fmt_chunk_len; |
| + uint16_t format; |
| + uint16_t channel_count; |
| + uint32_t frame_rate; |
| + uint32_t average_byte_rate; |
| + uint16_t frame_size; |
| + uint16_t bits_per_sample; |
| + }; |
| + |
| + // TODO(johngro): as mentioned before... endianness! |
|
jeffbrown
2015/11/04 19:34:44
Mentioned where?
johngro
2015/11/05 00:25:16
line 55.
|
| + static constexpr uint32_t RIFF_FOUR_CC = 'FFIR'; |
| + static constexpr uint32_t WAVE_FOUR_CC = 'EVAW'; |
| + static constexpr uint32_t FMT_FOUR_CC = ' tmf'; |
| + static constexpr uint32_t DATA_FOUR_CC = 'atad'; |
| + |
| + static constexpr uint16_t FORMAT_LPCM = 0x0001; |
| + static constexpr uint16_t FORMAT_MULAW = 0x0101; |
| + static constexpr uint16_t FORMAT_ALAW = 0x0102; |
| + static constexpr uint16_t FORMAT_ADPCM = 0x0103; |
| + |
| + static const std::set<std::string> VALID_MIME_TYPES; |
| + static const std::set<uint16_t> VALID_FRAME_RATES; |
| + static const std::set<uint16_t> VALID_BITS_PER_SAMPLES; |
| + |
| + bool BlockingRead(void* buf, uint32_t len); |
| + void ProcessHTTPResponse(URLResponsePtr resp); |
| + void PlayWAV(); |
| + |
| + void OnAudioConfigured(MediaResult res); |
| + void OnHasRateControl(MediaResult res); |
| + bool OnNeedsData(MediaResult res); |
| + void OnPlayoutComplete(MediaResult res); |
| + |
| + uint32_t USecToFrames(uint32_t usec) { |
| + uint64_t ret = (static_cast<uint64_t>(usec) * wav_info_.frame_rate) |
| + / 1000000; |
| + DCHECK_LT(ret, std::numeric_limits<uint32_t>::max()); |
| + return ret; |
| + } |
| + |
| + uint32_t USecToBytes(uint32_t usec) { |
| + uint32_t frames = USecToFrames(usec); |
| + |
| + DCHECK(wav_info_.frame_size); |
| + DCHECK_LT(frames, |
| + std::numeric_limits<uint32_t>::max() / wav_info_.frame_size); |
| + |
| + return frames * wav_info_.frame_size; |
| + } |
| + |
| + AudioServerPtr audio_server_; |
| + AudioTrackPtr audio_track_; |
| + AudioPipePtr audio_pipe_; |
| + MediaPipePtr media_pipe_; |
| + RateControlPtr rate_control_; |
| + AudioPacket audio_packet_; |
| + PacketCbk playout_complete_cbk_; |
| + NetworkServicePtr network_service_; |
| + URLLoaderPtr url_loader_; |
| + ScopedDataPipeConsumerHandle payload_; |
| + uint32_t payload_len_; |
| + WAVHeader wav_info_; |
| + bool sent_first_packet_ = false; |
| + bool clock_started_ = false; |
| +}; |
| + |
| +const std::set<std::string> PlayWAVApp::VALID_MIME_TYPES({ |
| + "audio/x-wav", |
| + "audio/wav", |
| +}); |
| + |
| +const std::set<uint16_t> PlayWAVApp::VALID_FRAME_RATES({ |
| + 8000, 16000, 24000, 32000, 48000, |
| + 11025, 22050, 44100, |
| +}); |
| + |
| +const std::set<uint16_t> PlayWAVApp::VALID_BITS_PER_SAMPLES({ |
| + 8, 16, |
| +}); |
| + |
| +void PlayWAVApp::Initialize(ApplicationImpl* app) { |
| + app->ConnectToService("mojo:audio_server", &audio_server_); |
| + app->ConnectToService("mojo:network_service", &network_service_); |
| + |
| + network_service_->CreateURLLoader(GetProxy(&url_loader_)); |
| + |
| + playout_complete_cbk_ = PacketCbk([this](MediaResult res) { |
| + this->OnPlayoutComplete(res); |
| + }); |
| + |
| + URLRequestPtr req(URLRequest::New()); |
| + req->url = TEST_FILE; |
| + req->method = "GET"; |
| + |
| + auto cbk = [this](URLResponsePtr resp) { ProcessHTTPResponse(resp.Pass()); }; |
| + url_loader_->Start(req.Pass(), URLLoader::StartCallback(cbk)); |
| +} |
| + |
| +void PlayWAVApp::Quit() { |
| + if (audio_packet_.packet()) { |
| + DCHECK(audio_pipe_); |
| + audio_pipe_->CancelMediaPacket(&audio_packet_); |
| + } |
| + |
| + payload_.reset(); |
| + url_loader_.reset(); |
| + network_service_.reset(); |
| + media_pipe_.reset(); |
| + audio_pipe_.reset(); |
| + audio_track_.reset(); |
| + audio_server_.reset(); |
| +} |
| + |
| +bool PlayWAVApp::BlockingRead(void* buf, uint32_t op_len) { |
| + MojoResult res; |
| + uint32_t amt; |
| + |
| + while (true) { |
| + amt = op_len; |
| + res = ReadDataRaw(payload_.get(), buf, &amt, |
| + MOJO_READ_DATA_FLAG_ALL_OR_NONE); |
| + |
| + if ((res == MOJO_RESULT_SHOULD_WAIT) || |
| + (res == MOJO_RESULT_OUT_OF_RANGE)) { |
| + Wait(payload_.get(), |
|
jeffbrown
2015/11/04 19:34:44
Isn't there a non-blocking way to do this?
johngro
2015/11/05 00:25:16
yes; if I understand correctly it involves creatin
|
| + MOJO_HANDLE_SIGNAL_READABLE, |
| + MOJO_DEADLINE_INDEFINITE, |
| + nullptr); |
| + continue; |
| + } |
| + |
| + break; |
| + } |
| + |
| + return ((res == MOJO_RESULT_OK) && (amt == op_len)); |
| +} |
| + |
| +void PlayWAVApp::ProcessHTTPResponse(URLResponsePtr resp) { |
| + if (resp->mime_type.is_null() || |
| + (VALID_MIME_TYPES.find(resp->mime_type) == VALID_MIME_TYPES.end())) { |
| + LOG(ERROR) << "Bad MimeType \"" |
| + << (resp->mime_type.is_null() ? "<null>" : resp->mime_type) |
| + << "\""; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + payload_ = resp->body.Pass(); |
| + |
| + // Read and sanity check the top level RIFF header |
| + RIFFChunkHeader riff_hdr; |
| + if (!BlockingRead(&riff_hdr, sizeof(riff_hdr))) { |
| + LOG(ERROR) << "Failed to read top level RIFF header!"; |
| + RunLoop::current()->Quit(); |
|
jeffbrown
2015/11/04 19:34:44
This is telling me that Mojo needs a better patter
johngro
2015/11/05 00:25:16
Acknowledged.
There is some discussion on the sub
|
| + return; |
| + } |
| + |
| + if (riff_hdr.four_cc != RIFF_FOUR_CC) { |
| + LOG(ERROR) << "Missing expected 'RIFF' 4CC " |
| + << "(expected 0x " << std::hex << RIFF_FOUR_CC |
| + << " got 0x" << std::hex << riff_hdr.four_cc |
| + << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + // Now read the WAVE header along with its required format chunk. |
| + if (!BlockingRead(&wav_info_, sizeof(wav_info_))) { |
| + LOG(ERROR) << "Failed to read top level WAVE header!"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if (wav_info_.wave_four_cc != WAVE_FOUR_CC) { |
| + LOG(ERROR) << "Missing expected 'WAVE' 4CC " |
| + << "(expected 0x " << std::hex << WAVE_FOUR_CC |
| + << " got 0x" << std::hex << wav_info_.wave_four_cc |
| + << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + // Sanity check the format of the wave file. This demo only support a limited |
| + // subset of the possible formats. |
| + if (wav_info_.format != FORMAT_LPCM) { |
| + LOG(ERROR) << "Unsupported format (0x" |
| + << std::hex << wav_info_.format |
| + << ") must be LPCM (0x" |
| + << std::hex << FORMAT_LPCM |
| + << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if ((wav_info_.channel_count != 1) && (wav_info_.channel_count != 2)) { |
| + LOG(ERROR) << "Unsupported channel count (" |
| + << wav_info_.channel_count |
| + << ") must be either mono or stereo"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if ((wav_info_.channel_count != 1) && (wav_info_.channel_count != 2)) { |
|
jeffbrown
2015/11/04 19:34:44
duplicate code
johngro
2015/11/05 00:25:16
Done.
good catch.
|
| + LOG(ERROR) << "Unsupported channel count (" |
| + << wav_info_.channel_count |
| + << ") must be either mono or stereo"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if (VALID_FRAME_RATES.find(wav_info_.frame_rate) == VALID_FRAME_RATES.end()) { |
| + LOG(ERROR) << "Unsupported frame_rate (" << wav_info_.frame_rate << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if (VALID_BITS_PER_SAMPLES.find(wav_info_.bits_per_sample) == |
| + VALID_BITS_PER_SAMPLES.end()) { |
| + LOG(ERROR) << "Unsupported bits per sample (" << wav_info_.bits_per_sample |
| + << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + uint16_t expected_frame_size; |
| + expected_frame_size = (wav_info_.channel_count * wav_info_.bits_per_sample) |
| + >> 3; |
| + if (wav_info_.frame_size != expected_frame_size) { |
| + LOG(ERROR) << "Frame size sanity check failed. (expected " |
| + << expected_frame_size << " got " |
| + << wav_info_.frame_size << ")"; |
| + RunLoop::current()->Quit(); |
|
jeffbrown
2015/11/04 19:34:44
FWIW, you might be better off making a single Vali
johngro
2015/11/05 00:25:16
Agreed, Done.
|
| + return; |
| + } |
| + |
| + // Technically, there could be format specific member of the wave format |
| + // chunk, or other riff chunks which could come after this, but for this demo, |
| + // we only handle getting the 'data' chunk at this point. |
| + RIFFChunkHeader data_hdr; |
| + if (!BlockingRead(&data_hdr, sizeof(data_hdr))) { |
| + LOG(ERROR) << "Failed to read data header!"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if (data_hdr.four_cc != DATA_FOUR_CC) { |
| + LOG(ERROR) << "Missing expected 'data' 4CC " |
| + << "(expected 0x " << std::hex << DATA_FOUR_CC |
| + << " got 0x" << std::hex << data_hdr.four_cc |
| + << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + if ((data_hdr.length + sizeof(WAVHeader) + sizeof(RIFFChunkHeader)) |
| + != riff_hdr.length) { |
| + LOG(ERROR) << "Header length sanity check failure (" |
| + << data_hdr.length << " + " |
| + << sizeof(WAVHeader) + sizeof(RIFFChunkHeader) << " != " |
| + << riff_hdr.length << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + // If the length of the data chunk is not a multiple of the frame size, log a |
| + // warning and truncate the length. |
| + uint16_t leftover; |
| + payload_len_ = data_hdr.length; |
| + leftover = payload_len_ % wav_info_.frame_size; |
| + if (leftover) { |
| + LOG(WARNING) << "Data chunk length (" << payload_len_ |
| + << ") not a multiple of frame size (" << wav_info_.frame_size |
| + << ")"; |
| + payload_len_ -= leftover; |
| + } |
| + |
| + LOG(INFO) << "Preparing to play..."; |
| + LOG(INFO) << "File : " << TEST_FILE; |
| + LOG(INFO) << "Rate : " << wav_info_.frame_rate; |
| + LOG(INFO) << "Chan : " << wav_info_.channel_count; |
| + LOG(INFO) << "BPS : " << wav_info_.bits_per_sample; |
| + |
| + // Create the audio sink we will use to play this WAV file and start to |
| + // configure it. |
| + audio_server_->CreateTrack(GetProxy(&audio_track_)); |
| + |
| + LinearTransform::Ratio audio_rate(wav_info_.frame_rate, 1); |
| + LinearTransform::Ratio local_rate(LocalDuration::period::num, |
| + LocalDuration::period::den); |
| + LinearTransform::Ratio tmp; |
| + bool success = LinearTransform::Ratio::Compose(audio_rate, local_rate, &tmp); |
| + DCHECK(success); |
| + |
| + AudioTrackConfigurationPtr cfg; |
| + cfg = AudioTrackConfiguration::New(); |
| + cfg->max_frames = USecToFrames(BUF_DEPTH_USEC); |
| + cfg->audio_frame_ratio = tmp.numerator; |
| + cfg->media_time_ratio = tmp.denominator; |
| + |
| + LpcmMediaTypeDetailsPtr pcm_cfg = LpcmMediaTypeDetails::New(); |
| + pcm_cfg->sample_format = (wav_info_.bits_per_sample == 8) |
| + ? LpcmSampleFormat::UNSIGNED_8 |
| + : LpcmSampleFormat::SIGNED_16; |
| + pcm_cfg->samples_per_frame = wav_info_.channel_count; |
| + pcm_cfg->frames_per_second = wav_info_.frame_rate; |
| + |
| + cfg->media_type = MediaType::New(); |
| + cfg->media_type->scheme = MediaTypeScheme::LPCM; |
| + cfg->media_type->details = MediaTypeDetails::New(); |
| + cfg->media_type->details->set_lpcm(pcm_cfg.Pass()); |
| + |
| + |
| + audio_track_->Configure(cfg.Pass(), |
| + GetProxy(&media_pipe_), |
| + [this](MediaResult res) { |
| + this->OnAudioConfigured(res); |
|
jeffbrown
2015/11/04 19:34:44
Why can't we just send the rate control request im
johngro
2015/11/05 00:25:16
will change, I'm going to finish responses to init
|
| + }); |
| +} |
| + |
| +void PlayWAVApp::OnAudioConfigured(MediaResult res) { |
| + if (res != MediaResult::OK) { |
| + LOG(ERROR) << "Failed to configure audio track (res = " << res << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + // Grab the rate control interface for our audio renderer. |
| + audio_track_->GetRateControl(GetProxy(&rate_control_), |
| + [this](MediaResult res) { |
| + this->OnHasRateControl(res); |
|
jeffbrown
2015/11/04 19:34:44
Likewise, no need to wait for the callback before
johngro
2015/11/05 00:25:16
Acknowledged.
|
| + }); |
| +} |
| + |
| +void PlayWAVApp::OnHasRateControl(MediaResult res) { |
| + if (res != MediaResult::OK) { |
| + LOG(ERROR) << "Failed to fetch rate control interface " |
| + << "(res = " << res << ")"; |
| + RunLoop::current()->Quit(); |
| + return; |
| + } |
| + |
| + // Set up our media pipe helper, configure its callback and water marks to |
| + // kick off the playback process. |
| + audio_pipe_.reset(new CircularBufferMediaPipeAdapter(media_pipe_.Pass())); |
| + audio_pipe_->SetWatermarks(USecToBytes(BUF_HI_WATER_USEC), |
| + USecToBytes(BUF_LO_WATER_USEC)); |
| + audio_pipe_->SetSignalCallback( |
| + [this](MediaResult res) -> bool { |
| + return OnNeedsData(res); |
| + }); |
| +} |
| + |
| +bool PlayWAVApp::OnNeedsData(MediaResult res) { |
| + if (res != MediaResult::OK) { |
| + LOG(ERROR) << "Error during playback! (res = " << res << ")"; |
| + RunLoop::current()->Quit(); |
| + return false; |
| + } |
| + |
| + uint64_t bytes = USecToBytes(CHUNK_SIZE_USEC); |
| + if (bytes > payload_len_) { |
| + bytes = payload_len_; |
| + } |
| + |
| + res = audio_pipe_->CreateMediaPacket(bytes, false, &audio_packet_); |
| + if (res != MediaResult::OK) { |
| + LOG(ERROR) << "Failed to create " << bytes << " byte media packet! " |
| + << "(res = " << res << ")"; |
| + RunLoop::current()->Quit(); |
| + return false; |
| + } |
| + |
| + if (!sent_first_packet_) { |
| + DCHECK(audio_packet_.packet()); |
| + audio_packet_.packet()->pts = 0; |
| + sent_first_packet_ = true; |
| + } |
| + |
| + for (size_t i = 0; i < AudioPacket::kMaxRegions; ++i) { |
| + if (audio_packet_.data(i)) { |
| + DCHECK(audio_packet_.length(i)); |
| + DCHECK(audio_packet_.length(i) <= payload_len_); |
| + |
| + if (!BlockingRead(audio_packet_.data(i), |
| + audio_packet_.length(i))) { |
| + LOG(ERROR) << "Failed to read source, shutting down..."; |
| + RunLoop::current()->Quit(); |
| + return false; |
| + } |
| + |
| + payload_len_ -= audio_packet_.length(i); |
| + } |
| + } |
| + |
| + if (payload_len_) { |
| + res = audio_pipe_->SendMediaPacket(&audio_packet_); |
| + } else { |
| + res = audio_pipe_->SendMediaPacket(&audio_packet_, playout_complete_cbk_); |
| + } |
| + |
| + if (res != MediaResult::OK) { |
| + LOG(ERROR) << "Failed to send media packet! " |
| + << "(res = " << res << ")"; |
| + RunLoop::current()->Quit(); |
| + return false; |
| + } |
| + |
| + if (!clock_started_ && (audio_pipe_->AboveHiWater() || !payload_len_)) { |
| + LocalTime sched = LocalClock::now() + local_time::from_msec(50); |
| + rate_control_->SetRateAtTargetTime(1, 1, sched.time_since_epoch().count()); |
| + clock_started_ = true; |
| + } |
| + |
| + return (payload_len_ != 0); |
| +} |
| + |
| +void PlayWAVApp::OnPlayoutComplete(MediaResult res) { |
| + DCHECK(!audio_pipe_->GetPending()); |
| + RunLoop::current()->Quit(); |
| +} |
| + |
| +} // namespace examples |
| +} // namespace audio |
| +} // namespace media |
| +} // namespace mojo |
| + |
| +MojoResult MojoMain(MojoHandle app_request) { |
| + mojo::ApplicationRunner runner(new mojo::media::audio::examples::PlayWAVApp); |
| + return runner.Run(app_request); |
| +} |