summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Krzysztof Kopyscinski (xWF) <kopyscinski@google.com> 2025-03-21 10:18:05 -0700
committer Android (Google) Code Review <android-gerrit@google.com> 2025-03-21 10:18:05 -0700
commit2aaf2069ef3ffabd09841b0d5464bdc092246f56 (patch)
tree0fc54f038e3b7e40c50f326b5e315d119a2122cc
parent728504ce72974b0d1f2058bd772c6ed9f0a573b0 (diff)
parentf0d5530b3b6937884105198385d851274ef2aa13 (diff)
Merge "Pandora: prepare for LE Audio streaming" into main
-rw-r--r--android/pandora/mmi2grpc/mmi2grpc/_audio.py2
-rw-r--r--android/pandora/mmi2grpc/mmi2grpc/hap.py7
-rw-r--r--android/pandora/server/configs/PtsBotTest.xml1
-rw-r--r--android/pandora/server/src/Hap.kt66
-rw-r--r--android/pandora/server/src/LeAudio.kt96
-rw-r--r--pandora/interfaces/pandora_experimental/hap.proto12
-rw-r--r--pandora/interfaces/pandora_experimental/le_audio.proto25
7 files changed, 126 insertions, 83 deletions
diff --git a/android/pandora/mmi2grpc/mmi2grpc/_audio.py b/android/pandora/mmi2grpc/mmi2grpc/_audio.py
index 7bc97167a8..6ab5b148ff 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/_audio.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/_audio.py
@@ -18,7 +18,7 @@ import math
import os
from threading import Thread
-# import numpy as np
+import numpy as np
# from scipy.io import wavfile
SINE_FREQUENCY = 440
diff --git a/android/pandora/mmi2grpc/mmi2grpc/hap.py b/android/pandora/mmi2grpc/mmi2grpc/hap.py
index bd2fd83643..99202e9f7f 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/hap.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/hap.py
@@ -28,7 +28,8 @@ from pandora.security_grpc import Security
from pandora.security_pb2 import LE_LEVEL3, PairingEventAnswer
from pandora_experimental.gatt_grpc import GATT
from pandora_experimental.hap_grpc import HAP
-from pandora_experimental.hap_pb2 import HaPlaybackAudioRequest
+from pandora_experimental.le_audio_pb2 import LeAudioPlaybackAudioRequest
+from pandora_experimental.le_audio_grpc import LeAudio
BASE_UUID = uuid.UUID("00000000-0000-1000-8000-00805F9B34FB")
SINK_ASE_UUID = 0x2BC4
@@ -60,9 +61,9 @@ class HAPProxy(ProfileProxy):
self.connection = None
def convert_frame(data):
- return HaPlaybackAudioRequest(data=data, source=self.source)
+ return LeAudioPlaybackAudioRequest(data=data)
- self.audio = AudioSignal(lambda frames: self.hap.HaPlaybackAudio(map(convert_frame, frames)),
+ self.audio = AudioSignal(lambda frames: self.le_audio.LeAudioPlaybackAudio(map(convert_frame, frames)),
AUDIO_SIGNAL_AMPLITUDE, AUDIO_SIGNAL_SAMPLING_RATE)
def test_started(self, test: str, **kwargs):
diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml
index 2acbd64521..60a609cc56 100644
--- a/android/pandora/server/configs/PtsBotTest.xml
+++ b/android/pandora/server/configs/PtsBotTest.xml
@@ -35,6 +35,7 @@
<target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
<option name="dep-module" value="grpcio" />
<option name="dep-module" value="protobuf==3.20.1" />
+ <option name="dep-module" value="numpy" />
<!-- Re-enable when A2DP audio streaming tests are active, disabling to speed up atest runtime
(installation takes roughly 30s each time, never cached) -->
diff --git a/android/pandora/server/src/Hap.kt b/android/pandora/server/src/Hap.kt
index ddecaeb773..a242f4bcf2 100644
--- a/android/pandora/server/src/Hap.kt
+++ b/android/pandora/server/src/Hap.kt
@@ -29,8 +29,6 @@ import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED
import android.bluetooth.BluetoothProfile.STATE_CONNECTED
import android.content.Context
import android.content.IntentFilter
-import android.media.AudioManager
-import android.media.AudioTrack
import android.util.Log
import com.google.protobuf.Empty
import io.grpc.Status
@@ -61,7 +59,6 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
private val bluetoothAdapter = bluetoothManager.adapter
- private val audioManager = context.getSystemService(AudioManager::class.java)!!
private val bluetoothHapClient =
getProfileProxy<BluetoothHapClient>(context, BluetoothProfile.HAP_CLIENT)
@@ -79,8 +76,6 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
)
.shareIn(scope, SharingStarted.Eagerly)
- private var audioTrack: AudioTrack? = null
-
private class PresetInfoChanged(
var connection: Connection,
var presetInfoList: List<BluetoothHapPresetInfo>,
@@ -287,67 +282,6 @@ class Hap(val context: Context) : HAPImplBase(), Closeable {
}
}
- override fun haPlaybackAudio(
- responseObserver: StreamObserver<Empty>
- ): StreamObserver<HaPlaybackAudioRequest> {
- Log.i(TAG, "haPlaybackAudio")
-
- if (audioTrack == null) {
- audioTrack = buildAudioTrack()
- }
-
- // Play an audio track.
- audioTrack!!.play()
-
- if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
- responseObserver.onError(
- Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
- )
- }
-
- // Volume is maxed out to avoid any amplitude modification of the provided audio data,
- // enabling the test runner to do comparisons between input and output audio signal.
- // Any volume modification should be done before providing the audio data.
- if (audioManager.isVolumeFixed) {
- Log.w(TAG, "Volume is fixed, cannot max out the volume")
- } else {
- val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
- if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
- audioManager.setStreamVolume(
- AudioManager.STREAM_MUSIC,
- maxVolume,
- AudioManager.FLAG_SHOW_UI,
- )
- }
- }
-
- return object : StreamObserver<HaPlaybackAudioRequest> {
- override fun onNext(request: HaPlaybackAudioRequest) {
- val data = request.data.toByteArray()
- val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
- if (written != data.size) {
- responseObserver.onError(
- Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
- )
- }
- }
-
- override fun onError(t: Throwable) {
- t.printStackTrace()
- val sw = StringWriter()
- t.printStackTrace(PrintWriter(sw))
- responseObserver.onError(
- Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
- )
- }
-
- override fun onCompleted() {
- responseObserver.onNext(Empty.getDefaultInstance())
- responseObserver.onCompleted()
- }
- }
- }
-
override fun waitPresetChanged(
request: Empty,
responseObserver: StreamObserver<WaitPresetChangedResponse>,
diff --git a/android/pandora/server/src/LeAudio.kt b/android/pandora/server/src/LeAudio.kt
index 7e7e7e6147..7677f9ea71 100644
--- a/android/pandora/server/src/LeAudio.kt
+++ b/android/pandora/server/src/LeAudio.kt
@@ -25,9 +25,11 @@ import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.media.*
+import android.media.AudioTrack
+import android.media.AudioManager
import android.util.Log
import com.google.protobuf.Empty
+import io.grpc.Status
import io.grpc.stub.StreamObserver
import java.io.Closeable
import kotlinx.coroutines.CoroutineScope
@@ -41,6 +43,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import pandora.LeAudioGrpc.LeAudioImplBase
import pandora.LeAudioProto.*
+import java.io.PrintWriter
+import java.io.StringWriter
@kotlinx.coroutines.ExperimentalCoroutinesApi
class LeAudio(val context: Context) : LeAudioImplBase(), Closeable {
@@ -57,6 +61,8 @@ class LeAudio(val context: Context) : LeAudioImplBase(), Closeable {
private val bluetoothLeAudio =
getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO)
+ private var audioTrack: AudioTrack? = null
+
init {
scope = CoroutineScope(Dispatchers.Default)
val intentFilter = IntentFilter()
@@ -98,4 +104,92 @@ class LeAudio(val context: Context) : LeAudioImplBase(), Closeable {
Empty.getDefaultInstance()
}
}
+
+ override fun leAudioStart(request: LeAudioStartRequest, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ if (audioTrack == null) {
+ audioTrack = buildAudioTrack()
+ }
+ val device = request.connection.toBluetoothDevice(bluetoothAdapter)
+ Log.i(TAG, "start: device=$device")
+
+ if (bluetoothLeAudio.getConnectionState(device) != BluetoothLeAudio.STATE_CONNECTED) {
+ throw RuntimeException("Device is not connected, cannot start")
+ }
+
+ // Configure the selected device as active device if it is not
+ // already.
+ bluetoothLeAudio.setActiveDevice(device)
+
+ // Play an audio track.
+ audioTrack!!.play()
+
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun leAudioStop(request: LeAudioStopRequest, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ checkNotNull(audioTrack) { "No track to pause!" }
+
+ // Play an audio track.
+ audioTrack!!.pause()
+
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun leAudioPlaybackAudio(
+ responseObserver: StreamObserver<LeAudioPlaybackAudioResponse>
+ ): StreamObserver<LeAudioPlaybackAudioRequest> {
+ Log.i(TAG, "leAudioPlaybackAudio")
+
+ if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
+ responseObserver.onError(
+ Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
+ )
+ }
+
+ // Volume is maxed out to avoid any amplitude modification of the provided audio data,
+ // enabling the test runner to do comparisons between input and output audio signal.
+ // Any volume modification should be done before providing the audio data.
+ if (audioManager.isVolumeFixed) {
+ Log.w(TAG, "Volume is fixed, cannot max out the volume")
+ } else {
+ val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
+ audioManager.setStreamVolume(
+ AudioManager.STREAM_MUSIC,
+ maxVolume,
+ AudioManager.FLAG_SHOW_UI,
+ )
+ }
+ }
+
+ return object : StreamObserver<LeAudioPlaybackAudioRequest> {
+ override fun onNext(request: LeAudioPlaybackAudioRequest) {
+ val data = request.data.toByteArray()
+ val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
+ if (written != data.size) {
+ responseObserver.onError(
+ Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
+ )
+ }
+ }
+
+ override fun onError(t: Throwable) {
+ t.printStackTrace()
+ val sw = StringWriter()
+ t.printStackTrace(PrintWriter(sw))
+ responseObserver.onError(
+ Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
+ )
+ }
+
+ override fun onCompleted() {
+ responseObserver.onNext(LeAudioPlaybackAudioResponse.getDefaultInstance())
+ responseObserver.onCompleted()
+ }
+ }
+ }
}
diff --git a/pandora/interfaces/pandora_experimental/hap.proto b/pandora/interfaces/pandora_experimental/hap.proto
index 5847887529..d56fdb515d 100644
--- a/pandora/interfaces/pandora_experimental/hap.proto
+++ b/pandora/interfaces/pandora_experimental/hap.proto
@@ -28,8 +28,6 @@ service HAP {
rpc SetNextPreset(SetNextPresetRequest) returns (google.protobuf.Empty);
// Set next preset
rpc SetPreviousPreset(SetPreviousPresetRequest) returns (google.protobuf.Empty);
- // Playback audio
- rpc HaPlaybackAudio(stream HaPlaybackAudioRequest) returns (google.protobuf.Empty);
// Set preset name
rpc WritePresetName(WritePresetNameRequest) returns (google.protobuf.Empty);
// Get preset record
@@ -50,16 +48,6 @@ message GetFeaturesResponse{
int32 features = 1;
}
-// Request of the `PlaybackAudio` method.
-message HaPlaybackAudioRequest {
- // Low Energy connection.
- Connection connection = 1;
- // Audio data to playback.
- // `data` should be interleaved stereo frames with 16-bit signed little-endian
- // linear PCM samples at 44100Hz sample rate
- bytes data = 2;
-}
-
// Request of the `SetActivePreset` method.
message SetActivePresetRequest {
// Connection crafted by grpc server
diff --git a/pandora/interfaces/pandora_experimental/le_audio.proto b/pandora/interfaces/pandora_experimental/le_audio.proto
index 4500564991..4628ac9a2e 100644
--- a/pandora/interfaces/pandora_experimental/le_audio.proto
+++ b/pandora/interfaces/pandora_experimental/le_audio.proto
@@ -10,8 +10,33 @@ package pandora;
// Service to trigger LE Audio procedures.
service LeAudio {
rpc Open(OpenRequest) returns (google.protobuf.Empty);
+ // Playback audio
+ rpc LeAudioPlaybackAudio(stream LeAudioPlaybackAudioRequest) returns (LeAudioPlaybackAudioResponse);
+ // Start an opened stream.
+ rpc LeAudioStart(LeAudioStartRequest) returns (google.protobuf.Empty);
+ // Stop an opened stream.
+ rpc LeAudioStop(LeAudioStopRequest) returns (google.protobuf.Empty);
}
message OpenRequest {
Connection connection = 1;
}
+
+// Request for the `LeAudioStart` method.
+message LeAudioStartRequest {
+ Connection connection = 1;
+}
+
+// Request of the `PlaybackAudio` method.
+message LeAudioPlaybackAudioRequest {
+ // Audio data to playback.
+ bytes data = 1;
+}
+
+// Response of the `LeAudioPlaybackAudio` method.
+message LeAudioPlaybackAudioResponse {}
+
+// Request of the `LeAudioStop` method.
+message LeAudioStopRequest {
+ Connection connection = 1;
+}