// Copyright 2022 The Cobalt Authors. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
#include "starboard/shared/uwp/audio_renderer_passthrough.h" | |
#include <algorithm> | |
#include <string> | |
#include "starboard/common/log.h" | |
#include "starboard/common/string.h" | |
namespace starboard { | |
namespace shared { | |
namespace uwp { | |
namespace { | |
int CodecToIecSampleRate(SbMediaAudioCodec codec) { | |
switch (codec) { | |
case kSbMediaAudioCodecAc3: | |
return 48000; | |
case kSbMediaAudioCodecEac3: | |
return 192000; | |
default: | |
SB_NOTREACHED(); | |
return 0; | |
} | |
} | |
} // namespace | |
AudioRendererPassthrough::AudioRendererPassthrough( | |
scoped_ptr<AudioDecoder> audio_decoder, | |
const AudioStreamInfo& audio_stream_info) | |
: channels_(audio_stream_info.number_of_channels), | |
codec_(audio_stream_info.codec), | |
iec_sample_rate_(CodecToIecSampleRate(audio_stream_info.codec)), | |
decoder_(audio_decoder.Pass()), | |
sink_(new WASAPIAudioSink), | |
process_audio_buffers_job_( | |
std::bind(&AudioRendererPassthrough::ProcessAudioBuffers, this)) { | |
SB_DCHECK(codec_ == kSbMediaAudioCodecAc3 || | |
codec_ == kSbMediaAudioCodecEac3); | |
SB_DCHECK(decoder_); | |
QueryPerformanceFrequency(&performance_frequency_); | |
SB_DCHECK(performance_frequency_.QuadPart > 0); | |
SB_LOG(INFO) << "Creating AudioRendererPassthrough with " << channels_ | |
<< " channels."; | |
} | |
AudioRendererPassthrough::~AudioRendererPassthrough() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_LOG(INFO) << "Destroying AudioRendererPassthrough with " << channels_ | |
<< " channels."; | |
} | |
void AudioRendererPassthrough::Initialize(const ErrorCB& error_cb, | |
const PrerolledCB& prerolled_cb, | |
const EndedCB& ended_cb) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_DCHECK(error_cb); | |
SB_DCHECK(prerolled_cb); | |
SB_DCHECK(ended_cb); | |
SB_DCHECK(!error_cb_); | |
SB_DCHECK(!prerolled_cb_); | |
SB_DCHECK(!ended_cb_); | |
error_cb_ = error_cb; | |
prerolled_cb_ = prerolled_cb; | |
ended_cb_ = ended_cb; | |
decoder_->Initialize( | |
std::bind(&AudioRendererPassthrough::OnDecoderOutput, this), error_cb); | |
if (!sink_->Initialize(channels_, iec_sample_rate_, codec_)) { | |
error_cb_(kSbPlayerErrorDecode, "failed to start audio sink"); | |
} | |
} | |
void AudioRendererPassthrough::WriteSamples(const InputBuffers& input_buffers) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_DCHECK(input_buffers.size() == 1); | |
SB_DCHECK(input_buffers[0]); | |
SB_DCHECK(can_accept_more_data_.load()); | |
if (end_of_stream_written_.load()) { | |
SB_LOG(ERROR) << "Appending audio sample at " | |
<< input_buffers[0]->timestamp() << " after EOS reached."; | |
return; | |
} | |
can_accept_more_data_.store(false); | |
decoder_->Decode( | |
input_buffers, | |
std::bind(&AudioRendererPassthrough::OnDecoderConsumed, this)); | |
} | |
void AudioRendererPassthrough::WriteEndOfStream() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
if (end_of_stream_written_.load()) { | |
SB_LOG(ERROR) << "Try to write EOS after EOS is reached"; | |
return; | |
} | |
end_of_stream_written_.store(true); | |
decoder_->WriteEndOfStream(); | |
} | |
void AudioRendererPassthrough::SetVolume(double volume) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
sink_->SetVolume(volume); | |
} | |
bool AudioRendererPassthrough::IsEndOfStreamWritten() const { | |
SB_DCHECK(BelongsToCurrentThread()); | |
return end_of_stream_written_.load(); | |
} | |
bool AudioRendererPassthrough::IsEndOfStreamPlayed() const { | |
SB_DCHECK(BelongsToCurrentThread()); | |
return end_of_stream_played_.load(); | |
} | |
bool AudioRendererPassthrough::CanAcceptMoreData() const { | |
SB_DCHECK(BelongsToCurrentThread()); | |
return !end_of_stream_written_.load() && | |
pending_inputs_.size() < kMaxDecodedAudios && | |
can_accept_more_data_.load(); | |
} | |
void AudioRendererPassthrough::Play() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
ScopedLock lock(mutex_); | |
paused_ = false; | |
sink_->Play(); | |
} | |
void AudioRendererPassthrough::Pause() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
ScopedLock lock(mutex_); | |
paused_ = true; | |
sink_->Pause(); | |
} | |
void AudioRendererPassthrough::SetPlaybackRate(double playback_rate) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
if (playback_rate > 0.0 && playback_rate != 1.0) { | |
std::string error_message = ::starboard::FormatString( | |
"Playback rate %f is not supported", playback_rate); | |
error_cb_(kSbPlayerErrorDecode, error_message); | |
return; | |
} | |
ScopedLock lock(mutex_); | |
playback_rate_ = playback_rate; | |
sink_->SetPlaybackRate(playback_rate); | |
} | |
void AudioRendererPassthrough::Seek(int64_t seek_to_time) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_DCHECK(seek_to_time >= 0); | |
{ | |
ScopedLock lock(mutex_); | |
seeking_to_time_ = std::max<int64_t>(seek_to_time, 0); | |
seeking_ = true; | |
} | |
total_frames_sent_to_sink_ = 0; | |
can_accept_more_data_.store(true); | |
process_audio_buffers_job_token_.ResetToInvalid(); | |
total_buffers_sent_to_sink_ = 0; | |
end_of_stream_written_.store(false); | |
end_of_stream_played_.store(false); | |
pending_inputs_ = std::queue<scoped_refptr<DecodedAudio>>(); | |
sink_->Reset(); | |
decoder_->Reset(); | |
CancelPendingJobs(); | |
} | |
int64_t AudioRendererPassthrough::GetCurrentMediaTime(bool* is_playing, | |
bool* is_eos_played, | |
bool* is_underflow, | |
double* playback_rate) { | |
SB_DCHECK(is_playing); | |
SB_DCHECK(is_eos_played); | |
SB_DCHECK(is_underflow); | |
SB_DCHECK(playback_rate); | |
ScopedLock lock(mutex_); | |
*is_playing = !paused_ && !seeking_; | |
*is_eos_played = end_of_stream_played_.load(); | |
*is_underflow = false; // TODO: Support underflow | |
*playback_rate = playback_rate_; | |
if (seeking_) { | |
return seeking_to_time_; | |
} | |
uint64_t sink_playback_time_updated_at; | |
int64_t sink_playback_time = static_cast<int64_t>( | |
sink_->GetCurrentPlaybackTime(&sink_playback_time_updated_at)); | |
if (sink_playback_time <= 0) { | |
if (sink_playback_time < 0) { | |
error_cb_(kSbPlayerErrorDecode, | |
"Error obtaining playback time from WASAPI sink"); | |
} | |
return seeking_to_time_; | |
} | |
int64_t media_time = seeking_to_time_ + sink_playback_time; | |
if (!sink_->playing()) { | |
return media_time; | |
} | |
return media_time + | |
CalculateElapsedPlaybackTime(sink_playback_time_updated_at); | |
} | |
void AudioRendererPassthrough::OnDecoderConsumed() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
can_accept_more_data_.store(true); | |
} | |
void AudioRendererPassthrough::OnDecoderOutput() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
int samples_per_second = 0; | |
scoped_refptr<DecodedAudio> decoded_audio = | |
decoder_->Read(&samples_per_second); | |
SB_DCHECK(decoded_audio); | |
pending_inputs_.push(decoded_audio); | |
if (process_audio_buffers_job_token_.is_valid()) { | |
RemoveJobByToken(process_audio_buffers_job_token_); | |
process_audio_buffers_job_token_.ResetToInvalid(); | |
} | |
ProcessAudioBuffers(); | |
} | |
void AudioRendererPassthrough::ProcessAudioBuffers() { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_DCHECK(!pending_inputs_.empty()); | |
process_audio_buffers_job_token_.ResetToInvalid(); | |
int64_t process_audio_buffers_job_delay_usec = 5'000; // 5ms | |
scoped_refptr<DecodedAudio> decoded_audio = pending_inputs_.front(); | |
SB_DCHECK(decoded_audio); | |
if (decoded_audio->is_end_of_stream()) { | |
SB_DCHECK(end_of_stream_written_.load()); | |
ScopedLock lock(mutex_); | |
if (seeking_) { | |
seeking_ = false; | |
Schedule(prerolled_cb_); | |
} | |
pending_inputs_.pop(); | |
SB_DCHECK(pending_inputs_.empty()); | |
} else { | |
ScopedLock lock(mutex_); | |
while (seeking_ && decoded_audio && | |
CalculateLastOutputTime(decoded_audio) < seeking_to_time_) { | |
pending_inputs_.pop(); | |
decoded_audio = pending_inputs_.empty() ? scoped_refptr<DecodedAudio>() | |
: pending_inputs_.front(); | |
} | |
if (decoded_audio && TryToWriteAudioBufferToSink(decoded_audio)) { | |
pending_inputs_.pop(); | |
process_audio_buffers_job_delay_usec = 0; | |
if (seeking_ && total_buffers_sent_to_sink_ >= kNumPrerollDecodedAudios) { | |
seeking_ = false; | |
Schedule(prerolled_cb_); | |
} | |
} | |
} | |
if (!pending_inputs_.empty()) { | |
process_audio_buffers_job_token_ = Schedule( | |
process_audio_buffers_job_, process_audio_buffers_job_delay_usec); | |
return; | |
} | |
if (end_of_stream_written_.load() && !end_of_stream_played_.load()) { | |
TryToEndStream(); | |
} | |
} | |
bool AudioRendererPassthrough::TryToWriteAudioBufferToSink( | |
scoped_refptr<DecodedAudio> decoded_audio) { | |
SB_DCHECK(BelongsToCurrentThread()); | |
SB_DCHECK(decoded_audio); | |
bool buffer_written = sink_->WriteBuffer(decoded_audio); | |
if (buffer_written && !decoded_audio->is_end_of_stream()) { | |
total_frames_sent_to_sink_ += decoded_audio->frames(); | |
total_buffers_sent_to_sink_++; | |
} | |
return buffer_written; | |
} | |
void AudioRendererPassthrough::TryToEndStream() { | |
bool is_playing, is_eos_played, is_underflow; | |
double playback_rate; | |
int64_t total_frames_played_by_sink = | |
GetCurrentMediaTime(&is_playing, &is_eos_played, &is_underflow, | |
&playback_rate) * | |
iec_sample_rate_ / 1'000'000; | |
// Wait for the audio sink to output the remaining frames before calling | |
// Pause(). | |
if (total_frames_played_by_sink >= total_frames_sent_to_sink_) { | |
sink_->Pause(); | |
end_of_stream_played_.store(true); | |
ended_cb_(); | |
return; | |
} | |
Schedule(std::bind(&AudioRendererPassthrough::TryToEndStream, this), 5'000); | |
} | |
int64_t AudioRendererPassthrough::CalculateElapsedPlaybackTime( | |
uint64_t update_time) { | |
LARGE_INTEGER current_time; | |
QueryPerformanceCounter(¤t_time); | |
// Convert current performance counter timestamp to units of 100 nanoseconds. | |
// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclock-getposition#remarks | |
uint64_t current_time_converted = | |
static_cast<double>(current_time.QuadPart) * | |
(10000000.0 / static_cast<double>(performance_frequency_.QuadPart)); | |
SB_DCHECK(current_time_converted >= update_time); | |
// Convert elapsed time to microseconds. | |
return ((current_time_converted - update_time) * 100) / 1000; | |
} | |
int64_t AudioRendererPassthrough::CalculateLastOutputTime( | |
scoped_refptr<DecodedAudio>& decoded_audio) { | |
return decoded_audio->timestamp() + | |
(decoded_audio->frames() / iec_sample_rate_ * 1'000'000); | |
} | |
} // namespace uwp | |
} // namespace shared | |
} // namespace starboard |