| // Copyright 2021 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/android/shared/audio_track_bridge.h" |
| |
| #include <algorithm> |
| |
| #include "starboard/android/shared/jni_utils.h" |
| #include "starboard/android/shared/media_common.h" |
| #include "starboard/audio_sink.h" |
| #include "starboard/common/log.h" |
| #include "starboard/shared/starboard/media/media_util.h" |
| |
| namespace starboard { |
| namespace android { |
| namespace shared { |
| |
| namespace { |
| |
| using ::starboard::shared::starboard::media::GetBytesPerSample; |
| |
| const jint kNoOffset = 0; |
| |
| } // namespace |
| |
| AudioTrackBridge::AudioTrackBridge(SbMediaAudioCodingType coding_type, |
| optional<SbMediaAudioSampleType> sample_type, |
| int channels, |
| int sampling_frequency_hz, |
| int preferred_buffer_size_in_bytes, |
| bool enable_audio_device_callback, |
| bool enable_pcm_content_type_movie, |
| int tunnel_mode_audio_session_id, |
| bool is_web_audio) { |
| if (coding_type == kSbMediaAudioCodingTypePcm) { |
| SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(sample_type.value())); |
| |
| // TODO: Support query if platform supports float type for tunnel mode. |
| if (tunnel_mode_audio_session_id != -1) { |
| SB_DCHECK(sample_type.value() == kSbMediaAudioSampleTypeInt16Deprecated); |
| } |
| } else { |
| SB_DCHECK(coding_type == kSbMediaAudioCodingTypeAc3 || |
| coding_type == kSbMediaAudioCodingTypeDolbyDigitalPlus); |
| // TODO: Support passthrough under tunnel mode. |
| SB_DCHECK(tunnel_mode_audio_session_id == -1); |
| // TODO: |sample_type| is not used in passthrough mode, we should make this |
| // explicit. |
| } |
| |
| JniEnvExt* env = JniEnvExt::Get(); |
| ScopedLocalJavaRef<jobject> j_audio_output_manager( |
| env->CallStarboardObjectMethodOrAbort( |
| "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;")); |
| jobject j_audio_track_bridge = env->CallObjectMethodOrAbort( |
| j_audio_output_manager.Get(), "createAudioTrackBridge", |
| "(IIIIZZIZ)Ldev/cobalt/media/AudioTrackBridge;", |
| GetAudioFormatSampleType(coding_type, sample_type), sampling_frequency_hz, |
| channels, preferred_buffer_size_in_bytes, enable_audio_device_callback, |
| enable_pcm_content_type_movie, tunnel_mode_audio_session_id, |
| is_web_audio); |
| if (!j_audio_track_bridge) { |
| // One of the cases that this may hit is when output happened to be switched |
| // to a device that doesn't support tunnel mode. |
| // TODO: Find a way to exclude the device from tunnel mode playback, to |
| // avoid infinite loop in creating the audio sink on a device |
| // claims to support tunnel mode but fails to create the audio sink. |
| // TODO: Currently this will be reported as a general decode error, |
| // investigate if this can be reported as a capability changed error. |
| SB_LOG(WARNING) << "Failed to create |j_audio_track_bridge|."; |
| return; |
| } |
| j_audio_track_bridge_ = env->ConvertLocalRefToGlobalRef(j_audio_track_bridge); |
| if (coding_type != kSbMediaAudioCodingTypePcm) { |
| // This must be passthrough. |
| SB_DCHECK(!sample_type); |
| max_samples_per_write_ = kMaxFramesPerRequest; |
| j_audio_data_ = env->NewByteArray(max_samples_per_write_); |
| } else if (sample_type == kSbMediaAudioSampleTypeFloat32) { |
| max_samples_per_write_ = channels * kMaxFramesPerRequest; |
| j_audio_data_ = env->NewFloatArray(channels * kMaxFramesPerRequest); |
| } else if (sample_type == kSbMediaAudioSampleTypeInt16Deprecated) { |
| max_samples_per_write_ = channels * kMaxFramesPerRequest; |
| j_audio_data_ = |
| env->NewByteArray(channels * GetBytesPerSample(sample_type.value()) * |
| kMaxFramesPerRequest); |
| } else { |
| SB_NOTREACHED(); |
| } |
| SB_DCHECK(j_audio_data_) << "Failed to allocate |j_audio_data_|"; |
| |
| j_audio_data_ = env->ConvertLocalRefToGlobalRef(j_audio_data_); |
| } |
| |
| AudioTrackBridge::~AudioTrackBridge() { |
| JniEnvExt* env = JniEnvExt::Get(); |
| if (j_audio_track_bridge_) { |
| ScopedLocalJavaRef<jobject> j_audio_output_manager( |
| env->CallStarboardObjectMethodOrAbort( |
| "getAudioOutputManager", |
| "()Ldev/cobalt/media/AudioOutputManager;")); |
| env->CallVoidMethodOrAbort( |
| j_audio_output_manager.Get(), "destroyAudioTrackBridge", |
| "(Ldev/cobalt/media/AudioTrackBridge;)V", j_audio_track_bridge_); |
| env->DeleteGlobalRef(j_audio_track_bridge_); |
| j_audio_track_bridge_ = nullptr; |
| } |
| |
| if (j_audio_data_) { |
| env->DeleteGlobalRef(j_audio_data_); |
| j_audio_data_ = nullptr; |
| } |
| } |
| |
| // static |
| int AudioTrackBridge::GetMinBufferSizeInFrames( |
| SbMediaAudioSampleType sample_type, |
| int channels, |
| int sampling_frequency_hz, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| |
| ScopedLocalJavaRef<jobject> j_audio_output_manager( |
| env->CallStarboardObjectMethodOrAbort( |
| "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;")); |
| int audio_track_min_buffer_size = static_cast<int>(env->CallIntMethodOrAbort( |
| j_audio_output_manager.Get(), "getMinBufferSize", "(III)I", |
| GetAudioFormatSampleType(kSbMediaAudioCodingTypePcm, sample_type), |
| sampling_frequency_hz, channels)); |
| return audio_track_min_buffer_size / channels / |
| GetBytesPerSample(sample_type); |
| } |
| |
| void AudioTrackBridge::Play(JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V"); |
| SB_LOG(INFO) << "AudioTrackBridge playing."; |
| } |
| |
| void AudioTrackBridge::Pause(JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V"); |
| SB_LOG(INFO) << "AudioTrackBridge paused."; |
| } |
| |
| void AudioTrackBridge::Stop(JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| env->CallVoidMethodOrAbort(j_audio_track_bridge_, "stop", "()V"); |
| SB_LOG(INFO) << "AudioTrackBridge stopped."; |
| } |
| |
| void AudioTrackBridge::PauseAndFlush(JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| // For an immediate stop, use pause(), followed by flush() to discard audio |
| // data that hasn't been played back yet. |
| env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V"); |
| // Flushes the audio data currently queued for playback. Any data that has |
| // been written but not yet presented will be discarded. |
| env->CallVoidMethodOrAbort(j_audio_track_bridge_, "flush", "()V"); |
| } |
| |
| int AudioTrackBridge::WriteSample(const float* samples, |
| int num_of_samples, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| SB_DCHECK(num_of_samples <= max_samples_per_write_); |
| |
| num_of_samples = std::min(num_of_samples, max_samples_per_write_); |
| |
| // TODO: Test this code path |
| env->SetFloatArrayRegion(static_cast<jfloatArray>(j_audio_data_), kNoOffset, |
| num_of_samples, samples); |
| int samples_written = env->CallIntMethodOrAbort( |
| j_audio_track_bridge_, "write", "([FI)I", j_audio_data_, num_of_samples); |
| return samples_written; |
| } |
| |
| int AudioTrackBridge::WriteSample(const uint16_t* samples, |
| int num_of_samples, |
| SbTime sync_time, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| SB_DCHECK(num_of_samples <= max_samples_per_write_); |
| |
| num_of_samples = std::min(num_of_samples, max_samples_per_write_); |
| |
| // TODO: Test this code path |
| env->SetByteArrayRegion(static_cast<jbyteArray>(j_audio_data_), kNoOffset, |
| num_of_samples * sizeof(uint16_t), |
| reinterpret_cast<const jbyte*>(samples)); |
| |
| int bytes_written = env->CallIntMethodOrAbort( |
| j_audio_track_bridge_, "write", "([BIJ)I", j_audio_data_, |
| num_of_samples * sizeof(uint16_t), sync_time); |
| if (bytes_written < 0) { |
| // Error code returned as negative value, like AudioTrack.ERROR_DEAD_OBJECT. |
| return bytes_written; |
| } |
| SB_DCHECK(bytes_written % sizeof(uint16_t) == 0); |
| return bytes_written / sizeof(uint16_t); |
| } |
| |
| int AudioTrackBridge::WriteSample(const uint8_t* samples, |
| int num_of_samples, |
| SbTime sync_time, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| SB_DCHECK(num_of_samples <= max_samples_per_write_); |
| |
| num_of_samples = std::min(num_of_samples, max_samples_per_write_); |
| |
| env->SetByteArrayRegion(static_cast<jbyteArray>(j_audio_data_), kNoOffset, |
| num_of_samples, |
| reinterpret_cast<const jbyte*>(samples)); |
| |
| int bytes_written = |
| env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([BIJ)I", |
| j_audio_data_, num_of_samples, sync_time); |
| if (bytes_written < 0) { |
| // Error code returned as negative value, like AudioTrack.ERROR_DEAD_OBJECT. |
| return bytes_written; |
| } |
| return bytes_written; |
| } |
| |
| void AudioTrackBridge::SetVolume(double volume, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| jint status = env->CallIntMethodOrAbort(j_audio_track_bridge_, "setVolume", |
| "(F)I", static_cast<float>(volume)); |
| if (status != 0) { |
| SB_LOG(ERROR) << "Failed to set volume to " << volume; |
| } |
| } |
| |
| int64_t AudioTrackBridge::GetAudioTimestamp( |
| SbTime* updated_at, |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| ScopedLocalJavaRef<jobject> j_audio_timestamp( |
| env->CallObjectMethodOrAbort(j_audio_track_bridge_, "getAudioTimestamp", |
| "()Landroid/media/AudioTimestamp;")); |
| if (updated_at) { |
| *updated_at = |
| env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "nanoTime", "J") / |
| kSbTimeNanosecondsPerMicrosecond; |
| } |
| return env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "framePosition", |
| "J"); |
| } |
| |
| bool AudioTrackBridge::GetAndResetHasAudioDeviceChanged( |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| ScopedLocalJavaRef<jobject> j_audio_output_manager( |
| env->CallStarboardObjectMethodOrAbort( |
| "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;")); |
| |
| return env->CallBooleanMethodOrAbort( |
| j_audio_output_manager.Get(), "getAndResetHasAudioDeviceChanged", "()Z"); |
| } |
| |
| int AudioTrackBridge::GetUnderrunCount(JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| return env->CallIntMethodOrAbort(j_audio_track_bridge_, "getUnderrunCount", |
| "()I"); |
| } |
| |
| int AudioTrackBridge::GetStartThresholdInFrames( |
| JniEnvExt* env /*= JniEnvExt::Get()*/) { |
| SB_DCHECK(env); |
| SB_DCHECK(is_valid()); |
| |
| return env->CallIntMethodOrAbort(j_audio_track_bridge_, |
| "getStartThresholdInFrames", "()I"); |
| } |
| |
| } // namespace shared |
| } // namespace android |
| } // namespace starboard |