diff --git a/audio/aidl/Android.bp b/audio/aidl/Android.bp index 7db50d5ca5..ba22d7bff4 100644 --- a/audio/aidl/Android.bp +++ b/audio/aidl/Android.bp @@ -75,10 +75,14 @@ aidl_interface { "android/hardware/audio/core/IModule.aidl", "android/hardware/audio/core/IStreamIn.aidl", "android/hardware/audio/core/IStreamOut.aidl", + "android/hardware/audio/core/MmapBufferDescriptor.aidl", "android/hardware/audio/core/ModuleDebug.aidl", + "android/hardware/audio/core/StreamDescriptor.aidl", ], imports: [ "android.hardware.audio.common-V1", + "android.hardware.common-V2", + "android.hardware.common.fmq-V1", "android.media.audio.common.types-V1", ], stability: "vintf", diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/AudioPatch.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/AudioPatch.aidl index 1cef4cd1ae..078b5ea329 100644 --- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/AudioPatch.aidl +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/AudioPatch.aidl @@ -37,4 +37,6 @@ parcelable AudioPatch { int id; int[] sourcePortConfigIds; int[] sinkPortConfigIds; + int minimumStreamBufferSizeFrames; + int[] latenciesMs; } diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl index f8bc2c79c6..a8bbb152c7 100644 --- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl @@ -43,10 +43,33 @@ interface IModule { android.media.audio.common.AudioPort[] getAudioPorts(); android.hardware.audio.core.AudioRoute[] getAudioRoutes(); android.hardware.audio.core.AudioRoute[] getAudioRoutesForAudioPort(int portId); - android.hardware.audio.core.IStreamIn openInputStream(int portConfigId, in android.hardware.audio.common.SinkMetadata sinkMetadata); - android.hardware.audio.core.IStreamOut openOutputStream(int portConfigId, in android.hardware.audio.common.SourceMetadata sourceMetadata, in @nullable android.media.audio.common.AudioOffloadInfo offloadInfo); + android.hardware.audio.core.IModule.OpenInputStreamReturn openInputStream(in android.hardware.audio.core.IModule.OpenInputStreamArguments args); + android.hardware.audio.core.IModule.OpenOutputStreamReturn openOutputStream(in android.hardware.audio.core.IModule.OpenOutputStreamArguments args); android.hardware.audio.core.AudioPatch setAudioPatch(in android.hardware.audio.core.AudioPatch requested); boolean setAudioPortConfig(in android.media.audio.common.AudioPortConfig requested, out android.media.audio.common.AudioPortConfig suggested); void resetAudioPatch(int patchId); void resetAudioPortConfig(int portConfigId); + @VintfStability + parcelable OpenInputStreamArguments { + int portConfigId; + android.hardware.audio.common.SinkMetadata sinkMetadata; + long bufferSizeFrames; + } + @VintfStability + parcelable OpenInputStreamReturn { + android.hardware.audio.core.IStreamIn stream; + android.hardware.audio.core.StreamDescriptor desc; + } + @VintfStability + parcelable OpenOutputStreamArguments { + int portConfigId; + android.hardware.audio.common.SourceMetadata sourceMetadata; + @nullable android.media.audio.common.AudioOffloadInfo offloadInfo; + long bufferSizeFrames; + } + @VintfStability + parcelable OpenOutputStreamReturn { + android.hardware.audio.core.IStreamOut stream; + android.hardware.audio.core.StreamDescriptor desc; + } } diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/MmapBufferDescriptor.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/MmapBufferDescriptor.aidl new file mode 100644 index 0000000000..6ea1c69526 --- /dev/null +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/MmapBufferDescriptor.aidl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ +/////////////////////////////////////////////////////////////////////////////// +// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. // +/////////////////////////////////////////////////////////////////////////////// + +// This file is a snapshot of an AIDL file. Do not edit it manually. There are +// two cases: +// 1). this is a frozen version file - do not edit this in any case. +// 2). this is a 'current' file. If you make a backwards compatible change to +// the interface (from the latest frozen version), the build system will +// prompt you to update this file with `m -update-api`. +// +// You must not make a backward incompatible change to any AIDL file built +// with the aidl_interface module type with versions property set. The module +// type is used to build AIDL files in a way that they can be used across +// independently updatable components of the system. If a device is shipped +// with such a backward incompatible change, it has a high risk of breaking +// later when a module using the interface is updated, e.g., Mainline modules. + +package android.hardware.audio.core; +@JavaDerive(equals=true, toString=true) @VintfStability +parcelable MmapBufferDescriptor { + android.hardware.common.Ashmem sharedMemory; + long burstSizeFrames; + int flags; + const int FLAG_INDEX_APPLICATION_SHAREABLE = 0; +} diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/StreamDescriptor.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/StreamDescriptor.aidl new file mode 100644 index 0000000000..472a8a2f45 --- /dev/null +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/StreamDescriptor.aidl @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ +/////////////////////////////////////////////////////////////////////////////// +// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. // +/////////////////////////////////////////////////////////////////////////////// + +// This file is a snapshot of an AIDL file. Do not edit it manually. There are +// two cases: +// 1). this is a frozen version file - do not edit this in any case. +// 2). this is a 'current' file. If you make a backwards compatible change to +// the interface (from the latest frozen version), the build system will +// prompt you to update this file with `m -update-api`. +// +// You must not make a backward incompatible change to any AIDL file built +// with the aidl_interface module type with versions property set. The module +// type is used to build AIDL files in a way that they can be used across +// independently updatable components of the system. If a device is shipped +// with such a backward incompatible change, it has a high risk of breaking +// later when a module using the interface is updated, e.g., Mainline modules. + +package android.hardware.audio.core; +@JavaDerive(equals=true, toString=true) @VintfStability +parcelable StreamDescriptor { + android.hardware.common.fmq.MQDescriptor command; + android.hardware.common.fmq.MQDescriptor reply; + long bufferSizeFrames; + android.hardware.audio.core.StreamDescriptor.AudioBuffer audio; + const int COMMAND_EXIT = 0; + const int COMMAND_BURST = 1; + const int STATUS_OK = 0; + const int STATUS_ILLEGAL_ARGUMENT = 1; + const int STATUS_ILLEGAL_STATE = 2; + @FixedSize @VintfStability + parcelable Position { + long frames; + long timeNs; + } + @FixedSize @VintfStability + parcelable Command { + int code; + int fmqByteCount; + } + @FixedSize @VintfStability + parcelable Reply { + int status; + int fmqByteCount; + android.hardware.audio.core.StreamDescriptor.Position observable; + android.hardware.audio.core.StreamDescriptor.Position hardware; + int latencyMs; + } + @VintfStability + union AudioBuffer { + android.hardware.common.fmq.MQDescriptor fmq; + android.hardware.audio.core.MmapBufferDescriptor mmap; + } +} diff --git a/audio/aidl/android/hardware/audio/core/AudioPatch.aidl b/audio/aidl/android/hardware/audio/core/AudioPatch.aidl index 48ca2142f8..005d4c00ac 100644 --- a/audio/aidl/android/hardware/audio/core/AudioPatch.aidl +++ b/audio/aidl/android/hardware/audio/core/AudioPatch.aidl @@ -37,4 +37,18 @@ parcelable AudioPatch { * unique. */ int[] sinkPortConfigIds; + /** + * The minimum buffer size, in frames, which streams must use for + * this connection configuration. This field is filled out by the + * HAL module on creation of the patch and must be a positive number. + */ + int minimumStreamBufferSizeFrames; + /** + * Latencies, in milliseconds, associated with each sink port config from + * the 'sinkPortConfigIds' field. This field is filled out by the HAL module + * on creation or updating of the patch and must be a positive number. This + * is a nominal value. The current value of latency is provided via + * 'StreamDescriptor' command exchange on each audio I/O operation. + */ + int[] latenciesMs; } diff --git a/audio/aidl/android/hardware/audio/core/IModule.aidl b/audio/aidl/android/hardware/audio/core/IModule.aidl index 802cb2f151..363eb683f3 100644 --- a/audio/aidl/android/hardware/audio/core/IModule.aidl +++ b/audio/aidl/android/hardware/audio/core/IModule.aidl @@ -23,6 +23,7 @@ import android.hardware.audio.core.AudioRoute; import android.hardware.audio.core.IStreamIn; import android.hardware.audio.core.IStreamOut; import android.hardware.audio.core.ModuleDebug; +import android.hardware.audio.core.StreamDescriptor; import android.media.audio.common.AudioOffloadInfo; import android.media.audio.common.AudioPort; import android.media.audio.common.AudioPortConfig; @@ -241,22 +242,49 @@ interface IModule { * 'setAudioPortConfig' method. Existence of an audio patch involving this * port configuration is not required for successful opening of a stream. * + * The requested buffer size is expressed in frames, thus the actual size + * in bytes depends on the audio port configuration. Also, the HAL module + * may end up providing a larger buffer, thus the requested size is treated + * as the minimum size that the client needs. The minimum buffer size + * suggested by the HAL is in the 'AudioPatch.minimumStreamBufferSizeFrames' + * field, returned as a result of calling the 'setAudioPatch' method. + * * Only one stream is allowed per audio port configuration. HAL module can * also set a limit on how many output streams can be opened for a particular * mix port by using its 'AudioPortMixExt.maxOpenStreamCount' field. * - * @return An opened input stream. - * @param portConfigId The ID of the audio mix port config. - * @param sinkMetadata Description of the audio that will be recorded. + * Note that although it's not prohibited to open a stream on a mix port + * configuration which is not connected (using a patch) to any device port, + * and set up a patch afterwards, this is not the recommended sequence of + * calls, because setting up of a patch might fail due to an insufficient + * stream buffer size. + * + * @return An opened input stream and the associated descriptor. + * @param args Input arguments, see 'OpenInputStreamArguments' parcelable. * @throws EX_ILLEGAL_ARGUMENT In the following cases: * - If the port config can not be found by the ID. * - If the port config is not of an input mix port. + * - If a buffer of the requested size can not be provided. * @throws EX_ILLEGAL_STATE In the following cases: * - If the port config already has a stream opened on it. * - If the limit on the open stream count for the port has * been reached. */ - IStreamIn openInputStream(int portConfigId, in SinkMetadata sinkMetadata); + @VintfStability + parcelable OpenInputStreamArguments { + /** The ID of the audio mix port config. */ + int portConfigId; + /** Description of the audio that will be recorded. */ + SinkMetadata sinkMetadata; + /** Requested audio I/O buffer minimum size, in frames. */ + long bufferSizeFrames; + } + @VintfStability + parcelable OpenInputStreamReturn { + IStreamIn stream; + StreamDescriptor desc; + } + OpenInputStreamReturn openInputStream(in OpenInputStreamArguments args); /** * Open an output stream using an existing audio mix port configuration. @@ -269,21 +297,33 @@ interface IModule { * the framework must provide additional information about the encoded * audio stream in 'offloadInfo' argument. * + * The requested buffer size is expressed in frames, thus the actual size + * in bytes depends on the audio port configuration. Also, the HAL module + * may end up providing a larger buffer, thus the requested size is treated + * as the minimum size that the client needs. The minimum buffer size + * suggested by the HAL is in the 'AudioPatch.minimumStreamBufferSizeFrames' + * field, returned as a result of calling the 'setAudioPatch' method. + * * Only one stream is allowed per audio port configuration. HAL module can * also set a limit on how many output streams can be opened for a particular * mix port by using its 'AudioPortMixExt.maxOpenStreamCount' field. * Only one stream can be opened on the audio port with 'PRIMARY' output * flag. This rule can not be overridden with 'maxOpenStreamCount' field. * - * @return An opened output stream. - * @param portConfigId The ID of the audio mix port config. - * @param sourceMetadata Description of the audio that will be played. - * @param offloadInfo Additional information for offloaded playback. + * Note that although it's not prohibited to open a stream on a mix port + * configuration which is not connected (using a patch) to any device port, + * and set up a patch afterwards, this is not the recommended sequence of + * calls, because setting up of a patch might fail due to an insufficient + * stream buffer size. + * + * @return An opened output stream and the associated descriptor. + * @param args Input arguments, see 'OpenOutputStreamArguments' parcelable. * @throws EX_ILLEGAL_ARGUMENT In the following cases: * - If the port config can not be found by the ID. * - If the port config is not of an output mix port. * - If the offload info is not provided for an offload * port configuration. + * - If a buffer of the requested size can not be provided. * @throws EX_ILLEGAL_STATE In the following cases: * - If the port config already has a stream opened on it. * - If the limit on the open stream count for the port has @@ -291,8 +331,23 @@ interface IModule { * - If another opened stream already exists for the 'PRIMARY' * output port. */ - IStreamOut openOutputStream(int portConfigId, in SourceMetadata sourceMetadata, - in @nullable AudioOffloadInfo offloadInfo); + @VintfStability + parcelable OpenOutputStreamArguments { + /** The ID of the audio mix port config. */ + int portConfigId; + /** Description of the audio that will be played. */ + SourceMetadata sourceMetadata; + /** Additional information used for offloaded playback only. */ + @nullable AudioOffloadInfo offloadInfo; + /** Requested audio I/O buffer minimum size, in frames. */ + long bufferSizeFrames; + } + @VintfStability + parcelable OpenOutputStreamReturn { + IStreamOut stream; + StreamDescriptor desc; + } + OpenOutputStreamReturn openOutputStream(in OpenOutputStreamArguments args); /** * Set an audio patch. @@ -300,19 +355,27 @@ interface IModule { * This method creates new or updates an existing audio patch. If the * requested audio patch does not have a specified id, then a new patch is * created and an ID is allocated for it by the HAL module. Otherwise an - * attempt to update an existing patch is made. It is recommended that - * updating of an existing audio patch should be performed by the HAL module - * in a way that does not interrupt active audio streams involving audio - * port configurations of the patch. If the HAL module is unable to avoid - * interruption when updating a certain patch, it is permitted to allocate a - * new patch ID for the result. The returned audio patch contains all the - * information about the new or updated audio patch. + * attempt to update an existing patch is made. + * + * The operation of updating an existing audio patch must not change + * playback state of audio streams opened on the audio port configurations + * of the patch. That is, the HAL module must still be able to consume or + * to provide data from / to streams continuously during the patch + * switching. Natural intermittent audible loss of some audio frames due to + * switching between device ports which does not affect stream playback is + * allowed. If the HAL module is unable to avoid playback or recording + * state change when updating a certain patch, it must return an error. In + * that case, the client must take care of changing port configurations, + * patches, and recreating streams in a way which provides an acceptable + * user experience. * * Audio port configurations specified in the patch must be obtained by * calling 'setAudioPortConfig' method. There must be an audio route which * allows connection between the audio ports whose configurations are used. - * An audio patch may be created before or after an audio steam is created - * for this configuration. + * + * When updating an existing audio patch, nominal latency values may change + * and must be provided by the HAL module in the returned 'AudioPatch' + * structure. * * @return Resulting audio patch. * @param requested Requested audio patch. @@ -324,6 +387,9 @@ interface IModule { * @throws EX_ILLEGAL_STATE In the following cases: * - If application of the patch can only use a route with an * exclusive use the sink port, and it is already patched. + * - If updating an existing patch will cause interruption + * of audio, or requires re-opening of streams due to + * change of minimum audio I/O buffer size. * @throws EX_UNSUPPORTED_OPERATION If the patch can not be established because * the HAL module does not support this otherwise valid * patch configuration. For example, if it's a patch diff --git a/audio/aidl/android/hardware/audio/core/IStreamIn.aidl b/audio/aidl/android/hardware/audio/core/IStreamIn.aidl index b770449d52..7205bb8e18 100644 --- a/audio/aidl/android/hardware/audio/core/IStreamIn.aidl +++ b/audio/aidl/android/hardware/audio/core/IStreamIn.aidl @@ -28,7 +28,9 @@ interface IStreamIn { * * Releases any resources allocated for this stream on the HAL module side. * The stream can not be operated after it has been closed. Methods of this - * interface throw EX_ILLEGAL_STATE in for a closed stream. + * interface throw EX_ILLEGAL_STATE for a closed stream. + * + * The associated stream descriptor can be released once this method returns. * * @throws EX_ILLEGAL_STATE If the stream has already been closed. */ diff --git a/audio/aidl/android/hardware/audio/core/IStreamOut.aidl b/audio/aidl/android/hardware/audio/core/IStreamOut.aidl index 60212fc891..0a5aacdd7a 100644 --- a/audio/aidl/android/hardware/audio/core/IStreamOut.aidl +++ b/audio/aidl/android/hardware/audio/core/IStreamOut.aidl @@ -28,7 +28,9 @@ interface IStreamOut { * * Releases any resources allocated for this stream on the HAL module side. * The stream can not be operated after it has been closed. Methods of this - * interface throw EX_ILLEGAL_STATE in for a closed stream. + * interface throw EX_ILLEGAL_STATE for a closed stream. + * + * The associated stream descriptor can be released once this method returns. * * @throws EX_ILLEGAL_STATE If the stream has already been closed. */ diff --git a/audio/aidl/android/hardware/audio/core/MmapBufferDescriptor.aidl b/audio/aidl/android/hardware/audio/core/MmapBufferDescriptor.aidl new file mode 100644 index 0000000000..108bcbed40 --- /dev/null +++ b/audio/aidl/android/hardware/audio/core/MmapBufferDescriptor.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +package android.hardware.audio.core; + +import android.hardware.common.Ashmem; + +/** + * MMap buffer descriptor is used by streams opened in MMap No IRQ mode. + */ +@JavaDerive(equals=true, toString=true) +@VintfStability +parcelable MmapBufferDescriptor { + /** + * MMap memory buffer. + */ + Ashmem sharedMemory; + /** + * Transfer size granularity in frames. + */ + long burstSizeFrames; + /** + * Attributes describing the buffer. Bitmask indexed by FLAG_INDEX_* + * constants. + */ + int flags; + + /** + * Whether the buffer can be securely shared to untrusted applications + * through the AAudio exclusive mode. + * + * Only set this flag if applications are restricted from accessing the + * memory surrounding the audio data buffer by a kernel mechanism. + * See Linux kernel's dma-buf + * (https://www.kernel.org/doc/html/v4.16/driver-api/dma-buf.html). + */ + const int FLAG_INDEX_APPLICATION_SHAREABLE = 0; +} diff --git a/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl b/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl new file mode 100644 index 0000000000..f2338e0d2a --- /dev/null +++ b/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +package android.hardware.audio.core; + +import android.hardware.audio.core.MmapBufferDescriptor; +import android.hardware.common.fmq.MQDescriptor; +import android.hardware.common.fmq.SynchronizedReadWrite; +import android.hardware.common.fmq.UnsynchronizedWrite; + +/** + * Stream descriptor contains fast message queues and buffers used for sending + * and receiving audio data. The descriptor complements IStream* interfaces by + * providing communication channels that serve as an alternative to Binder + * transactions. + * + * Handling of audio data and commands must be done by the HAL module on a + * dedicated thread with high priority, for all modes, including MMap No + * IRQ. The HAL module is responsible for creating this thread and setting its + * priority. The HAL module is also responsible for serializing access to the + * internal components of the stream while serving commands invoked via the + * stream's AIDL interface and commands invoked via the command queue of the + * descriptor. + */ +@JavaDerive(equals=true, toString=true) +@VintfStability +parcelable StreamDescriptor { + /** + * Position binds together a position within the stream and time. + * + * The timestamp must use "monotonic" clock. + * + * The frame count must advance between consecutive I/O operations, and stop + * advancing when the stream was put into the 'standby' mode. On exiting the + * 'standby' mode, the frame count must not reset, but continue counting. + */ + @VintfStability + @FixedSize + parcelable Position { + /** Frame count. */ + long frames; + /** Timestamp in nanoseconds. */ + long timeNs; + } + + /** + * The exit command is used to unblock the HAL thread and ask it to exit. + * This is the last command that the client sends via the StreamDescriptor. + * The HAL module must reply to this command in order to unblock the client, + * and cease waiting on the command queue. + */ + const int COMMAND_EXIT = 0; + /** + * The command used for audio I/O, see 'AudioBuffer'. For MMap No IRQ mode + * this command only provides updated positions and latency because actual + * audio I/O is done via the 'AudioBuffer.mmap' shared buffer. + */ + const int COMMAND_BURST = 1; + + /** + * Used for sending commands to the HAL module. The client writes into + * the queue, the HAL module reads. The queue can only contain a single + * command. + */ + @VintfStability + @FixedSize + parcelable Command { + /** + * One of COMMAND_* codes. + */ + int code; + /** + * For output streams: the amount of bytes provided by the client in the + * 'audio.fmq' queue. + * For input streams: the amount of bytes requested by the client to read + * from the hardware into the 'audio.fmq' queue. + */ + int fmqByteCount; + } + MQDescriptor command; + + /** + * No error, the command completed successfully. + */ + const int STATUS_OK = 0; + /** + * Invalid data provided in the command, e.g. unknown command code or + * negative 'fmqByteCount' value. + */ + const int STATUS_ILLEGAL_ARGUMENT = 1; + /** + * The HAL module is not in the state when it can complete the command. + */ + const int STATUS_ILLEGAL_STATE = 2; + + /** + * Used for providing replies to commands. The HAL module writes into + * the queue, the client reads. The queue can only contain a single reply, + * corresponding to the last command sent by the client. + */ + @VintfStability + @FixedSize + parcelable Reply { + /** + * One of STATUS_* statuses. + */ + int status; + /** + * For output streams: the amount of bytes actually consumed by the HAL + * module from the 'audio.fmq' queue. + * For input streams: the amount of bytes actually provided by the HAL + * in the 'audio.fmq' queue. + */ + int fmqByteCount; + /** + * For output streams: the moment when the specified stream position + * was presented to an external observer (i.e. presentation position). + * For input streams: the moment when data at the specified stream position + * was acquired (i.e. capture position). + */ + Position observable; + /** + * Used only for MMap streams to provide the hardware read / write + * position for audio data in the shared memory buffer 'audio.mmap'. + */ + Position hardware; + /** + * Current latency reported by the hardware. + */ + int latencyMs; + } + MQDescriptor reply; + + /** + * Total buffer size in frames. This applies both to the size of the 'audio.fmq' + * queue and to the size of the shared memory buffer for MMap No IRQ streams. + * Note that this size may end up being slightly larger than the size requested + * in a call to 'IModule.openInputStream' or 'openOutputStream' due to memory + * alignment requirements. + */ + long bufferSizeFrames; + + /** + * Used for sending or receiving audio data to/from the stream. In the case + * of MMap No IRQ streams this structure only contains the information about + * the shared memory buffer. Audio data is sent via the shared buffer + * directly. + */ + @VintfStability + union AudioBuffer { + /** + * The fast message queue used for all modes except MMap No IRQ. Access + * to this queue is synchronized via the 'command' and 'reply' queues + * as described below. + * + * For output streams the following sequence of operations is used: + * 1. The client puts audio data into the 'audio.fmq' queue. + * 2. The client writes the 'BURST' command into the 'command' queue, + * and hangs on waiting on a read from the 'reply' queue. + * 3. The high priority thread in the HAL module wakes up due to 2. + * 4. The HAL module reads the command and audio data. + * 5. The HAL module writes the command status and current positions + * into 'reply' queue, and hangs on waiting on a read from + * the 'command' queue. + * + * For input streams the following sequence of operations is used: + * 1. The client writes the 'BURST' command into the 'command' queue, + * and hangs on waiting on a read from the 'reply' queue. + * 2. The high priority thread in the HAL module wakes up due to 1. + * 3. The HAL module puts audio data into the 'audio.fmq' queue. + * 4. The HAL module writes the command status and current positions + * into 'reply' queue, and hangs on waiting on a read from + * the 'command' queue. + * 5. The client wakes up due to 4. + * 6. The client reads the reply and audio data. + */ + MQDescriptor fmq; + /** + * MMap buffers are shared directly with the DSP, which operates + * independently from the CPU. Writes and reads into these buffers + * are not synchronized with 'command' and 'reply' queues. However, + * the client still uses the 'BURST' command for obtaining current + * positions from the HAL module. + */ + MmapBufferDescriptor mmap; + } + AudioBuffer audio; +} diff --git a/audio/aidl/common/Android.bp b/audio/aidl/common/Android.bp new file mode 100644 index 0000000000..37da9d66ec --- /dev/null +++ b/audio/aidl/common/Android.bp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "hardware_interfaces_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["hardware_interfaces_license"], +} + +cc_library_headers { + name: "libaudioaidlcommon", + host_supported: true, + vendor_available: true, + export_include_dirs: ["include"], + header_libs: [ + "libbase_headers", + "libsystem_headers", + ], + export_header_lib_headers: [ + "libbase_headers", + "libsystem_headers", + ], +} + +cc_test { + name: "libaudioaidlcommon_test", + host_supported: true, + vendor_available: true, + header_libs: [ + "libaudioaidlcommon", + ], + shared_libs: [ + "liblog", + ], + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + "-Wthread-safety", + ], + srcs: [ + "tests/streamworker_tests.cpp", + ], + test_suites: [ + "general-tests", + ], +} diff --git a/audio/aidl/common/TEST_MAPPING b/audio/aidl/common/TEST_MAPPING new file mode 100644 index 0000000000..9dcf44ed7c --- /dev/null +++ b/audio/aidl/common/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "libaudioaidlcommon_test" + } + ] +} diff --git a/audio/aidl/common/include/StreamWorker.h b/audio/aidl/common/include/StreamWorker.h new file mode 100644 index 0000000000..776490493d --- /dev/null +++ b/audio/aidl/common/include/StreamWorker.h @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +template +class StreamWorker { + enum class WorkerState { STOPPED, RUNNING, PAUSE_REQUESTED, PAUSED, RESUME_REQUESTED }; + + public: + StreamWorker() = default; + ~StreamWorker() { stop(); } + // Note that 'priority' here is what is known as the 'nice number' in *nix systems. + // The nice number is used with the default scheduler. For threads that + // need to use a specialized scheduler (e.g. SCHED_FIFO) and set the priority within it, + // it is recommended to implement an appropriate configuration sequence within `workerInit`. + bool start(const std::string& name = "", int priority = ANDROID_PRIORITY_DEFAULT) { + mThreadName = name; + mThreadPriority = priority; + mWorker = std::thread(&StreamWorker::workerThread, this); + std::unique_lock lock(mWorkerLock); + android::base::ScopedLockAssertion lock_assertion(mWorkerLock); + mWorkerCv.wait(lock, [&]() { + android::base::ScopedLockAssertion lock_assertion(mWorkerLock); + return mWorkerState == WorkerState::RUNNING || !mError.empty(); + }); + mWorkerStateChangeRequest = false; + return mWorkerState == WorkerState::RUNNING; + } + void pause() { switchWorkerStateSync(WorkerState::RUNNING, WorkerState::PAUSE_REQUESTED); } + void resume() { switchWorkerStateSync(WorkerState::PAUSED, WorkerState::RESUME_REQUESTED); } + bool hasError() { + std::lock_guard lock(mWorkerLock); + return !mError.empty(); + } + std::string getError() { + std::lock_guard lock(mWorkerLock); + return mError; + } + void stop() { + { + std::lock_guard lock(mWorkerLock); + if (mError.empty()) { + if (mWorkerState == WorkerState::STOPPED) return; + mWorkerState = WorkerState::STOPPED; + mWorkerStateChangeRequest = true; + } + } + if (mWorker.joinable()) { + mWorker.join(); + } + } + bool waitForAtLeastOneCycle() { + WorkerState newState; + switchWorkerStateSync(WorkerState::RUNNING, WorkerState::PAUSE_REQUESTED, &newState); + if (newState != WorkerState::PAUSED) return false; + switchWorkerStateSync(newState, WorkerState::RESUME_REQUESTED, &newState); + return newState == WorkerState::RUNNING; + } + // Only used by unit tests. + void testLockUnlockMutex(bool lock) NO_THREAD_SAFETY_ANALYSIS { + lock ? mWorkerLock.lock() : mWorkerLock.unlock(); + } + std::thread::native_handle_type testGetThreadNativeHandle() { return mWorker.native_handle(); } + + // Methods that need to be provided by subclasses: + // + // Called once at the beginning of the thread loop. Must return + // an empty string to enter the thread loop, otherwise the thread loop + // exits and the worker switches into the 'error' state, setting + // the error to the returned value. + // std::string workerInit(); + // + // Called for each thread loop unless the thread is in 'paused' state. + // Must return 'true' to continue running, otherwise the thread loop + // exits and the worker switches into the 'error' state with a generic + // error message. It is recommended that the subclass reports any + // problems via logging facilities. + // bool workerCycle(); + + private: + void switchWorkerStateSync(WorkerState oldState, WorkerState newState, + WorkerState* finalState = nullptr) { + std::unique_lock lock(mWorkerLock); + android::base::ScopedLockAssertion lock_assertion(mWorkerLock); + if (mWorkerState != oldState) { + if (finalState) *finalState = mWorkerState; + return; + } + mWorkerState = newState; + mWorkerStateChangeRequest = true; + mWorkerCv.wait(lock, [&]() { + android::base::ScopedLockAssertion lock_assertion(mWorkerLock); + return mWorkerState != newState; + }); + if (finalState) *finalState = mWorkerState; + } + void workerThread() { + std::string error = static_cast(this)->workerInit(); + if (error.empty() && !mThreadName.empty()) { + std::string compliantName(mThreadName.substr(0, 15)); + if (int errCode = pthread_setname_np(pthread_self(), compliantName.c_str()); + errCode != 0) { + error.append("Failed to set thread name: ").append(strerror(errCode)); + } + } + if (error.empty() && mThreadPriority != ANDROID_PRIORITY_DEFAULT) { + if (int result = setpriority(PRIO_PROCESS, 0, mThreadPriority); result != 0) { + int errCode = errno; + error.append("Failed to set thread priority: ").append(strerror(errCode)); + } + } + { + std::lock_guard lock(mWorkerLock); + mWorkerState = error.empty() ? WorkerState::RUNNING : WorkerState::STOPPED; + mError = error; + } + mWorkerCv.notify_one(); + if (!error.empty()) return; + + for (WorkerState state = WorkerState::RUNNING; state != WorkerState::STOPPED;) { + bool needToNotify = false; + if (state != WorkerState::PAUSED ? static_cast(this)->workerCycle() + : (sched_yield(), true)) { + { + // See https://developer.android.com/training/articles/smp#nonracing + android::base::ScopedLockAssertion lock_assertion(mWorkerLock); + if (!mWorkerStateChangeRequest.load(std::memory_order_relaxed)) continue; + } + // + // Pause and resume are synchronous. One worker cycle must complete + // before the worker indicates a state change. This is how 'mWorkerState' and + // 'state' interact: + // + // mWorkerState == RUNNING + // client sets mWorkerState := PAUSE_REQUESTED + // last workerCycle gets executed, state := mWorkerState := PAUSED by us + // (or the workers enters the 'error' state if workerCycle fails) + // client gets notified about state change in any case + // thread is doing a busy wait while 'state == PAUSED' + // client sets mWorkerState := RESUME_REQUESTED + // state := mWorkerState (RESUME_REQUESTED) + // mWorkerState := RUNNING, but we don't notify the client yet + // first workerCycle gets executed, the code below triggers a client notification + // (or if workerCycle fails, worker enters 'error' state and also notifies) + // state := mWorkerState (RUNNING) + std::lock_guard lock(mWorkerLock); + if (state == WorkerState::RESUME_REQUESTED) { + needToNotify = true; + } + state = mWorkerState; + if (mWorkerState == WorkerState::PAUSE_REQUESTED) { + state = mWorkerState = WorkerState::PAUSED; + needToNotify = true; + } else if (mWorkerState == WorkerState::RESUME_REQUESTED) { + mWorkerState = WorkerState::RUNNING; + } + } else { + std::lock_guard lock(mWorkerLock); + if (state == WorkerState::RESUME_REQUESTED || + mWorkerState == WorkerState::PAUSE_REQUESTED) { + needToNotify = true; + } + state = mWorkerState = WorkerState::STOPPED; + mError = "workerCycle failed"; + } + if (needToNotify) { + { + std::lock_guard lock(mWorkerLock); + mWorkerStateChangeRequest = false; + } + mWorkerCv.notify_one(); + } + } + } + + std::string mThreadName; + int mThreadPriority = ANDROID_PRIORITY_DEFAULT; + std::thread mWorker; + std::mutex mWorkerLock; + std::condition_variable mWorkerCv; + WorkerState mWorkerState GUARDED_BY(mWorkerLock) = WorkerState::STOPPED; + std::string mError GUARDED_BY(mWorkerLock); + // The atomic lock-free variable is used to prevent priority inversions + // that can occur when a high priority worker tries to acquire the lock + // which has been taken by a lower priority control thread which in its turn + // got preempted. To prevent a PI under normal operating conditions, that is, + // when there are no errors or state changes, the worker does not attempt + // taking `mWorkerLock` unless `mWorkerStateChangeRequest` is set. + // To make sure that updates to `mWorkerState` and `mWorkerStateChangeRequest` + // are serialized, they are always made under a lock. + static_assert(std::atomic::is_always_lock_free); + std::atomic mWorkerStateChangeRequest GUARDED_BY(mWorkerLock) = false; +}; diff --git a/audio/aidl/common/tests/streamworker_tests.cpp b/audio/aidl/common/tests/streamworker_tests.cpp new file mode 100644 index 0000000000..9fb1a8ee3f --- /dev/null +++ b/audio/aidl/common/tests/streamworker_tests.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 +#include +#include + +#include + +#include + +#include +#define LOG_TAG "StreamWorker_Test" +#include + +struct TestStream { + std::atomic error = false; +}; + +class TestWorker : public StreamWorker { + public: + // Use nullptr to test error reporting from the worker thread. + explicit TestWorker(TestStream* stream) : mStream(stream) {} + + size_t getWorkerCycles() const { return mWorkerCycles; } + int getPriority() const { return mPriority; } + bool hasWorkerCycleCalled() const { return mWorkerCycles != 0; } + bool hasNoWorkerCycleCalled(useconds_t usec) { + const size_t cyclesBefore = mWorkerCycles; + usleep(usec); + return mWorkerCycles == cyclesBefore; + } + + std::string workerInit() { return mStream != nullptr ? "" : "Expected error"; } + bool workerCycle() { + mPriority = getpriority(PRIO_PROCESS, 0); + do { + mWorkerCycles++; + } while (mWorkerCycles == 0); + return !mStream->error; + } + + private: + TestStream* const mStream; + std::atomic mWorkerCycles = 0; + std::atomic mPriority = ANDROID_PRIORITY_DEFAULT; +}; + +// The parameter specifies whether an extra call to 'stop' is made at the end. +class StreamWorkerInvalidTest : public testing::TestWithParam { + public: + StreamWorkerInvalidTest() : StreamWorkerInvalidTest(nullptr) {} + void TearDown() override { + if (GetParam()) { + worker.stop(); + } + } + + protected: + StreamWorkerInvalidTest(TestStream* stream) : testing::TestWithParam(), worker(stream) {} + TestWorker worker; +}; + +TEST_P(StreamWorkerInvalidTest, Uninitialized) { + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, UninitializedPauseIgnored) { + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, UninitializedResumeIgnored) { + EXPECT_FALSE(worker.hasError()); + worker.resume(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, Start) { + EXPECT_FALSE(worker.start()); + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, PauseIgnored) { + EXPECT_FALSE(worker.start()); + EXPECT_TRUE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerInvalidTest, ResumeIgnored) { + EXPECT_FALSE(worker.start()); + EXPECT_TRUE(worker.hasError()); + worker.resume(); + EXPECT_TRUE(worker.hasError()); +} + +INSTANTIATE_TEST_SUITE_P(StreamWorkerInvalid, StreamWorkerInvalidTest, testing::Bool()); + +class StreamWorkerTest : public StreamWorkerInvalidTest { + public: + StreamWorkerTest() : StreamWorkerInvalidTest(&stream) {} + + protected: + TestStream stream; +}; + +static constexpr unsigned kWorkerIdleCheckTime = 50 * 1000; + +TEST_P(StreamWorkerTest, Uninitialized) { + EXPECT_FALSE(worker.hasWorkerCycleCalled()); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, Start) { + ASSERT_TRUE(worker.start()); + worker.waitForAtLeastOneCycle(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, WorkerError) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.waitForAtLeastOneCycle(); + EXPECT_TRUE(worker.hasError()); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); +} + +TEST_P(StreamWorkerTest, PauseResume) { + ASSERT_TRUE(worker.start()); + worker.waitForAtLeastOneCycle(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_FALSE(worker.hasError()); + const size_t workerCyclesBefore = worker.getWorkerCycles(); + worker.resume(); + // 'resume' is synchronous and returns after the worker has looped at least once. + EXPECT_GT(worker.getWorkerCycles(), workerCyclesBefore); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, StopPaused) { + ASSERT_TRUE(worker.start()); + worker.waitForAtLeastOneCycle(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + worker.stop(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, PauseAfterErrorIgnored) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.waitForAtLeastOneCycle(); + EXPECT_TRUE(worker.hasError()); + worker.pause(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, ResumeAfterErrorIgnored) { + ASSERT_TRUE(worker.start()); + stream.error = true; + worker.waitForAtLeastOneCycle(); + EXPECT_TRUE(worker.hasError()); + worker.resume(); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); + EXPECT_TRUE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, WorkerErrorOnResume) { + ASSERT_TRUE(worker.start()); + worker.waitForAtLeastOneCycle(); + EXPECT_FALSE(worker.hasError()); + worker.pause(); + EXPECT_FALSE(worker.hasError()); + stream.error = true; + EXPECT_FALSE(worker.hasError()); + worker.resume(); + worker.waitForAtLeastOneCycle(); + EXPECT_TRUE(worker.hasError()); + EXPECT_TRUE(worker.hasNoWorkerCycleCalled(kWorkerIdleCheckTime)); +} + +TEST_P(StreamWorkerTest, WaitForAtLeastOneCycle) { + ASSERT_TRUE(worker.start()); + const size_t workerCyclesBefore = worker.getWorkerCycles(); + EXPECT_TRUE(worker.waitForAtLeastOneCycle()); + EXPECT_GT(worker.getWorkerCycles(), workerCyclesBefore); +} + +TEST_P(StreamWorkerTest, WaitForAtLeastOneCycleError) { + ASSERT_TRUE(worker.start()); + stream.error = true; + EXPECT_FALSE(worker.waitForAtLeastOneCycle()); +} + +TEST_P(StreamWorkerTest, MutexDoesNotBlockWorker) { + ASSERT_TRUE(worker.start()); + const size_t workerCyclesBefore = worker.getWorkerCycles(); + worker.testLockUnlockMutex(true); + while (worker.getWorkerCycles() == workerCyclesBefore) { + usleep(kWorkerIdleCheckTime); + } + worker.testLockUnlockMutex(false); + worker.waitForAtLeastOneCycle(); + EXPECT_FALSE(worker.hasError()); +} + +TEST_P(StreamWorkerTest, ThreadName) { + const std::string workerName = "TestWorker"; + ASSERT_TRUE(worker.start(workerName)) << worker.getError(); + char nameBuf[128]; + ASSERT_EQ(0, pthread_getname_np(worker.testGetThreadNativeHandle(), nameBuf, sizeof(nameBuf))); + EXPECT_EQ(workerName, nameBuf); +} + +TEST_P(StreamWorkerTest, ThreadPriority) { + const int priority = ANDROID_PRIORITY_LOWEST; + ASSERT_TRUE(worker.start("", priority)) << worker.getError(); + worker.waitForAtLeastOneCycle(); + EXPECT_EQ(priority, worker.getPriority()); +} + +INSTANTIATE_TEST_SUITE_P(StreamWorker, StreamWorkerTest, testing::Bool()); diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp index ad1d9c761a..027d92873f 100644 --- a/audio/aidl/default/Android.bp +++ b/audio/aidl/default/Android.bp @@ -16,6 +16,8 @@ cc_library_static { "libstagefright_foundation", "android.media.audio.common.types-V1-ndk", "android.hardware.audio.core-V1-ndk", + "android.hardware.common-V2-ndk", + "android.hardware.common.fmq-V1-ndk", ], export_include_dirs: ["include"], srcs: [ @@ -41,6 +43,8 @@ cc_binary { "libstagefright_foundation", "android.media.audio.common.types-V1-ndk", "android.hardware.audio.core-V1-ndk", + "android.hardware.common-V2-ndk", + "android.hardware.common.fmq-V1-ndk", ], static_libs: [ "libaudioserviceexampleimpl", diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp index 5b4d48a837..1c6f90a021 100644 --- a/audio/aidl/default/Module.cpp +++ b/audio/aidl/default/Module.cpp @@ -27,7 +27,9 @@ using aidl::android::hardware::audio::common::SinkMetadata; using aidl::android::hardware::audio::common::SourceMetadata; +using aidl::android::media::audio::common::AudioChannelLayout; using aidl::android::media::audio::common::AudioFormatDescription; +using aidl::android::media::audio::common::AudioFormatType; using aidl::android::media::audio::common::AudioIoFlags; using aidl::android::media::audio::common::AudioOffloadInfo; using aidl::android::media::audio::common::AudioOutputFlags; @@ -36,6 +38,7 @@ using aidl::android::media::audio::common::AudioPortConfig; using aidl::android::media::audio::common::AudioPortExt; using aidl::android::media::audio::common::AudioProfile; using aidl::android::media::audio::common::Int; +using aidl::android::media::audio::common::PcmType; namespace aidl::android::hardware::audio::core { @@ -69,6 +72,49 @@ bool generateDefaultPortConfig(const AudioPort& port, AudioPortConfig* config) { return true; } +constexpr size_t getPcmSampleSizeInBytes(PcmType pcm) { + switch (pcm) { + case PcmType::UINT_8_BIT: + return 1; + case PcmType::INT_16_BIT: + return 2; + case PcmType::INT_32_BIT: + return 4; + case PcmType::FIXED_Q_8_24: + return 4; + case PcmType::FLOAT_32_BIT: + return 4; + case PcmType::INT_24_BIT: + return 3; + } + return 0; +} + +constexpr size_t getChannelCount(const AudioChannelLayout& layout) { + using Tag = AudioChannelLayout::Tag; + switch (layout.getTag()) { + case Tag::none: + return 0; + case Tag::invalid: + return 0; + case Tag::indexMask: + return __builtin_popcount(layout.get()); + case Tag::layoutMask: + return __builtin_popcount(layout.get()); + case Tag::voiceMask: + return __builtin_popcount(layout.get()); + } + return 0; +} + +size_t getFrameSizeInBytes(const AudioFormatDescription& format, const AudioChannelLayout& layout) { + if (format.type == AudioFormatType::PCM) { + return getPcmSampleSizeInBytes(format.pcm) * getChannelCount(layout); + } + // For non-PCM formats always use frame size of 1. + return 1; +} + bool findAudioProfile(const AudioPort& port, const AudioFormatDescription& format, AudioProfile* profile) { if (auto profilesIt = @@ -111,6 +157,78 @@ void Module::cleanUpPatches(int32_t portConfigId) { erase_all_values(mPatches, erasedPatches); } +ndk::ScopedAStatus Module::createStreamDescriptor(int32_t in_portConfigId, + int64_t in_bufferSizeFrames, + StreamDescriptor* out_descriptor) { + if (in_bufferSizeFrames <= 0) { + LOG(ERROR) << __func__ << ": non-positive buffer size " << in_bufferSizeFrames; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + if (in_bufferSizeFrames < kMinimumStreamBufferSizeFrames) { + LOG(ERROR) << __func__ << ": insufficient buffer size " << in_bufferSizeFrames + << ", must be at least " << kMinimumStreamBufferSizeFrames; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + auto& configs = getConfig().portConfigs; + auto portConfigIt = findById(configs, in_portConfigId); + // Since 'createStreamDescriptor' is an internal method, it is assumed that + // validity of the portConfigId has already been checked. + const size_t frameSize = + getFrameSizeInBytes(portConfigIt->format.value(), portConfigIt->channelMask.value()); + if (frameSize == 0) { + LOG(ERROR) << __func__ << ": could not calculate frame size for port config " + << portConfigIt->toString(); + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + LOG(DEBUG) << __func__ << ": frame size " << frameSize << " bytes"; + if (frameSize > kMaximumStreamBufferSizeBytes / in_bufferSizeFrames) { + LOG(ERROR) << __func__ << ": buffer size " << in_bufferSizeFrames + << " frames is too large, maximum size is " + << kMaximumStreamBufferSizeBytes / frameSize; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + (void)out_descriptor; + return ndk::ScopedAStatus::ok(); +} + +ndk::ScopedAStatus Module::findPortIdForNewStream(int32_t in_portConfigId, AudioPort** port) { + auto& configs = getConfig().portConfigs; + auto portConfigIt = findById(configs, in_portConfigId); + if (portConfigIt == configs.end()) { + LOG(ERROR) << __func__ << ": existing port config id " << in_portConfigId << " not found"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + const int32_t portId = portConfigIt->portId; + // In our implementation, configs of mix ports always have unique IDs. + CHECK(portId != in_portConfigId); + auto& ports = getConfig().ports; + auto portIt = findById(ports, portId); + if (portIt == ports.end()) { + LOG(ERROR) << __func__ << ": port id " << portId << " used by port config id " + << in_portConfigId << " not found"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + if (mStreams.count(in_portConfigId) != 0) { + LOG(ERROR) << __func__ << ": port config id " << in_portConfigId + << " already has a stream opened on it"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); + } + if (portIt->ext.getTag() != AudioPortExt::Tag::mix) { + LOG(ERROR) << __func__ << ": port config id " << in_portConfigId + << " does not correspond to a mix port"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + const int32_t maxOpenStreamCount = portIt->ext.get().maxOpenStreamCount; + if (maxOpenStreamCount != 0 && mStreams.count(portId) >= maxOpenStreamCount) { + LOG(ERROR) << __func__ << ": port id " << portId + << " has already reached maximum allowed opened stream count: " + << maxOpenStreamCount; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); + } + *port = &(*portIt); + return ndk::ScopedAStatus::ok(); +} + internal::Configuration& Module::getConfig() { if (!mConfig) { mConfig.reset(new internal::Configuration(internal::getNullPrimaryConfiguration())); @@ -336,98 +454,59 @@ ndk::ScopedAStatus Module::getAudioRoutesForAudioPort(int32_t in_portId, return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Module::openInputStream(int32_t in_portConfigId, - const SinkMetadata& in_sinkMetadata, - std::shared_ptr* _aidl_return) { - auto& configs = getConfig().portConfigs; - auto portConfigIt = findById(configs, in_portConfigId); - if (portConfigIt == configs.end()) { - LOG(ERROR) << __func__ << ": existing port config id " << in_portConfigId << " not found"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); +ndk::ScopedAStatus Module::openInputStream(const OpenInputStreamArguments& in_args, + OpenInputStreamReturn* _aidl_return) { + LOG(DEBUG) << __func__ << ": port config id " << in_args.portConfigId << ", buffer size " + << in_args.bufferSizeFrames << " frames"; + AudioPort* port = nullptr; + if (auto status = findPortIdForNewStream(in_args.portConfigId, &port); !status.isOk()) { + return status; } - const int32_t portId = portConfigIt->portId; - // In our implementation, configs of mix ports always have unique IDs. - CHECK(portId != in_portConfigId); - auto& ports = getConfig().ports; - auto portIt = findById(ports, portId); - if (portIt == ports.end()) { - LOG(ERROR) << __func__ << ": port id " << portId << " used by port config id " - << in_portConfigId << " not found"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); - } - if (portIt->flags.getTag() != AudioIoFlags::Tag::input || - portIt->ext.getTag() != AudioPortExt::Tag::mix) { - LOG(ERROR) << __func__ << ": port config id " << in_portConfigId + if (port->flags.getTag() != AudioIoFlags::Tag::input) { + LOG(ERROR) << __func__ << ": port config id " << in_args.portConfigId << " does not correspond to an input mix port"; return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); } - if (mStreams.count(in_portConfigId) != 0) { - LOG(ERROR) << __func__ << ": port config id " << in_portConfigId - << " already has a stream opened on it"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); + if (auto status = createStreamDescriptor(in_args.portConfigId, in_args.bufferSizeFrames, + &_aidl_return->desc); + !status.isOk()) { + return status; } - const int32_t maxOpenStreamCount = portIt->ext.get().maxOpenStreamCount; - if (maxOpenStreamCount != 0 && mStreams.count(portId) >= maxOpenStreamCount) { - LOG(ERROR) << __func__ << ": port id " << portId - << " has already reached maximum allowed opened stream count: " - << maxOpenStreamCount; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); - } - auto stream = ndk::SharedRefBase::make(in_sinkMetadata); - mStreams.insert(portId, in_portConfigId, StreamWrapper(stream)); - *_aidl_return = std::move(stream); + auto stream = ndk::SharedRefBase::make(in_args.sinkMetadata); + mStreams.insert(port->id, in_args.portConfigId, StreamWrapper(stream)); + _aidl_return->stream = std::move(stream); return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus Module::openOutputStream(int32_t in_portConfigId, - const SourceMetadata& in_sourceMetadata, - const std::optional& in_offloadInfo, - std::shared_ptr* _aidl_return) { - auto& configs = getConfig().portConfigs; - auto portConfigIt = findById(configs, in_portConfigId); - if (portConfigIt == configs.end()) { - LOG(ERROR) << __func__ << ": existing port config id " << in_portConfigId << " not found"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); +ndk::ScopedAStatus Module::openOutputStream(const OpenOutputStreamArguments& in_args, + OpenOutputStreamReturn* _aidl_return) { + LOG(DEBUG) << __func__ << ": port config id " << in_args.portConfigId << ", has offload info? " + << (in_args.offloadInfo.has_value()) << ", buffer size " << in_args.bufferSizeFrames + << " frames"; + AudioPort* port = nullptr; + if (auto status = findPortIdForNewStream(in_args.portConfigId, &port); !status.isOk()) { + return status; } - const int32_t portId = portConfigIt->portId; - // In our implementation, configs of mix ports always have unique IDs. - CHECK(portId != in_portConfigId); - auto& ports = getConfig().ports; - auto portIt = findById(ports, portId); - if (portIt == ports.end()) { - LOG(ERROR) << __func__ << ": port id " << portId << " used by port config id " - << in_portConfigId << " not found"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); - } - if (portIt->flags.getTag() != AudioIoFlags::Tag::output || - portIt->ext.getTag() != AudioPortExt::Tag::mix) { - LOG(ERROR) << __func__ << ": port config id " << in_portConfigId + if (port->flags.getTag() != AudioIoFlags::Tag::output) { + LOG(ERROR) << __func__ << ": port config id " << in_args.portConfigId << " does not correspond to an output mix port"; return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); } - if (portConfigIt->flags.has_value() && - ((portConfigIt->flags.value().get() & - 1 << static_cast(AudioOutputFlags::COMPRESS_OFFLOAD)) != 0) && - !in_offloadInfo.has_value()) { - LOG(ERROR) << __func__ << ": port config id " << in_portConfigId + if ((port->flags.get() & + 1 << static_cast(AudioOutputFlags::COMPRESS_OFFLOAD)) != 0 && + !in_args.offloadInfo.has_value()) { + LOG(ERROR) << __func__ << ": port id " << port->id << " has COMPRESS_OFFLOAD flag set, requires offload info"; return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); } - if (mStreams.count(in_portConfigId) != 0) { - LOG(ERROR) << __func__ << ": port config id " << in_portConfigId - << " already has a stream opened on it"; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); + if (auto status = createStreamDescriptor(in_args.portConfigId, in_args.bufferSizeFrames, + &_aidl_return->desc); + !status.isOk()) { + return status; } - const int32_t maxOpenStreamCount = portIt->ext.get().maxOpenStreamCount; - if (maxOpenStreamCount != 0 && mStreams.count(portId) >= maxOpenStreamCount) { - LOG(ERROR) << __func__ << ": port id " << portId - << " has already reached maximum allowed opened stream count: " - << maxOpenStreamCount; - return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); - } - auto stream = ndk::SharedRefBase::make(in_sourceMetadata, in_offloadInfo); - mStreams.insert(portId, in_portConfigId, StreamWrapper(stream)); - *_aidl_return = std::move(stream); + auto stream = ndk::SharedRefBase::make(in_args.sourceMetadata, in_args.offloadInfo); + mStreams.insert(port->id, in_args.portConfigId, StreamWrapper(stream)); + _aidl_return->stream = std::move(stream); return ndk::ScopedAStatus::ok(); } @@ -512,6 +591,10 @@ ndk::ScopedAStatus Module::setAudioPatch(const AudioPatch& in_requested, AudioPa } } *_aidl_return = in_requested; + _aidl_return->minimumStreamBufferSizeFrames = kMinimumStreamBufferSizeFrames; + _aidl_return->latenciesMs.clear(); + _aidl_return->latenciesMs.insert(_aidl_return->latenciesMs.end(), + _aidl_return->sinkPortConfigIds.size(), kLatencyMs); if (existing == patches.end()) { _aidl_return->id = getConfig().nextPatchId++; patches.push_back(*_aidl_return); diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp index e16b2c62b9..ab3e451364 100644 --- a/audio/aidl/default/Stream.cpp +++ b/audio/aidl/default/Stream.cpp @@ -15,7 +15,6 @@ */ #define LOG_TAG "AHAL_Stream" -#define LOG_NDEBUG 0 #include #include "core-impl/Stream.h" @@ -26,7 +25,9 @@ using aidl::android::media::audio::common::AudioOffloadInfo; namespace aidl::android::hardware::audio::core { -StreamIn::StreamIn(const SinkMetadata& sinkMetadata) : mMetadata(sinkMetadata) {} +StreamIn::StreamIn(const SinkMetadata& sinkMetadata) : mMetadata(sinkMetadata) { + LOG(DEBUG) << __func__; +} ndk::ScopedAStatus StreamIn::close() { LOG(DEBUG) << __func__; @@ -51,7 +52,9 @@ ndk::ScopedAStatus StreamIn::updateMetadata(const SinkMetadata& in_sinkMetadata) StreamOut::StreamOut(const SourceMetadata& sourceMetadata, const std::optional& offloadInfo) - : mMetadata(sourceMetadata), mOffloadInfo(offloadInfo) {} + : mMetadata(sourceMetadata), mOffloadInfo(offloadInfo) { + LOG(DEBUG) << __func__; +} ndk::ScopedAStatus StreamOut::close() { LOG(DEBUG) << __func__; diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h index 81a02baabb..f7e14affce 100644 --- a/audio/aidl/default/include/core-impl/Module.h +++ b/audio/aidl/default/include/core-impl/Module.h @@ -48,15 +48,15 @@ class Module : public BnModule { int32_t in_portId, std::vector<::aidl::android::hardware::audio::core::AudioRoute>* _aidl_return) override; ndk::ScopedAStatus openInputStream( - int32_t in_portConfigId, - const ::aidl::android::hardware::audio::common::SinkMetadata& in_sinkMetadata, - std::shared_ptr* _aidl_return) override; + const ::aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments& + in_args, + ::aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn* _aidl_return) + override; ndk::ScopedAStatus openOutputStream( - int32_t in_portConfigId, - const ::aidl::android::hardware::audio::common::SourceMetadata& in_sourceMetadata, - const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>& - in_offloadInfo, - std::shared_ptr* _aidl_return) override; + const ::aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments& + in_args, + ::aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn* _aidl_return) + override; ndk::ScopedAStatus setAudioPatch(const AudioPatch& in_requested, AudioPatch* _aidl_return) override; ndk::ScopedAStatus setAudioPortConfig( @@ -69,9 +69,21 @@ class Module : public BnModule { private: void cleanUpPatch(int32_t patchId); void cleanUpPatches(int32_t portConfigId); + ndk::ScopedAStatus createStreamDescriptor( + int32_t in_portConfigId, int64_t in_bufferSizeFrames, + ::aidl::android::hardware::audio::core::StreamDescriptor* out_descriptor); + ndk::ScopedAStatus findPortIdForNewStream( + int32_t in_portConfigId, ::aidl::android::media::audio::common::AudioPort** port); internal::Configuration& getConfig(); void registerPatch(const AudioPatch& patch); + // This value is used for all AudioPatches. + static constexpr int32_t kMinimumStreamBufferSizeFrames = 16; + // This value is used for all AudioPatches. + static constexpr int32_t kLatencyMs = 10; + // The maximum stream buffer size is 1 GiB = 2 ** 30 bytes; + static constexpr int32_t kMaximumStreamBufferSizeBytes = 1 << 30; + std::unique_ptr mConfig; ModuleDebug mDebug; // ids of ports created at runtime via 'connectExternalDevice'. diff --git a/audio/aidl/vts/Android.bp b/audio/aidl/vts/Android.bp index cd5915bbe2..75ff37f088 100644 --- a/audio/aidl/vts/Android.bp +++ b/audio/aidl/vts/Android.bp @@ -23,6 +23,8 @@ cc_test { static_libs: [ "android.hardware.audio.common-V1-ndk", "android.hardware.audio.core-V1-ndk", + "android.hardware.common-V2-ndk", + "android.hardware.common.fmq-V1-ndk", "android.media.audio.common.types-V1-ndk", ], test_suites: [ diff --git a/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp index bb24365741..0ecc057c35 100644 --- a/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp +++ b/audio/aidl/vts/VtsHalAudioCoreTargetTest.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -48,6 +49,7 @@ using aidl::android::hardware::audio::core::IModule; using aidl::android::hardware::audio::core::IStreamIn; using aidl::android::hardware::audio::core::IStreamOut; using aidl::android::hardware::audio::core::ModuleDebug; +using aidl::android::hardware::audio::core::StreamDescriptor; using aidl::android::media::audio::common::AudioContentType; using aidl::android::media::audio::common::AudioDevice; using aidl::android::media::audio::common::AudioDeviceAddress; @@ -225,6 +227,9 @@ class WithAudioPortConfig { class AudioCoreModule : public testing::TestWithParam { public: + // The default buffer size is used mostly for negative tests. + static constexpr int kDefaultBufferSize = 256; + void SetUp() override { ASSERT_NO_FATAL_FAILURE(ConnectToService()); debug.flags().simulateDeviceConnections = true; @@ -382,13 +387,14 @@ class WithStream { } } void SetUpPortConfig(IModule* module) { ASSERT_NO_FATAL_FAILURE(mPortConfig.SetUp(module)); } - ScopedAStatus SetUpNoChecks(IModule* module) { - return SetUpNoChecks(module, mPortConfig.get()); + ScopedAStatus SetUpNoChecks(IModule* module, long bufferSize) { + return SetUpNoChecks(module, mPortConfig.get(), bufferSize); } - ScopedAStatus SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig); - void SetUp(IModule* module) { + ScopedAStatus SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig, + long bufferSize); + void SetUp(IModule* module, long bufferSize) { ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module)); - ScopedAStatus status = SetUpNoChecks(module); + ScopedAStatus status = SetUpNoChecks(module, bufferSize); ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status << "; port config id " << getPortId(); ASSERT_NE(nullptr, mStream) << "; port config id " << getPortId(); @@ -401,6 +407,7 @@ class WithStream { private: WithAudioPortConfig mPortConfig; std::shared_ptr mStream; + StreamDescriptor mDescriptor; }; SinkMetadata GenerateSinkMetadata(const AudioPortConfig& portConfig) { @@ -415,8 +422,19 @@ SinkMetadata GenerateSinkMetadata(const AudioPortConfig& portConfig) { template <> ScopedAStatus WithStream::SetUpNoChecks(IModule* module, - const AudioPortConfig& portConfig) { - return module->openInputStream(portConfig.id, GenerateSinkMetadata(portConfig), &mStream); + const AudioPortConfig& portConfig, + long bufferSize) { + aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments args; + args.portConfigId = portConfig.id; + args.sinkMetadata = GenerateSinkMetadata(portConfig); + args.bufferSizeFrames = bufferSize; + aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn ret; + ScopedAStatus status = module->openInputStream(args, &ret); + if (status.isOk()) { + mStream = std::move(ret.stream); + mDescriptor = std::move(ret.desc); + } + return status; } SourceMetadata GenerateSourceMetadata(const AudioPortConfig& portConfig) { @@ -432,10 +450,20 @@ SourceMetadata GenerateSourceMetadata(const AudioPortConfig& portConfig) { template <> ScopedAStatus WithStream::SetUpNoChecks(IModule* module, - const AudioPortConfig& portConfig) { - return module->openOutputStream(portConfig.id, GenerateSourceMetadata(portConfig), - ModuleConfig::generateOffloadInfoIfNeeded(portConfig), - &mStream); + const AudioPortConfig& portConfig, + long bufferSize) { + aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; + args.portConfigId = portConfig.id; + args.sourceMetadata = GenerateSourceMetadata(portConfig); + args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig); + args.bufferSizeFrames = bufferSize; + aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; + ScopedAStatus status = module->openOutputStream(args, &ret); + if (status.isOk()) { + mStream = std::move(ret.stream); + mDescriptor = std::move(ret.desc); + } + return status; } class WithAudioPatch { @@ -467,6 +495,10 @@ class WithAudioPatch { ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status << "; source port config id " << mSrcPortConfig.getId() << "; sink port config id " << mSinkPortConfig.getId(); + EXPECT_GT(mPatch.minimumStreamBufferSizeFrames, 0) << "patch id " << getId(); + for (auto latencyMs : mPatch.latenciesMs) { + EXPECT_GT(latencyMs, 0) << "patch id " << getId(); + } } int32_t getId() const { return mPatch.id; } const AudioPatch& get() const { return mPatch; } @@ -739,18 +771,24 @@ TEST_P(AudioCoreModule, OpenStreamInvalidPortConfigId) { ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); for (const auto portConfigId : GetNonExistentIds(portConfigIds)) { { - std::shared_ptr stream; - ScopedAStatus status = module->openInputStream(portConfigId, {}, &stream); + aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments args; + args.portConfigId = portConfigId; + args.bufferSizeFrames = kDefaultBufferSize; + aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn ret; + ScopedAStatus status = module->openInputStream(args, &ret); EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) << status << " openInputStream returned for port config ID " << portConfigId; - EXPECT_EQ(nullptr, stream); + EXPECT_EQ(nullptr, ret.stream); } { - std::shared_ptr stream; - ScopedAStatus status = module->openOutputStream(portConfigId, {}, {}, &stream); + aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; + args.portConfigId = portConfigId; + args.bufferSizeFrames = kDefaultBufferSize; + aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; + ScopedAStatus status = module->openOutputStream(args, &ret); EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) << status << " openOutputStream returned for port config ID " << portConfigId; - EXPECT_EQ(nullptr, stream); + EXPECT_EQ(nullptr, ret.stream); } } } @@ -1120,7 +1158,7 @@ class AudioStream : public AudioCoreModule { std::shared_ptr heldStream; { WithStream stream(portConfig.value()); - ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSize)); heldStream = stream.getSharedPointer(); } ScopedAStatus status = heldStream->close(); @@ -1132,10 +1170,43 @@ class AudioStream : public AudioCoreModule { const auto allPortConfigs = moduleConfig->getPortConfigsForMixPorts(IsInput()); for (const auto& portConfig : allPortConfigs) { WithStream stream(portConfig); - ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSize)); } } + void OpenInvalidBufferSize() { + const auto portConfig = moduleConfig->getSingleConfigForMixPort(IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + WithStream stream(portConfig.value()); + ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); + // The buffer size of 1 frame should be impractically small, and thus + // less than any minimum buffer size suggested by any HAL. + for (long bufferSize : std::array{-1, 0, 1, std::numeric_limits::max()}) { + ScopedAStatus status = stream.SetUpNoChecks(module.get(), bufferSize); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) + << status << " open" << direction(true) << "Stream returned for " << bufferSize + << " buffer size"; + EXPECT_EQ(nullptr, stream.get()); + } + } + + void OpenInvalidDirection() { + // Important! The direction of the port config must be reversed. + const auto portConfig = moduleConfig->getSingleConfigForMixPort(!IsInput()); + if (!portConfig.has_value()) { + GTEST_SKIP() << "No mix port for attached devices"; + } + WithStream stream(portConfig.value()); + ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); + ScopedAStatus status = stream.SetUpNoChecks(module.get(), kDefaultBufferSize); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) + << status << " open" << direction(true) << "Stream returned for port config ID " + << stream.getPortId(); + EXPECT_EQ(nullptr, stream.get()); + } + void OpenOverMaxCount() { constexpr bool isInput = IsInput(); auto ports = moduleConfig->getMixPorts(isInput); @@ -1158,10 +1229,10 @@ class AudioStream : public AudioCoreModule { streamWraps[i].emplace(portConfigs[i]); WithStream& stream = streamWraps[i].value(); if (i < maxStreamCount) { - ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSize)); } else { ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); - ScopedAStatus status = stream.SetUpNoChecks(module.get()); + ScopedAStatus status = stream.SetUpNoChecks(module.get(), kDefaultBufferSize); EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) << status << " open" << direction(true) << "Stream returned for port config ID " << stream.getPortId() @@ -1175,21 +1246,6 @@ class AudioStream : public AudioCoreModule { } } - void OpenInvalidDirection() { - // Important! The direction of the port config must be reversed. - const auto portConfig = moduleConfig->getSingleConfigForMixPort(!IsInput()); - if (!portConfig.has_value()) { - GTEST_SKIP() << "No mix port for attached devices"; - } - WithStream stream(portConfig.value()); - ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); - ScopedAStatus status = stream.SetUpNoChecks(module.get()); - EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) - << status << " open" << direction(true) << "Stream returned for port config ID " - << stream.getPortId(); - EXPECT_EQ(nullptr, stream.get()); - } - void OpenTwiceSamePortConfig() { const auto portConfig = moduleConfig->getSingleConfigForMixPort(IsInput()); if (!portConfig.has_value()) { @@ -1204,7 +1260,7 @@ class AudioStream : public AudioCoreModule { GTEST_SKIP() << "No mix port for attached devices"; } WithStream stream(portConfig.value()); - ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get())); + ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSize)); ScopedAStatus status = module->resetAudioPortConfig(stream.getPortId()); EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) << status << " returned for port config ID " << stream.getPortId(); @@ -1212,9 +1268,10 @@ class AudioStream : public AudioCoreModule { void OpenTwiceSamePortConfigImpl(const AudioPortConfig& portConfig) { WithStream stream1(portConfig); - ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get())); + ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get(), kDefaultBufferSize)); WithStream stream2; - ScopedAStatus status = stream2.SetUpNoChecks(module.get(), stream1.getPortConfig()); + ScopedAStatus status = + stream2.SetUpNoChecks(module.get(), stream1.getPortConfig(), kDefaultBufferSize); EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) << status << " when opening " << direction(false) << " stream twice for the same port config ID " << stream1.getPortId(); @@ -1238,6 +1295,7 @@ std::string AudioStreamOut::direction(bool capitalize) { TEST_IO_STREAM(CloseTwice); TEST_IO_STREAM(OpenAllConfigs); +TEST_IO_STREAM(OpenInvalidBufferSize); TEST_IO_STREAM(OpenInvalidDirection); TEST_IO_STREAM(OpenOverMaxCount); TEST_IO_STREAM(OpenTwiceSamePortConfig); @@ -1277,10 +1335,14 @@ TEST_P(AudioStreamOut, RequireOffloadInfo) { const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, *offloadPortIt); ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for the compressed offload mix port"; + StreamDescriptor descriptor; std::shared_ptr ignored; - ScopedAStatus status = module->openOutputStream(portConfig.value().id, - GenerateSourceMetadata(portConfig.value()), - {} /* offloadInfo */, &ignored); + aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; + args.portConfigId = portConfig.value().id; + args.sourceMetadata = GenerateSourceMetadata(portConfig.value()); + args.bufferSizeFrames = kDefaultBufferSize; + aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; + ScopedAStatus status = module->openOutputStream(args, &ret); EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) << status << " returned when no offload info is provided for a compressed offload mix port";