| // 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 <algorithm> |
| #include <limits> |
| |
| #include "base/logging.h" |
| #include "mojo/services/media/common/cpp/linear_transform.h" |
| #include "mojo/services/media/common/cpp/timeline.h" |
| #include "services/media/audio/audio_output_manager.h" |
| #include "services/media/audio/audio_server_impl.h" |
| #include "services/media/audio/audio_track_impl.h" |
| #include "services/media/audio/audio_track_to_output_link.h" |
| |
| namespace mojo { |
| namespace media { |
| namespace audio { |
| |
| constexpr size_t AudioTrackImpl::PTS_FRACTIONAL_BITS; |
| |
| // TODO(johngro): If there is ever a better way to do this type of static-table |
| // initialization using mojom generated structs, we should switch to it. |
| static const struct { |
| AudioSampleFormat sample_format; |
| uint32_t min_channels; |
| uint32_t max_channels; |
| uint32_t min_frames_per_second; |
| uint32_t max_frames_per_second; |
| } kSupportedAudioTypeSets[] = { |
| { |
| .sample_format = AudioSampleFormat::UNSIGNED_8, |
| .min_channels = 1, |
| .max_channels = 2, |
| .min_frames_per_second = 1000, |
| .max_frames_per_second = 48000, |
| }, |
| { |
| .sample_format = AudioSampleFormat::SIGNED_16, |
| .min_channels = 1, |
| .max_channels = 2, |
| .min_frames_per_second = 1000, |
| .max_frames_per_second = 48000, |
| }, |
| }; |
| |
| AudioTrackImpl::AudioTrackImpl(InterfaceRequest<AudioTrack> iface, |
| AudioServerImpl* owner) |
| : owner_(owner), |
| binding_(this), |
| pipe_(this, owner) { |
| CHECK(nullptr != owner_); |
| binding_.Bind(iface.Pass()); |
| binding_.set_connection_error_handler([this]() -> void { |
| Shutdown(); |
| }); |
| } |
| |
| AudioTrackImpl::~AudioTrackImpl() { |
| // assert that we have been cleanly shutdown already. |
| MOJO_DCHECK(!binding_.is_bound()); |
| } |
| |
| AudioTrackImplPtr AudioTrackImpl::Create(InterfaceRequest<AudioTrack> iface, |
| AudioServerImpl* owner) { |
| AudioTrackImplPtr ret(new AudioTrackImpl(iface.Pass(), owner)); |
| ret->weak_this_ = ret; |
| return ret; |
| } |
| |
| void AudioTrackImpl::Shutdown() { |
| // If we are unbound, then we have already been shut down and are just waiting |
| // for the service to destroy us. Run some DCHECK sanity checks and get out. |
| if (!binding_.is_bound()) { |
| DCHECK(!pipe_.IsInitialized()); |
| DCHECK(!timeline_control_site_.is_bound()); |
| DCHECK(!outputs_.size()); |
| return; |
| } |
| |
| // Close the connection to our client |
| binding_.set_connection_error_handler(mojo::Closure()); |
| binding_.Close(); |
| |
| // reset all of our internal state and close any other client connections in |
| // the process. |
| pipe_.Reset(); |
| timeline_control_site_.Reset(); |
| outputs_.clear(); |
| |
| DCHECK(owner_); |
| AudioTrackImplPtr thiz = weak_this_.lock(); |
| owner_->RemoveTrack(thiz); |
| } |
| |
| void AudioTrackImpl::Describe(const DescribeCallback& cbk) { |
| // Build a minimal descriptor |
| // |
| // TODO(johngro): one day, we need to make this description much more rich and |
| // fully describe our capabilities, based on things like what outputs are |
| // available, the class of hardware we are on, and what options we were |
| // compiled with. |
| // |
| // For now, it would be nice to just be able to have a static const tree of |
| // capabilities in this translational unit which we could use to construct our |
| // message, but the nature of the structures generated by the C++ bindings |
| // make this difficult. For now, we just create a trivial descriptor entierly |
| // by hand. |
| AudioTrackDescriptorPtr desc(AudioTrackDescriptor::New()); |
| |
| desc->supported_media_types = |
| Array<MediaTypeSetPtr>::New(arraysize(kSupportedAudioTypeSets)); |
| |
| for (size_t i = 0; i < desc->supported_media_types.size(); ++i) { |
| const MediaTypeSetPtr& mts = |
| (desc->supported_media_types[i] = MediaTypeSet::New()); |
| |
| mts->medium = MediaTypeMedium::AUDIO; |
| mts->encodings = Array<String>::New(1); |
| mts->details = MediaTypeSetDetails::New(); |
| |
| mts->encodings[0] = MediaType::kAudioEncodingLpcm; |
| |
| const auto& s = kSupportedAudioTypeSets[i]; |
| AudioMediaTypeSetDetailsPtr audio_detail = AudioMediaTypeSetDetails::New(); |
| |
| audio_detail->sample_format = s.sample_format; |
| audio_detail->min_channels = s.min_channels; |
| audio_detail->max_channels = s.max_channels; |
| audio_detail->min_frames_per_second = s.min_frames_per_second; |
| audio_detail->max_frames_per_second = s.max_frames_per_second; |
| mts->details->set_audio(audio_detail.Pass()); |
| } |
| |
| cbk.Run(desc.Pass()); |
| } |
| |
| void AudioTrackImpl::Configure(AudioTrackConfigurationPtr configuration, |
| InterfaceRequest<MediaConsumer> req) { |
| // Are we already configured? |
| if (pipe_.IsInitialized()) { |
| LOG(ERROR) << "Attempting to reconfigure a configured audio track."; |
| Shutdown(); |
| return; |
| } |
| |
| // Check the requested configuration. |
| if ((configuration->media_type->medium != MediaTypeMedium::AUDIO) || |
| (configuration->media_type->encoding != MediaType::kAudioEncodingLpcm) || |
| (!configuration->media_type->details->is_audio())) { |
| LOG(ERROR) << "Unsupported configuration requested in " |
| "AudioTrack::Configure. Media type must be LPCM audio."; |
| Shutdown(); |
| return; |
| } |
| |
| // Search our supported configuration sets to find one compatible with this |
| // request. |
| auto& cfg = configuration->media_type->details->get_audio(); |
| size_t i; |
| for (i = 0; i < arraysize(kSupportedAudioTypeSets); ++i) { |
| const auto& cfg_set = kSupportedAudioTypeSets[i]; |
| |
| if ((cfg->sample_format == cfg_set.sample_format) && |
| (cfg->channels >= cfg_set.min_channels) && |
| (cfg->channels <= cfg_set.max_channels) && |
| (cfg->frames_per_second >= cfg_set.min_frames_per_second) && |
| (cfg->frames_per_second <= cfg_set.max_frames_per_second)) { |
| break; |
| } |
| } |
| |
| if (i >= arraysize(kSupportedAudioTypeSets)) { |
| LOG(ERROR) << "Unsupported LPCM configuration requested in " |
| "AudioTrack::Configure. " |
| << "(format = " << cfg->sample_format |
| << ", channels = " |
| << static_cast<uint32_t>(cfg->channels) |
| << ", frames_per_second = " << cfg->frames_per_second |
| << ")"; |
| Shutdown(); |
| return; |
| } |
| |
| // Sanity check the ratio which relates audio frames to media time. |
| uint32_t numerator = |
| static_cast<uint32_t>(configuration->audio_frame_ratio); |
| uint32_t denominator = |
| static_cast<uint32_t>(configuration->media_time_ratio); |
| if ((numerator < 1) || (denominator < 1)) { |
| LOG(ERROR) << "Invalid (audio frames:media time ticks) ratio (" |
| << numerator << "/" << denominator << ")"; |
| Shutdown(); |
| return; |
| } |
| |
| frames_per_ns_ = |
| TimelineRate(cfg->frames_per_second, Timeline::ns_from_seconds(1)); |
| |
| // Figure out the rate we need to scale by in order to produce our fixed |
| // point timestamps. |
| LinearTransform::Ratio frac_scale(1 << PTS_FRACTIONAL_BITS, 1); |
| LinearTransform::Ratio frame_scale(LinearTransform::Ratio(numerator, |
| denominator)); |
| bool no_loss = LinearTransform::Ratio::Compose(frac_scale, |
| frame_scale, |
| &frame_to_media_ratio_); |
| if (!no_loss) { |
| LOG(ERROR) << "Invalid (audio frames:media time ticks) ratio (" |
| << numerator << "/" << denominator << ")"; |
| Shutdown(); |
| return; |
| } |
| |
| // Figure out how many bytes we need to hold the requested number of nSec of |
| // audio. |
| switch (cfg->sample_format) { |
| case AudioSampleFormat::UNSIGNED_8: |
| bytes_per_frame_ = 1; |
| break; |
| |
| case AudioSampleFormat::SIGNED_16: |
| bytes_per_frame_ = 2; |
| break; |
| |
| case AudioSampleFormat::SIGNED_24_IN_32: |
| bytes_per_frame_ = 4; |
| break; |
| |
| default: |
| DCHECK(false); |
| bytes_per_frame_ = 2; |
| break; |
| } |
| bytes_per_frame_ *= cfg->channels; |
| |
| // Bind our pipe to the interface request. |
| if (pipe_.Init(req.Pass()) != MOJO_RESULT_OK) { |
| LOG(ERROR) << "Failed to media pipe to interface request."; |
| Shutdown(); |
| return; |
| } |
| |
| // Stash our configuration. |
| format_ = cfg.Pass(); |
| |
| // Have the audio output manager initialize our set of outputs. Note; there |
| // is currently no need for a lock here. Methods called from our user-facing |
| // interfaces are seriailzed by nature of the mojo framework, and none of the |
| // output manager's threads should ever need to manipulate the set. Cleanup |
| // of outputs which have gone away is currently handled in a lazy fashion when |
| // the track fails to promote its weak reference during an operation involving |
| // its outputs. |
| // |
| // TODO(johngro): someday, we will need to deal with recalculating properties |
| // which depend on a track's current set of outputs (for example, the minimum |
| // latency). This will probably be done using a dirty flag in the track |
| // implementations, and scheduling a job to recalculate the properties for the |
| // dirty tracks and notify the users as appropriate. |
| |
| // If we cannot promote our own weak pointer, something is seriously wrong. |
| AudioTrackImplPtr strong_this(weak_this_.lock()); |
| DCHECK(strong_this); |
| DCHECK(owner_); |
| owner_->GetOutputManager().SelectOutputsForTrack(strong_this); |
| } |
| |
| void AudioTrackImpl::GetTimelineControlSite( |
| InterfaceRequest<MediaTimelineControlSite> req) { |
| timeline_control_site_.Bind(req.Pass()); |
| } |
| |
| void AudioTrackImpl::SetGain(float db_gain) { |
| if (db_gain >= AudioTrack::kMaxGain) { |
| LOG(ERROR) << "Gain value too large (" << db_gain << ") for audio track."; |
| Shutdown(); |
| return; |
| } |
| |
| db_gain_ = db_gain; |
| |
| for (const auto& output : outputs_) { |
| DCHECK(output); |
| output->UpdateGain(); |
| } |
| } |
| |
| void AudioTrackImpl::AddOutput(AudioTrackToOutputLinkPtr link) { |
| // TODO(johngro): assert that we are on the main message loop thread. |
| DCHECK(link); |
| auto res = outputs_.emplace(link); |
| DCHECK(res.second); |
| link->UpdateGain(); |
| } |
| |
| void AudioTrackImpl::RemoveOutput(AudioTrackToOutputLinkPtr link) { |
| // TODO(johngro): assert that we are on the main message loop thread. |
| DCHECK(link); |
| |
| auto iter = outputs_.find(link); |
| if (iter != outputs_.end()) { |
| outputs_.erase(iter); |
| } else { |
| // TODO(johngro): that's odd. I can't think of a reason why we we should |
| // not be able to find this link in our set of outputs... should we log |
| // something about this? |
| DCHECK(false); |
| } |
| } |
| |
| void AudioTrackImpl::SnapshotRateTrans(LinearTransform* out, |
| uint32_t* generation) { |
| TimelineFunction timeline_function; |
| timeline_control_site_.SnapshotCurrentFunction( |
| Timeline::local_now(), &timeline_function, generation); |
| |
| // The control site works in ns units. We want the rate in frames per |
| // nanosecond, so we convert here. |
| TimelineRate rate_in_frames_per_ns = |
| timeline_function.rate() * frames_per_ns_; |
| |
| *out = LinearTransform(timeline_function.reference_time(), |
| rate_in_frames_per_ns.subject_delta(), |
| rate_in_frames_per_ns.reference_delta(), |
| timeline_function.subject_time() * frames_per_ns_); |
| } |
| |
| void AudioTrackImpl::OnPacketReceived(AudioPipe::AudioPacketRefPtr packet) { |
| DCHECK(packet); |
| for (const auto& output : outputs_) { |
| DCHECK(output); |
| output->PushToPendingQueue(packet); |
| } |
| |
| if (packet->state()->packet()->end_of_stream) { |
| timeline_control_site_.SetEndOfStreamPts( |
| (packet->state()->packet()->pts + packet->frame_count()) / |
| frames_per_ns_); |
| } |
| } |
| |
| bool AudioTrackImpl::OnFlushRequested(const MediaConsumer::FlushCallback& cbk) { |
| for (const auto& output : outputs_) { |
| DCHECK(output); |
| output->FlushPendingQueue(); |
| } |
| cbk.Run(); |
| return true; |
| } |
| |
| } // namespace audio |
| } // namespace media |
| } // namespace mojo |