blob: f9daaadc7d543c87fc135b0e4e026281d534038c [file] [log] [blame]
// 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 "mojo/public/c/system/main.h"
#include "mojo/public/cpp/application/application_impl_base.h"
#include "mojo/public/cpp/application/connect.h"
#include "mojo/public/cpp/application/run_application.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/wait.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/media/core/interfaces/media_renderer.mojom.h"
#include "mojo/services/network/interfaces/network_service.mojom.h"
#include "mojo/services/network/interfaces/url_loader.mojom.h"
#define PACKED __attribute__((packed))
static inline constexpr uint32_t make_fourcc(uint8_t a, uint8_t b,
uint8_t c, uint8_t d) {
return (static_cast<uint32_t>(a) << 24) |
(static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(c) << 8) |
static_cast<uint32_t>(d);
}
static inline constexpr uint32_t fetch_fourcc(const void* source) {
return (static_cast<uint32_t>(static_cast<const uint8_t*>(source)[0]) << 24) |
(static_cast<uint32_t>(static_cast<const uint8_t*>(source)[1]) << 16) |
(static_cast<uint32_t>(static_cast<const uint8_t*>(source)[2]) << 8) |
static_cast<uint32_t>(static_cast<const uint8_t*>(source)[3]);
}
namespace mojo {
namespace media {
namespace audio {
namespace examples {
static constexpr const char* TEST_FILE =
"http://localhost/test_content/piano2.wav";
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 ApplicationImplBase {
public:
~PlayWAVApp() override { OnQuit(); }
// ApplicationImplBase overrides:
void OnInitialize() override;
void OnQuit() override;
private:
using AudioPipePtr = std::unique_ptr<CircularBufferMediaPipeAdapter>;
using AudioPacket = CircularBufferMediaPipeAdapter::MappedPacket;
using PacketCbk = MediaPacketConsumer::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!
static constexpr uint32_t RIFF_FOUR_CC = make_fourcc('R', 'I', 'F', 'F');
static constexpr uint32_t WAVE_FOUR_CC = make_fourcc('W', 'A', 'V', 'E');
static constexpr uint32_t FMT_FOUR_CC = make_fourcc('f', 'm', 't', ' ');
static constexpr uint32_t DATA_FOUR_CC = make_fourcc('d', 'a', 't', 'a');
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);
bool ReadAndValidateRIFFHeader();
bool ReadAndValidateWAVHeader();
bool ReadAndValidateDATAHeader();
void OnNeedsData(MediaResult res);
void OnPlayoutComplete(MediaPacketConsumer::SendResult res);
void OnConnectionError(const std::string& connection_name);
void PostShutdown();
void Shutdown();
uint32_t USecToFrames(uint32_t usec) {
uint64_t ret = (static_cast<uint64_t>(usec) * wav_info_.frame_rate)
/ 1000000;
MOJO_DCHECK(ret < std::numeric_limits<uint32_t>::max());
return ret;
}
uint32_t USecToBytes(uint32_t usec) {
uint32_t frames = USecToFrames(usec);
MOJO_DCHECK(wav_info_.frame_size);
MOJO_DCHECK(frames <
std::numeric_limits<uint32_t>::max() / wav_info_.frame_size);
return frames * wav_info_.frame_size;
}
AudioServerPtr audio_server_;
AudioTrackPtr audio_track_;
MediaRendererPtr media_renderer_;
AudioPipePtr audio_pipe_;
TimelineConsumerPtr timeline_consumer_;
AudioPacket audio_packet_;
PacketCbk playout_complete_cbk_;
NetworkServicePtr network_service_;
URLLoaderPtr url_loader_;
ScopedDataPipeConsumerHandle payload_;
uint32_t payload_len_;
RIFFChunkHeader riff_hdr_;
WAVHeader wav_info_;
RIFFChunkHeader data_hdr_;
bool sent_first_packet_ = false;
bool clock_started_ = false;
bool shutting_down_ = 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::OnInitialize() {
ConnectToService(shell(), "mojo:audio_server", GetProxy(&audio_server_));
audio_server_.set_connection_error_handler([this]() {
OnConnectionError("audio_server");
});
ConnectToService(shell(), "mojo:network_service",
GetProxy(&network_service_));
audio_server_.set_connection_error_handler([this]() {
OnConnectionError("network_service");
});
network_service_->CreateURLLoader(GetProxy(&url_loader_));
url_loader_.set_connection_error_handler([this]() {
OnConnectionError("url_loader");
});
playout_complete_cbk_ =
PacketCbk([this](MediaPacketConsumer::SendResult 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::OnQuit() {
if (audio_packet_.packet()) {
MOJO_DCHECK(audio_pipe_);
audio_pipe_->CancelMediaPacket(&audio_packet_);
}
payload_.reset();
url_loader_.reset();
network_service_.reset();
audio_pipe_.reset();
timeline_consumer_.reset();
audio_track_.reset();
media_renderer_.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(),
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())) {
MOJO_LOG(ERROR) << "Bad MimeType \""
<< (resp->mime_type.is_null() ? "<null>" : resp->mime_type)
<< "\"";
Shutdown();
return;
}
payload_ = resp->body.Pass();
if (!ReadAndValidateRIFFHeader() ||
!ReadAndValidateWAVHeader() ||
!ReadAndValidateDATAHeader()) {
Shutdown();
return;
}
MOJO_LOG(INFO) << "Preparing to play...";
MOJO_LOG(INFO) << "File : " << TEST_FILE;
MOJO_LOG(INFO) << "Rate : " << wav_info_.frame_rate;
MOJO_LOG(INFO) << "Chan : " << wav_info_.channel_count;
MOJO_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_), GetProxy(&media_renderer_));
// TODO(johngro): when there is some better diagnostic information made
// available to us, make sure that we log it so we have some way to proceed
// with debugging.
audio_track_.set_connection_error_handler([this]() {
OnConnectionError("audio_track");
});
media_renderer_.set_connection_error_handler([this]() {
OnConnectionError("media_renderer");
});
AudioMediaTypeDetailsPtr pcm_cfg = AudioMediaTypeDetails::New();
pcm_cfg->sample_format = (wav_info_.bits_per_sample == 8)
? AudioSampleFormat::UNSIGNED_8
: AudioSampleFormat::SIGNED_16;
pcm_cfg->channels = wav_info_.channel_count;
pcm_cfg->frames_per_second = wav_info_.frame_rate;
MediaTypePtr media_type = MediaType::New();
media_type->medium = MediaTypeMedium::AUDIO;
media_type->details = MediaTypeDetails::New();
media_type->details->set_audio(pcm_cfg.Pass());
media_type->encoding = MediaType::kAudioEncodingLpcm;
// Configure the track based on the WAV header information.
media_renderer_->SetMediaType(media_type.Pass());
MediaPacketConsumerPtr media_pipe;
media_renderer_->GetPacketConsumer(GetProxy(&media_pipe));
// Grab the timeline consumer interface for our audio renderer.
MediaTimelineControlPointPtr timeline_control_point;
media_renderer_->GetTimelineControlPoint(GetProxy(&timeline_control_point));
timeline_control_point->GetTimelineConsumer(GetProxy(&timeline_consumer_));
timeline_consumer_.set_connection_error_handler(
[this]() { OnConnectionError("timeline_consumer"); });
// 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_->Init(USecToBytes(BUF_DEPTH_USEC));
audio_pipe_->SetWatermarks(USecToBytes(BUF_HI_WATER_USEC),
USecToBytes(BUF_LO_WATER_USEC));
audio_pipe_->SetSignalCallback(
[this](MediaResult res) -> void {
OnNeedsData(res);
});
}
bool PlayWAVApp::ReadAndValidateRIFFHeader() {
// Read and sanity check the top level RIFF header
if (!BlockingRead(&riff_hdr_, sizeof(riff_hdr_))) {
MOJO_LOG(ERROR) << "Failed to read top level RIFF header!";
return false;
}
if (fetch_fourcc(&riff_hdr_.four_cc) != RIFF_FOUR_CC) {
MOJO_LOG(ERROR) << "Missing expected 'RIFF' 4CC "
<< "(expected 0x " << std::hex << RIFF_FOUR_CC
<< " got 0x" << std::hex
<< fetch_fourcc(&riff_hdr_.four_cc)
<< ")";
return false;
}
return true;
}
bool PlayWAVApp::ReadAndValidateWAVHeader() {
// Read the WAVE header along with its required format chunk.
if (!BlockingRead(&wav_info_, sizeof(wav_info_))) {
MOJO_LOG(ERROR) << "Failed to read top level WAVE header!";
return false;
}
if (fetch_fourcc(&wav_info_.wave_four_cc) != WAVE_FOUR_CC) {
MOJO_LOG(ERROR) << "Missing expected 'WAVE' 4CC "
<< "(expected 0x " << std::hex << WAVE_FOUR_CC
<< " got 0x"
<< std::hex << fetch_fourcc(&wav_info_.wave_four_cc)
<< ")";
return false;
}
if (fetch_fourcc(&wav_info_.fmt_four_cc) != FMT_FOUR_CC) {
MOJO_LOG(ERROR) << "Missing expected 'fmt ' 4CC "
<< "(expected 0x " << std::hex << FMT_FOUR_CC
<< " got 0x"
<< std::hex << fetch_fourcc(&wav_info_.fmt_four_cc)
<< ")";
return false;
}
// 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) {
MOJO_LOG(ERROR) << "Unsupported format (0x"
<< std::hex << wav_info_.format
<< ") must be LPCM (0x"
<< std::hex << FORMAT_LPCM
<< ")";
return false;
}
if ((wav_info_.channel_count != 1) && (wav_info_.channel_count != 2)) {
MOJO_LOG(ERROR) << "Unsupported channel count ("
<< wav_info_.channel_count
<< ") must be either mono or stereo";
return false;
}
if (VALID_FRAME_RATES.find(wav_info_.frame_rate) == VALID_FRAME_RATES.end()) {
MOJO_LOG(ERROR) << "Unsupported frame_rate ("
<< wav_info_.frame_rate << ")";
return false;
}
if (VALID_BITS_PER_SAMPLES.find(wav_info_.bits_per_sample) ==
VALID_BITS_PER_SAMPLES.end()) {
MOJO_LOG(ERROR) << "Unsupported bits per sample ("
<< wav_info_.bits_per_sample << ")";
return false;
}
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) {
MOJO_LOG(ERROR) << "Frame size sanity check failed. (expected "
<< expected_frame_size << " got "
<< wav_info_.frame_size << ")";
return false;
}
return true;
}
bool PlayWAVApp::ReadAndValidateDATAHeader() {
// 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.
if (!BlockingRead(&data_hdr_, sizeof(data_hdr_))) {
MOJO_LOG(ERROR) << "Failed to read data header!";
return false;
}
if (fetch_fourcc(&data_hdr_.four_cc) != DATA_FOUR_CC) {
MOJO_LOG(ERROR) << "Missing expected 'data' 4CC "
<< "(expected 0x " << std::hex << DATA_FOUR_CC
<< " got 0x" << std::hex
<< fetch_fourcc(&data_hdr_.four_cc)
<< ")";
return false;
}
if ((data_hdr_.length + sizeof(WAVHeader) + sizeof(RIFFChunkHeader))
!= riff_hdr_.length) {
MOJO_LOG(ERROR) << "Header length sanity check failure ("
<< data_hdr_.length << " + "
<< sizeof(WAVHeader) + sizeof(RIFFChunkHeader) << " != "
<< riff_hdr_.length << ")";
return false;
}
// 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) {
MOJO_LOG(WARNING)
<< "Data chunk length (" << payload_len_
<< ") not a multiple of frame size (" << wav_info_.frame_size
<< ")";
payload_len_ -= leftover;
}
return true;
}
void PlayWAVApp::OnNeedsData(MediaResult res) {
if (res != MediaResult::OK) {
MOJO_LOG(ERROR) << "Error during playback! (res = " << res << ")";
PostShutdown();
return;
}
if (!payload_len_) {
// If we are just waiting for playout to finish, keep receiving callbacks so
// we know if something went fatally wrong.
return;
}
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) {
MOJO_LOG(ERROR) << "Failed to create " << bytes << " byte media packet! "
<< "(res = " << res << ")";
PostShutdown();
return;
}
if (!sent_first_packet_) {
MOJO_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)) {
MOJO_DCHECK(audio_packet_.length(i));
MOJO_DCHECK(audio_packet_.length(i) <= payload_len_);
if (!BlockingRead(audio_packet_.data(i),
audio_packet_.length(i))) {
MOJO_LOG(ERROR) << "Failed to read source, shutting down...";
PostShutdown();
return;
}
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) {
MOJO_LOG(ERROR) << "Failed to send media packet! "
<< "(res = " << res << ")";
PostShutdown();
return;
}
if (!clock_started_ && (audio_pipe_->AboveHiWater() || !payload_len_)) {
TimelineTransformPtr timeline_transform = TimelineTransform::New();
timeline_transform->reference_time = kUnspecifiedTime;
timeline_transform->subject_time = kUnspecifiedTime;
timeline_transform->reference_delta = 1;
timeline_transform->subject_delta = 1;
timeline_consumer_->SetTimelineTransform(timeline_transform.Pass(),
[](bool completed) {});
clock_started_ = true;
}
}
void PlayWAVApp::OnPlayoutComplete(MediaPacketConsumer::SendResult res) {
MOJO_DCHECK(!audio_pipe_->GetPending());
audio_pipe_ = nullptr;
PostShutdown();
}
void PlayWAVApp::OnConnectionError(const std::string& connection_name) {
if (!shutting_down_) {
MOJO_LOG(ERROR) << connection_name << " connection closed unexpectedly!";
PostShutdown();
}
}
void PlayWAVApp::PostShutdown() {
if (audio_pipe_) {
audio_pipe_->ResetSignalCallback();
}
mojo::RunLoop::current()->PostDelayedTask([this]() -> void {
Shutdown();
}, 0);
}
// TODO(johngro): remove this when we can. Right now, the proper way to cleanly
// shut down a running mojo application is a bit unclear to me. Calling
// RunLoop::current()->Quit() seems like the best option, but the run loop does
// not seem to call our application's quit method. Instead, it starts to close
// all of our connections (triggering all of our connection error handlers we
// have registered on interfaces) before finally destroying our application
// object.
//
// The net result is that we end up spurious "connection closed unexpectedly"
// error messages when we are actually shutting down cleanly. For now, we
// suppress this by having a shutting_down_ flag and suppressing the error
// message which show up after shutdown has been triggered. When the proper
// pattern for shutting down an app has been established, come back here and
// remove all this junk.
void PlayWAVApp::Shutdown() {
OnQuit();
RunLoop::current()->Quit();
}
} // namespace examples
} // namespace audio
} // namespace media
} // namespace mojo
MojoResult MojoMain(MojoHandle app_request) {
mojo::media::audio::examples::PlayWAVApp play_wav_app;
return mojo::RunApplication(app_request, &play_wav_app);
}