diff options
author | 2025-03-21 07:53:54 +0000 | |
---|---|---|
committer | 2025-03-21 10:45:59 +0000 | |
commit | f0d5530b3b6937884105198385d851274ef2aa13 (patch) | |
tree | 57904ab5d93457305999184ea0a9048540ac97c9 | |
parent | 9001e81c9c3bb1598e46d82c4a41f3bba7c88323 (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.py | 2 | ||||
-rw-r--r-- | android/pandora/mmi2grpc/mmi2grpc/hap.py | 7 | ||||
-rw-r--r-- | android/pandora/server/configs/PtsBotTest.xml | 1 | ||||
-rw-r--r-- | android/pandora/server/src/Hap.kt | 66 | ||||
-rw-r--r-- | android/pandora/server/src/LeAudio.kt | 96 | ||||
-rw-r--r-- | pandora/interfaces/pandora_experimental/hap.proto | 12 | ||||
-rw-r--r-- | pandora/interfaces/pandora_experimental/le_audio.proto | 25 |
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; +} |