summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Krzysztof Kopyściński <kopyscinski@google.com> 2025-03-21 07:53:54 +0000
committer Krzysztof Kopyściński <kopyscinski@google.com> 2025-03-21 10:45:59 +0000
commitf0d5530b3b6937884105198385d851274ef2aa13 (patch)
tree57904ab5d93457305999184ea0a9048540ac97c9
parent9001e81c9c3bb1598e46d82c4a41f3bba7c88323 (diff)
Pandora: prepare for LE Audio streaming
This patch moves streaming functions from HAP interface to LE Audio one, making it possible to use by all test groups. It also applies fixes to logic in kotlin, making it usable to enter streaming state. Bug: 398162752 Flag: EXEMPT, test only change Test: atest pts-bot, not actually used in tests yet Change-Id: Ie543469206046ce58306a1d0fe753e22cb2ab113
-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;
+}