Commit 216db511 authored by Chris Kuiper's avatar Chris Kuiper Committed by Commit Bot

[Chromecast] Cache Android volume tables in native code

This implements a caching logic to store the Android volume tables in native
code. This avoids constant JNI calls that can slow things down too much leading
to problems (see bug for more details).

Bug: [internal] b/71871956
Test: Running on a several AThings Cast-enabled speakers, as well as unit-test.

Change-Id: I5c769d0d98e3d4b00f01d74a0b0ded984993431e
Reviewed-on: https://chromium-review.googlesource.com/1134589Reviewed-by: default avatarLuke Halliwell <halliwell@chromium.org>
Reviewed-by: default avatarKenneth MacKay <kmackay@chromium.org>
Commit-Queue: Chris Kuiper <ckuiper@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577193}
parent eee59aaa
......@@ -103,6 +103,10 @@ cast_test_group("cast_tests") {
]
}
if (is_android && is_cast_using_cma_backend) {
tests += [ "//chromecast/media/cma/backend/android:cast_android_cma_backend_unittests" ]
}
if (!is_android) {
tests += [
":cast_shell_browsertests",
......
......@@ -5,6 +5,7 @@
import("//build/config/android/config.gni")
import("//build/config/android/rules.gni")
import("//chromecast/chromecast.gni")
import("//testing/test.gni")
cast_source_set("cast_media_android") {
sources = [
......@@ -20,6 +21,8 @@ cast_source_set("cast_media_android") {
"media_codec_support_cast_audio.cc",
"media_pipeline_backend_android.cc",
"media_pipeline_backend_android.h",
"volume_cache.cc",
"volume_cache.h",
"volume_control_android.cc",
"volume_control_android.h",
]
......@@ -74,3 +77,23 @@ android_library("audio_track_java") {
"//third_party/android_tools:android_support_v13_java",
]
}
test("cast_android_cma_backend_unittests") {
deps = [
":unit_tests",
]
}
source_set("unit_tests") {
testonly = true
sources = [
"volume_cache_test.cc",
]
deps = [
":cast_media_android",
"//base",
"//base/test:run_all_unittests",
"//testing/gtest",
]
}
......@@ -89,12 +89,18 @@ public final class VolumeMap {
return db;
}
/** Return the max volume index for the given cast type. */
@CalledByNative
static int getMaxVolumeIndex(int castType) {
int streamType = getStreamType(castType);
return MAX_VOLUME_INDEX.get(streamType);
}
/**
* Logs the dB value at each discrete Android volume index for the given cast type.
* Note that this is not identical to the volume table, which may contain a different number
* of points and at different levels.
*/
@CalledByNative
static void dumpVolumeTables(int castType) {
int streamType = getStreamType(castType);
int maxIndex = MAX_VOLUME_INDEX.get(streamType);
......@@ -117,7 +123,6 @@ public final class VolumeMap {
return getStreamVolumeDB(streamType, volumeIndex);
}
@CalledByNative
/**
* Returns the volume level for the given dB value using the volume table for the given type.
*/
......
// Copyright 2018 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 "chromecast/media/cma/backend/android/volume_cache.h"
#include <algorithm>
#include <cmath>
#include "base/logging.h"
namespace chromecast {
namespace media {
VolumeCache::VolumeCache(AudioContentType type, SystemVolumeTableAccessApi* api)
: kMaxVolumeIndex(api->GetMaxVolumeIndex(type)) {
LOG(INFO) << "Build volume cache for type " << static_cast<int>(type) << ":";
cache_.resize(kMaxVolumeIndex + 1);
for (size_t v_idx = 0; v_idx < cache_.size(); v_idx++) {
float v_level = static_cast<float>(v_idx) / kMaxVolumeIndex;
cache_[v_idx] = api->VolumeToDbFS(type, v_level);
LOG(INFO) << " " << v_idx << "(" << v_level << ") -> " << cache_[v_idx];
}
}
VolumeCache::~VolumeCache() = default;
float VolumeCache::VolumeToDbFS(float vol_level) {
if (vol_level <= 0.0f)
return cache_[0];
if (vol_level >= 1.0f)
return cache_[kMaxVolumeIndex];
float vol_idx = vol_level * kMaxVolumeIndex;
// Find the nearest integers below and above vol_idx.
float vol_idx_high = std::ceil(vol_idx);
float vol_idx_low = std::floor(vol_idx);
float db_high = cache_[static_cast<int>(vol_idx_high)];
if (vol_idx_high == vol_idx_low) {
return db_high;
}
// We are in between two consecutive volume points, so interpolate.
// Note that vol_idx_high = vol_idx_low + 1.
float db_low = cache_[static_cast<int>(vol_idx_low)];
float m = (db_high - db_low) / 1.0f;
float db_interpolated = db_low + m * (vol_idx - vol_idx_low);
return db_interpolated;
}
float VolumeCache::DbFSToVolume(float db) {
auto db_high_it = std::lower_bound(cache_.begin(), cache_.end(), db);
if (db_high_it == cache_.end())
return 1.0f;
if (db_high_it == cache_.begin())
return 0.0f;
int vol_idx_high = db_high_it - cache_.begin();
if (db == *db_high_it)
return static_cast<float>(vol_idx_high) / kMaxVolumeIndex;
// We are in between two consecutive volume points, so interpolate.
// Note that vol_idx_high = vol_idx_low + 1.
auto db_low_it = std::prev(db_high_it);
float m = 1.0f / (*db_high_it - *db_low_it);
float vol_idx = static_cast<float>(vol_idx_high) - m * (*db_high_it - db);
return vol_idx / kMaxVolumeIndex;
}
} // namespace media
} // namespace chromecast
// Copyright 2018 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.
#ifndef CHROMECAST_MEDIA_CMA_BACKEND_ANDROID_VOLUME_CACHE_H_
#define CHROMECAST_MEDIA_CMA_BACKEND_ANDROID_VOLUME_CACHE_H_
#include <vector>
#include "base/macros.h"
#include "chromecast/public/volume_control.h"
namespace chromecast {
namespace media {
// Wrapper class to inject an API into VolumeCache that is used to populate the
// cache.
class SystemVolumeTableAccessApi {
public:
SystemVolumeTableAccessApi() = default;
virtual ~SystemVolumeTableAccessApi() = default;
virtual int GetMaxVolumeIndex(AudioContentType type) = 0;
virtual float VolumeToDbFS(AudioContentType type, float volume) = 0;
};
// Builds a cache of the system's volume table and provides access to it.
class VolumeCache {
public:
VolumeCache(AudioContentType type, SystemVolumeTableAccessApi* api);
~VolumeCache();
// Returns the mapped and interpolated dBFS value for the given volume level,
// using the cached volume table.
float VolumeToDbFS(float vol_level);
// Returns the mapped and interpolated volume value for the given dBFS value,
// using the cached volume table.
float DbFSToVolume(float db);
private:
const int kMaxVolumeIndex;
std::vector<float> cache_;
DISALLOW_COPY_AND_ASSIGN(VolumeCache);
};
} // namespace media
} // namespace chromecast
#endif // CHROMECAST_MEDIA_CMA_BACKEND_ANDROID_VOLUME_CACHE_H_
// Copyright 2018 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 "chromecast/media/cma/backend/android/volume_cache.h"
#include <cmath>
#include "testing/gtest/include/gtest/gtest.h"
namespace chromecast {
namespace media {
namespace {
const int kMaxVolumeIndex = 10;
const float kTestVolumeTable[kMaxVolumeIndex + 1] = {
-100.0f, -88.0f, -82.0f, -70.0f, -62.5f, -49.9f,
-40.5f, -30.0f, -28.2f, -10.0f, 0.0f};
} // namespace
class VolumeCacheTest : protected SystemVolumeTableAccessApi,
public testing::Test {
protected:
VolumeCacheTest() : volume_cache_(AudioContentType::kMedia, this) {}
~VolumeCacheTest() override {}
// SystemVolumeTableAccessApi implementation.
// We use kTestVolumeTable for kMedia and just return -1.0f for other types.
// That allows to test if the type is properly used in the c'tor.
int GetMaxVolumeIndex(AudioContentType type) override {
return (type == AudioContentType::kMedia) ? kMaxVolumeIndex : 2;
}
float VolumeToDbFS(AudioContentType type, float volume) override {
if (type != AudioContentType::kMedia)
return -1.0f;
int idx_vol = static_cast<int>(std::round(volume * kMaxVolumeIndex));
return kTestVolumeTable[idx_vol];
}
VolumeCache volume_cache_;
};
TEST_F(VolumeCacheTest, CachedValuesMatchesOriginalTable) {
for (int i = 0; i <= kMaxVolumeIndex; i++) {
float v = static_cast<float>(i) / kMaxVolumeIndex;
EXPECT_FLOAT_EQ(kTestVolumeTable[i], volume_cache_.VolumeToDbFS(v));
float db = kTestVolumeTable[i];
EXPECT_FLOAT_EQ(v, volume_cache_.DbFSToVolume(db));
}
}
TEST_F(VolumeCacheTest, BoundaryValues) {
EXPECT_FLOAT_EQ(kTestVolumeTable[0], volume_cache_.VolumeToDbFS(-100.0f));
EXPECT_FLOAT_EQ(kTestVolumeTable[0], volume_cache_.VolumeToDbFS(-1.0f));
EXPECT_FLOAT_EQ(kTestVolumeTable[0], volume_cache_.VolumeToDbFS(-0.1f));
EXPECT_FLOAT_EQ(kTestVolumeTable[0], volume_cache_.VolumeToDbFS(0.0f));
EXPECT_FLOAT_EQ(kTestVolumeTable[kMaxVolumeIndex],
volume_cache_.VolumeToDbFS(1.0f));
EXPECT_FLOAT_EQ(kTestVolumeTable[kMaxVolumeIndex],
volume_cache_.VolumeToDbFS(1.1f));
EXPECT_FLOAT_EQ(kTestVolumeTable[kMaxVolumeIndex],
volume_cache_.VolumeToDbFS(2.0f));
EXPECT_FLOAT_EQ(kTestVolumeTable[kMaxVolumeIndex],
volume_cache_.VolumeToDbFS(100.0f));
float min_db = kTestVolumeTable[0];
EXPECT_FLOAT_EQ(0.0f, volume_cache_.DbFSToVolume(min_db - 100.0f));
EXPECT_FLOAT_EQ(0.0f, volume_cache_.DbFSToVolume(min_db - 1.0f));
EXPECT_FLOAT_EQ(0.0f, volume_cache_.DbFSToVolume(min_db - 0.1f));
EXPECT_FLOAT_EQ(0.0f, volume_cache_.DbFSToVolume(min_db - 0.0f));
float max_db = kTestVolumeTable[kMaxVolumeIndex];
EXPECT_FLOAT_EQ(1.0f, volume_cache_.DbFSToVolume(max_db + 0.0f));
EXPECT_FLOAT_EQ(1.0f, volume_cache_.DbFSToVolume(max_db + 0.1f));
EXPECT_FLOAT_EQ(1.0f, volume_cache_.DbFSToVolume(max_db + 1.0f));
EXPECT_FLOAT_EQ(1.0f, volume_cache_.DbFSToVolume(max_db + 100.0f));
}
TEST_F(VolumeCacheTest, Volume2DbFSInterpolatesCorrectly) {
int i_low = 0, i_high = 1;
for (; i_high <= kMaxVolumeIndex; ++i_high, ++i_low) {
float v_low = static_cast<float>(i_low) / kMaxVolumeIndex;
float v_high = static_cast<float>(i_high) / kMaxVolumeIndex;
float db_low = kTestVolumeTable[i_low];
float db_high = kTestVolumeTable[i_high];
float m = (db_high - db_low) / (v_high - v_low);
for (float v = v_low; v <= v_high; v += 0.1f) {
float expected_db = db_low + m * (v - v_low);
EXPECT_FLOAT_EQ(expected_db, volume_cache_.VolumeToDbFS(v));
}
}
}
TEST_F(VolumeCacheTest, DbFSToVolumeInterpolatesCorrectly) {
int i_low = 0, i_high = 1;
for (; i_high <= kMaxVolumeIndex; ++i_high, ++i_low) {
float v_low = static_cast<float>(i_low) / kMaxVolumeIndex;
float v_high = static_cast<float>(i_high) / kMaxVolumeIndex;
float db_low = kTestVolumeTable[i_low];
float db_high = kTestVolumeTable[i_high];
float m = (v_high - v_low) / (db_high - db_low);
for (float db = db_low; db <= db_high; db += 0.1f) {
float expected_v = v_low + m * (db - db_low);
EXPECT_FLOAT_EQ(expected_v, volume_cache_.DbFSToVolume(db));
}
}
}
TEST_F(VolumeCacheTest, CacheHonorsAudioContentType) {
VolumeCache volume_cache(AudioContentType::kAlarm, this);
EXPECT_FLOAT_EQ(-1.0f, volume_cache.VolumeToDbFS(0.0f));
EXPECT_FLOAT_EQ(-1.0f, volume_cache.VolumeToDbFS(1.0f));
}
} // namespace media
} // namespace chromecast
......@@ -30,44 +30,6 @@
namespace chromecast {
namespace media {
namespace {
#if BUILDFLAG(ENABLE_VOLUME_TABLES_ACCESS)
float VolumeToDbFSByContentType(AudioContentType type, float volume) {
return Java_VolumeMap_volumeToDbFs(base::android::AttachCurrentThread(),
static_cast<int>(type), volume);
}
float DbFSToVolumeByContentType(AudioContentType type, float db) {
return Java_VolumeMap_dbFsToVolume(base::android::AttachCurrentThread(),
static_cast<int>(type), db);
}
#else // Dummy versions.
float VolumeToDbFSByContentType(AudioContentType type, float volume) {
return 1.0f;
}
float DbFSToVolumeByContentType(AudioContentType type, float db) {
return 100;
}
#endif
// For the user of the VolumeControl, all volume values are in the volume table
// domain of kMedia (MUSIC). For volume types other than media, VolumeControl
// converts them internally into their proper volume table domains.
float MapIntoDifferentVolumeTableDomain(AudioContentType from_type,
AudioContentType to_type,
float level) {
if (from_type == to_type) {
return level;
}
float from_db = VolumeToDbFSByContentType(from_type, level);
return DbFSToVolumeByContentType(to_type, from_db);
}
} // namespace
VolumeControlAndroid& GetVolumeControl() {
static base::NoDestructor<VolumeControlAndroid> volume_control;
return *volume_control;
......@@ -154,7 +116,7 @@ void VolumeControlAndroid::SetOutputLimit(AudioContentType type, float limit) {
// The input limit is in the kMedia (MUSIC) volume table domain.
limit = std::max(0.0f, std::min(limit, 1.0f));
float limit_db = VolumeToDbFSByContentType(AudioContentType::kMedia, limit);
float limit_db = VolumeToDbFSCached(AudioContentType::kMedia, limit);
AudioSinkManager::Get()->SetOutputLimitDb(type, limit_db);
}
......@@ -180,19 +142,43 @@ void VolumeControlAndroid::OnMuteChange(
base::Unretained(this), (AudioContentType)type, muted));
}
#if BUILDFLAG(ENABLE_VOLUME_TABLES_ACCESS)
int VolumeControlAndroid::GetMaxVolumeIndex(AudioContentType type) {
return Java_VolumeMap_getMaxVolumeIndex(base::android::AttachCurrentThread(),
static_cast<int>(type));
}
float VolumeControlAndroid::VolumeToDbFS(AudioContentType type, float volume) {
return Java_VolumeMap_volumeToDbFs(base::android::AttachCurrentThread(),
static_cast<int>(type), volume);
}
#else // Dummies:
int VolumeControlAndroid::GetMaxVolumeIndex(AudioContentType type) {
return 1;
}
float VolumeControlAndroid::VolumeToDbFS(AudioContentType type, float volume) {
return 1.0f;
}
#endif
void VolumeControlAndroid::InitializeOnThread() {
DCHECK(thread_.task_runner()->BelongsToCurrentThread());
for (auto type : {AudioContentType::kMedia, AudioContentType::kAlarm,
AudioContentType::kCommunication}) {
#if BUILDFLAG(ENABLE_VOLUME_TABLES_ACCESS)
Java_VolumeMap_dumpVolumeTables(base::android::AttachCurrentThread(),
static_cast<int>(type));
#endif
for (auto type :
{AudioContentType::kMedia, AudioContentType::kAlarm,
AudioContentType::kCommunication, AudioContentType::kOther}) {
std::unique_ptr<VolumeCache> vc(new VolumeCache(type, this));
volume_cache_.emplace(type, std::move(vc));
volumes_[type] =
Java_VolumeControl_getVolume(base::android::AttachCurrentThread(),
j_volume_control_, static_cast<int>(type));
float volume_db = VolumeToDbFSByContentType(type, volumes_[type]);
float volume_db = VolumeToDbFSCached(type, volumes_[type]);
AudioSinkManager::Get()->SetTypeVolumeDb(type, volume_db);
muted_[type] =
Java_VolumeControl_isMuted(base::android::AttachCurrentThread(),
......@@ -226,7 +212,7 @@ void VolumeControlAndroid::SetVolumeOnThread(AudioContentType type,
volumes_[type] = level;
}
float level_db = VolumeToDbFSByContentType(type, level);
float level_db = VolumeToDbFSCached(type, level);
LOG(INFO) << __func__ << ": level=" << level << " (" << level_db << ")";
// Provide the type volume to the sink manager so it can properly calculate
// the limiter multiplier. The volume is *not* applied by the sink though.
......@@ -305,6 +291,27 @@ void VolumeControlAndroid::ReportMuteChangeOnThread(AudioContentType type,
SetMutedOnThread(type, muted, true /* from_android */);
}
float VolumeControlAndroid::MapIntoDifferentVolumeTableDomain(
AudioContentType from_type,
AudioContentType to_type,
float level) {
if (from_type == to_type) {
return level;
}
float from_db = VolumeToDbFSCached(from_type, level);
return DbFSToVolumeCached(to_type, from_db);
}
float VolumeControlAndroid::VolumeToDbFSCached(AudioContentType type,
float vol_level) {
return volume_cache_[type]->VolumeToDbFS(vol_level);
}
float VolumeControlAndroid::DbFSToVolumeCached(AudioContentType type,
float db) {
return volume_cache_[type]->DbFSToVolume(db);
}
//
// Implementation of VolumeControl as defined in public/volume_control.h
//
......@@ -357,13 +364,14 @@ void VolumeControl::SetOutputLimit(AudioContentType type, float limit) {
// static
float VolumeControl::VolumeToDbFS(float volume) {
// The volume value is the kMedia (MUSIC) volume table domain.
return VolumeToDbFSByContentType(AudioContentType::kMedia, volume);
return GetVolumeControl().VolumeToDbFSCached(AudioContentType::kMedia,
volume);
}
// static
float VolumeControl::DbFSToVolume(float db) {
// The db value is the kMedia (MUSIC) volume table domain.
return DbFSToVolumeByContentType(AudioContentType::kMedia, db);
return GetVolumeControl().DbFSToVolumeCached(AudioContentType::kMedia, db);
}
} // namespace media
......
......@@ -15,14 +15,15 @@
#include "base/threading/thread.h"
#include "base/values.h"
#include "chromecast/media/cma/backend/android/audio_sink_manager.h"
#include "chromecast/media/cma/backend/android/volume_cache.h"
namespace chromecast {
namespace media {
class VolumeControlAndroid {
class VolumeControlAndroid : SystemVolumeTableAccessApi {
public:
VolumeControlAndroid();
~VolumeControlAndroid();
~VolumeControlAndroid() override;
void AddVolumeObserver(VolumeObserver* observer);
void RemoveVolumeObserver(VolumeObserver* observer);
......@@ -31,6 +32,8 @@ class VolumeControlAndroid {
bool IsMuted(AudioContentType type);
void SetMuted(AudioContentType type, bool muted);
void SetOutputLimit(AudioContentType type, float limit);
float VolumeToDbFSCached(AudioContentType type, float volume);
float DbFSToVolumeCached(AudioContentType type, float db);
// Called from java to signal a change volume.
void OnVolumeChange(JNIEnv* env,
......@@ -43,6 +46,10 @@ class VolumeControlAndroid {
jint type,
jboolean muted);
// SystemVolumeTableAccessApi implementation.
int GetMaxVolumeIndex(AudioContentType type) override;
float VolumeToDbFS(AudioContentType type, float volume) override;
private:
void InitializeOnThread();
void SetVolumeOnThread(AudioContentType type, float level, bool from_android);
......@@ -50,8 +57,18 @@ class VolumeControlAndroid {
void ReportVolumeChangeOnThread(AudioContentType type, float level);
void ReportMuteChangeOnThread(AudioContentType type, bool muted);
// For the user of the VolumeControl, all volume values are in the volume
// table domain of kMedia (MUSIC). For volume types other than media,
// VolumeControl converts them internally into their proper volume table
// domains.
float MapIntoDifferentVolumeTableDomain(AudioContentType from_type,
AudioContentType to_type,
float level);
base::android::ScopedJavaGlobalRef<jobject> j_volume_control_;
std::map<AudioContentType, std::unique_ptr<VolumeCache>> volume_cache_;
base::Lock volume_lock_;
std::map<AudioContentType, float> volumes_;
std::map<AudioContentType, bool> muted_;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment