audio: Add non-blocking I/O stream operations
Introduce IStreamCallback interface for non-blocking I/O. Add 2 new stream states: TRANSFERRING and TRANSFER_PAUSED, and define state machines for the async case. Add DrainMode as in previous HAL versions. Note that non-blocking input is not fully implemented because it did not exist in previous HAL versions, and the corresponding AudioInputFlag does not exist yet. Enhance VTS state machine tests to allow waiting for an async event. Bug: 205884982 Test: atest VtsHalAudioCoreTargetTest Change-Id: I0a18a6d930dee5941f769e08083817d41ff941e6
This commit is contained in:
parent
e467e01379
commit
30301a42c7
17 changed files with 1059 additions and 247 deletions
|
@ -96,6 +96,7 @@ aidl_interface {
|
|||
"android/hardware/audio/core/AudioRoute.aidl",
|
||||
"android/hardware/audio/core/IConfig.aidl",
|
||||
"android/hardware/audio/core/IModule.aidl",
|
||||
"android/hardware/audio/core/IStreamCallback.aidl",
|
||||
"android/hardware/audio/core/IStreamIn.aidl",
|
||||
"android/hardware/audio/core/IStreamOut.aidl",
|
||||
"android/hardware/audio/core/ITelephony.aidl",
|
||||
|
|
|
@ -76,6 +76,7 @@ interface IModule {
|
|||
android.hardware.audio.common.SourceMetadata sourceMetadata;
|
||||
@nullable android.media.audio.common.AudioOffloadInfo offloadInfo;
|
||||
long bufferSizeFrames;
|
||||
@nullable android.hardware.audio.core.IStreamCallback callback;
|
||||
}
|
||||
@VintfStability
|
||||
parcelable OpenOutputStreamReturn {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 <name>-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;
|
||||
@VintfStability
|
||||
interface IStreamCallback {
|
||||
oneway void onTransferReady();
|
||||
oneway void onError();
|
||||
oneway void onDrainReady();
|
||||
}
|
|
@ -54,15 +54,23 @@ parcelable StreamDescriptor {
|
|||
PAUSED = 4,
|
||||
DRAINING = 5,
|
||||
DRAIN_PAUSED = 6,
|
||||
TRANSFERRING = 7,
|
||||
TRANSFER_PAUSED = 8,
|
||||
ERROR = 100,
|
||||
}
|
||||
@Backing(type="byte") @VintfStability
|
||||
enum DrainMode {
|
||||
DRAIN_UNSPECIFIED = 0,
|
||||
DRAIN_ALL = 1,
|
||||
DRAIN_EARLY_NOTIFY = 2,
|
||||
}
|
||||
@FixedSize @VintfStability
|
||||
union Command {
|
||||
int halReservedExit;
|
||||
android.media.audio.common.Void getStatus;
|
||||
android.media.audio.common.Void start;
|
||||
int burst;
|
||||
android.media.audio.common.Void drain;
|
||||
android.hardware.audio.core.StreamDescriptor.DrainMode drain;
|
||||
android.media.audio.common.Void standby;
|
||||
android.media.audio.common.Void pause;
|
||||
android.media.audio.common.Void flush;
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.hardware.audio.common.SourceMetadata;
|
|||
import android.hardware.audio.core.AudioMode;
|
||||
import android.hardware.audio.core.AudioPatch;
|
||||
import android.hardware.audio.core.AudioRoute;
|
||||
import android.hardware.audio.core.IStreamCallback;
|
||||
import android.hardware.audio.core.IStreamIn;
|
||||
import android.hardware.audio.core.IStreamOut;
|
||||
import android.hardware.audio.core.ITelephony;
|
||||
|
@ -320,9 +321,13 @@ interface IModule {
|
|||
* 'setAudioPortConfig' method. Existence of an audio patch involving this
|
||||
* port configuration is not required for successful opening of a stream.
|
||||
*
|
||||
* If the port configuration has 'COMPRESS_OFFLOAD' output flag set,
|
||||
* the framework must provide additional information about the encoded
|
||||
* audio stream in 'offloadInfo' argument.
|
||||
* If the port configuration has the 'COMPRESS_OFFLOAD' output flag set,
|
||||
* the client must provide additional information about the encoded
|
||||
* audio stream in the 'offloadInfo' argument.
|
||||
*
|
||||
* If the port configuration has the 'NON_BLOCKING' output flag set,
|
||||
* the client must provide a callback for asynchronous notifications
|
||||
* in the 'callback' 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
|
||||
|
@ -358,6 +363,8 @@ interface IModule {
|
|||
* - If the offload info is not provided for an offload
|
||||
* port configuration.
|
||||
* - If a buffer of the requested size can not be provided.
|
||||
* - If the callback is not provided for a non-blocking
|
||||
* port configuration.
|
||||
* @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
|
||||
|
@ -376,6 +383,8 @@ interface IModule {
|
|||
@nullable AudioOffloadInfo offloadInfo;
|
||||
/** Requested audio I/O buffer minimum size, in frames. */
|
||||
long bufferSizeFrames;
|
||||
/** Client callback interface for the non-blocking output mode. */
|
||||
@nullable IStreamCallback callback;
|
||||
}
|
||||
@VintfStability
|
||||
parcelable OpenOutputStreamReturn {
|
||||
|
|
41
audio/aidl/android/hardware/audio/core/IStreamCallback.aidl
Normal file
41
audio/aidl/android/hardware/audio/core/IStreamCallback.aidl
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
package android.hardware.audio.core;
|
||||
|
||||
/**
|
||||
* This interface is used to indicate completion of asynchronous operations.
|
||||
* See the state machines referenced by StreamDescriptor for details.
|
||||
*/
|
||||
@VintfStability
|
||||
oneway interface IStreamCallback {
|
||||
/**
|
||||
* Indicate that the stream is ready for next data exchange.
|
||||
*/
|
||||
void onTransferReady();
|
||||
/**
|
||||
* Indicate that an irrecoverable error has occurred during the last I/O
|
||||
* operation. After sending this callback, the stream enters the 'ERROR'
|
||||
* state.
|
||||
*/
|
||||
void onError();
|
||||
/**
|
||||
* Indicate that the stream has finished draining. This is only used
|
||||
* for output streams because for input streams draining is performed
|
||||
* by the client.
|
||||
*/
|
||||
void onDrainReady();
|
||||
}
|
|
@ -42,6 +42,12 @@ parcelable ModuleDebug {
|
|||
* is to allow VTS to test sending of stream commands while the stream is
|
||||
* in a transient state. The delay must apply to newly created streams,
|
||||
* it is not required to apply the delay to already opened streams.
|
||||
*
|
||||
* Note: the drawback of enabling this delay for asynchronous (non-blocking)
|
||||
* modes is that sending of callbacks will also be delayed, because
|
||||
* callbacks are sent once the stream state machine exits a transient
|
||||
* state. Thus, it's not recommended to use it with tests that require
|
||||
* waiting for an async callback.
|
||||
*/
|
||||
int streamTransientStateDelayMs;
|
||||
}
|
||||
|
|
|
@ -100,6 +100,28 @@ import android.media.audio.common.Void;
|
|||
* client can never observe a stream with a functioning command queue in this
|
||||
* state. The 'ERROR' state is a special state which the state machine enters
|
||||
* when an unrecoverable hardware error is detected by the HAL module.
|
||||
*
|
||||
* Non-blocking (asynchronous) modes introduce a new 'TRANSFERRING' state, which
|
||||
* the state machine can enter after replying to the 'burst' command, instead of
|
||||
* staying in the 'ACTIVE' state. In this case the client gets unblocked
|
||||
* earlier, while the actual audio delivery to / from the observer is not
|
||||
* complete yet. Once the HAL module is ready for the next transfer, it notifies
|
||||
* the client via a oneway callback, and the machine switches to 'ACTIVE'
|
||||
* state. The 'TRANSFERRING' state is thus "transient", similar to the
|
||||
* 'DRAINING' state. For output streams, asynchronous transfer can be paused,
|
||||
* and it's another new state: 'TRANSFER_PAUSED'. It differs from 'PAUSED' by
|
||||
* the fact that no new writes are allowed. Please see 'stream-in-async-sm.gv'
|
||||
* and 'stream-out-async-sm.gv' files for details. Below is the table summary
|
||||
* for asynchronous only-states:
|
||||
*
|
||||
* Producer | Buffer state | Consumer | Applies | State
|
||||
* active? | | active? | to |
|
||||
* ==========|==============|==========|=========|==============================
|
||||
* Yes | Not empty | Yes | Both | TRANSFERRING, s/w x-runs counted
|
||||
* ----------|--------------|----------|---------|-----------------------------
|
||||
* Yes | Not empty | No | Output | TRANSFER_PAUSED,
|
||||
* | | | | h/w emits silence.
|
||||
*
|
||||
*/
|
||||
@JavaDerive(equals=true, toString=true)
|
||||
@VintfStability
|
||||
|
@ -171,10 +193,23 @@ parcelable StreamDescriptor {
|
|||
/**
|
||||
* Used for output streams only, pauses draining. This state is similar
|
||||
* to the 'PAUSED' state, except that the client is not adding any
|
||||
* new data. If it emits a 'BURST' command, this brings the stream
|
||||
* new data. If it emits a 'burst' command, this brings the stream
|
||||
* into the regular 'PAUSED' state.
|
||||
*/
|
||||
DRAIN_PAUSED = 6,
|
||||
/**
|
||||
* Used only for streams in asynchronous mode. The stream enters this
|
||||
* state after receiving a 'burst' command and returning control back
|
||||
* to the client, thus unblocking it.
|
||||
*/
|
||||
TRANSFERRING = 7,
|
||||
/**
|
||||
* Used only for output streams in asynchronous mode only. The stream
|
||||
* enters this state after receiving a 'pause' command while being in
|
||||
* the 'TRANSFERRING' state. Unlike 'PAUSED' state, this state does not
|
||||
* accept new writes.
|
||||
*/
|
||||
TRANSFER_PAUSED = 8,
|
||||
/**
|
||||
* The ERROR state is entered when the stream has encountered an
|
||||
* irrecoverable error from the lower layer. After entering it, the
|
||||
|
@ -183,6 +218,29 @@ parcelable StreamDescriptor {
|
|||
ERROR = 100,
|
||||
}
|
||||
|
||||
@VintfStability
|
||||
@Backing(type="byte")
|
||||
enum DrainMode {
|
||||
/**
|
||||
* Unspecified—used with input streams only, because the client controls
|
||||
* draining.
|
||||
*/
|
||||
DRAIN_UNSPECIFIED = 0,
|
||||
/**
|
||||
* Used with output streams only, the HAL module indicates drain
|
||||
* completion when all remaining audio data has been consumed.
|
||||
*/
|
||||
DRAIN_ALL = 1,
|
||||
/**
|
||||
* Used with output streams only, the HAL module indicates drain
|
||||
* completion shortly before all audio data has been consumed in order
|
||||
* to give the client an opportunity to provide data for the next track
|
||||
* for gapless playback. The exact amount of provided time is specific
|
||||
* to the HAL implementation.
|
||||
*/
|
||||
DRAIN_EARLY_NOTIFY = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -253,7 +311,7 @@ parcelable StreamDescriptor {
|
|||
* See the state machines on the applicability of this command to
|
||||
* different states.
|
||||
*/
|
||||
Void drain;
|
||||
DrainMode drain;
|
||||
/**
|
||||
* See the state machines on the applicability of this command to
|
||||
* different states.
|
||||
|
@ -413,6 +471,10 @@ parcelable StreamDescriptor {
|
|||
* into 'reply' queue, and hangs on waiting on a read from
|
||||
* the 'command' queue.
|
||||
* 6. The client wakes up due to 5. and reads the reply.
|
||||
* Note: in non-blocking mode, when the HAL module goes to
|
||||
* the 'TRANSFERRING' state (as indicated by the 'reply.state'
|
||||
* field), the client must wait for the 'IStreamCallback.onTransferReady'
|
||||
* notification to arrive before starting the next burst.
|
||||
*
|
||||
* For input streams the following sequence of operations is used:
|
||||
* 1. The client writes the BURST command into the 'command' queue,
|
||||
|
@ -427,6 +489,10 @@ parcelable StreamDescriptor {
|
|||
* 5. The client wakes up due to 4.
|
||||
* 6. The client reads the reply and audio data. The client must
|
||||
* always read from the FMQ all the data it contains.
|
||||
* Note: in non-blocking mode, when the HAL module goes to
|
||||
* the 'TRANSFERRING' state (as indicated by the 'reply.state'
|
||||
* field) the client must wait for the 'IStreamCallback.onTransferReady'
|
||||
* notification to arrive before starting the next burst.
|
||||
*
|
||||
*/
|
||||
MQDescriptor<byte, SynchronizedReadWrite> fmq;
|
||||
|
|
47
audio/aidl/android/hardware/audio/core/stream-in-async-sm.gv
Normal file
47
audio/aidl/android/hardware/audio/core/stream-in-async-sm.gv
Normal file
|
@ -0,0 +1,47 @@
|
|||
// 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.
|
||||
|
||||
// To render: dot -Tpng stream-in-async-sm.gv -o stream-in-async-sm.png
|
||||
digraph stream_in_async_state_machine {
|
||||
node [shape=doublecircle style=filled fillcolor=black width=0.5] I;
|
||||
node [shape=point width=0.5] F;
|
||||
node [shape=oval width=1];
|
||||
node [fillcolor=lightgreen] STANDBY; // buffer is empty
|
||||
node [fillcolor=tomato] CLOSED;
|
||||
node [fillcolor=tomato] ERROR;
|
||||
node [style=dashed] ANY_STATE;
|
||||
node [fillcolor=lightblue style=filled];
|
||||
// Note that when the producer (h/w) is passive, "burst" operations
|
||||
// complete synchronously, bypassing the TRANSFERRING state.
|
||||
I -> STANDBY;
|
||||
STANDBY -> IDLE [label="start"]; // producer -> active
|
||||
IDLE -> STANDBY [label="standby"]; // producer -> passive, buffer is cleared
|
||||
IDLE -> TRANSFERRING [label="burst"]; // consumer -> active
|
||||
ACTIVE -> PAUSED [label="pause"]; // consumer -> passive
|
||||
ACTIVE -> DRAINING [label="drain"]; // producer -> passive
|
||||
ACTIVE -> TRANSFERRING [label="burst"];
|
||||
TRANSFERRING -> ACTIVE [label="←IStreamCallback.onTransferReady"];
|
||||
TRANSFERRING -> PAUSED [label="pause"]; // consumer -> passive
|
||||
TRANSFERRING -> DRAINING [label="drain"]; // producer -> passive
|
||||
PAUSED -> TRANSFERRING [label="burst"]; // consumer -> active
|
||||
PAUSED -> STANDBY [label="flush"]; // producer -> passive, buffer is cleared
|
||||
DRAINING -> DRAINING [label="burst"];
|
||||
DRAINING -> ACTIVE [label="start"]; // producer -> active
|
||||
DRAINING -> STANDBY [label="<empty buffer>"]; // consumer deactivates
|
||||
IDLE -> ERROR [label="←IStreamCallback.onError"];
|
||||
PAUSED -> ERROR [label="←IStreamCallback.onError"];
|
||||
TRANSFERRING -> ERROR [label="←IStreamCallback.onError"];
|
||||
ANY_STATE -> CLOSED [label="→IStream*.close"];
|
||||
CLOSED -> F;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// 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.
|
||||
|
||||
// To render: dot -Tpng stream-out-async-sm.gv -o stream-out-async-sm.png
|
||||
digraph stream_out_async_state_machine {
|
||||
node [shape=doublecircle style=filled fillcolor=black width=0.5] I;
|
||||
node [shape=point width=0.5] F;
|
||||
node [shape=oval width=1];
|
||||
node [fillcolor=lightgreen] STANDBY; // buffer is empty
|
||||
node [fillcolor=lightgreen] IDLE; // buffer is empty
|
||||
node [fillcolor=tomato] CLOSED;
|
||||
node [fillcolor=tomato] ERROR;
|
||||
node [style=dashed] ANY_STATE;
|
||||
node [fillcolor=lightblue style=filled];
|
||||
// Note that when the consumer (h/w) is passive, "burst" operations
|
||||
// complete synchronously, bypassing the TRANSFERRING state.
|
||||
I -> STANDBY;
|
||||
STANDBY -> IDLE [label="start"]; // consumer -> active
|
||||
STANDBY -> PAUSED [label="burst"]; // producer -> active
|
||||
IDLE -> STANDBY [label="standby"]; // consumer -> passive
|
||||
IDLE -> TRANSFERRING [label="burst"]; // producer -> active
|
||||
ACTIVE -> PAUSED [label="pause"]; // consumer -> passive (not consuming)
|
||||
ACTIVE -> DRAINING [label="drain"]; // producer -> passive
|
||||
ACTIVE -> TRANSFERRING [label="burst"]; // early unblocking
|
||||
ACTIVE -> ACTIVE [label="burst"]; // full write
|
||||
TRANSFERRING -> ACTIVE [label="←IStreamCallback.onTransferReady"];
|
||||
TRANSFERRING -> TRANSFER_PAUSED [label="pause"]; // consumer -> passive (not consuming)
|
||||
TRANSFERRING -> DRAINING [label="drain"]; // producer -> passive
|
||||
TRANSFER_PAUSED -> TRANSFERRING [label="start"]; // consumer -> active
|
||||
TRANSFER_PAUSED -> DRAIN_PAUSED [label="drain"]; // producer -> passive
|
||||
TRANSFER_PAUSED -> IDLE [label="flush"]; // buffer is cleared
|
||||
PAUSED -> PAUSED [label="burst"];
|
||||
PAUSED -> ACTIVE [label="start"]; // consumer -> active
|
||||
PAUSED -> IDLE [label="flush"]; // producer -> passive, buffer is cleared
|
||||
DRAINING -> IDLE [label="←IStreamCallback.onDrainReady"];
|
||||
DRAINING -> TRANSFERRING [label="burst"]; // producer -> active
|
||||
DRAINING -> DRAIN_PAUSED [label="pause"]; // consumer -> passive (not consuming)
|
||||
DRAIN_PAUSED -> DRAINING [label="start"]; // consumer -> active
|
||||
DRAIN_PAUSED -> TRANSFER_PAUSED [label="burst"]; // producer -> active
|
||||
DRAIN_PAUSED -> IDLE [label="flush"]; // buffer is cleared
|
||||
IDLE -> ERROR [label="←IStreamCallback.onError"];
|
||||
DRAINING -> ERROR [label="←IStreamCallback.onError"];
|
||||
TRANSFERRING -> ERROR [label="←IStreamCallback.onError"];
|
||||
ANY_STATE -> CLOSED [label="→IStream*.close"];
|
||||
CLOSED -> F;
|
||||
}
|
|
@ -97,6 +97,7 @@ void Module::cleanUpPatch(int32_t patchId) {
|
|||
}
|
||||
|
||||
ndk::ScopedAStatus Module::createStreamContext(int32_t in_portConfigId, int64_t in_bufferSizeFrames,
|
||||
std::shared_ptr<IStreamCallback> asyncCallback,
|
||||
StreamContext* out_context) {
|
||||
if (in_bufferSizeFrames <= 0) {
|
||||
LOG(ERROR) << __func__ << ": non-positive buffer size " << in_bufferSizeFrames;
|
||||
|
@ -136,7 +137,7 @@ ndk::ScopedAStatus Module::createStreamContext(int32_t in_portConfigId, int64_t
|
|||
std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
|
||||
std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
|
||||
frameSize, std::make_unique<StreamContext::DataMQ>(frameSize * in_bufferSizeFrames),
|
||||
mDebug.streamTransientStateDelayMs);
|
||||
asyncCallback, mDebug.streamTransientStateDelayMs);
|
||||
if (temp.isValid()) {
|
||||
*out_context = std::move(temp);
|
||||
} else {
|
||||
|
@ -461,7 +462,8 @@ ndk::ScopedAStatus Module::openInputStream(const OpenInputStreamArguments& in_ar
|
|||
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
|
||||
}
|
||||
StreamContext context;
|
||||
if (auto status = createStreamContext(in_args.portConfigId, in_args.bufferSizeFrames, &context);
|
||||
if (auto status = createStreamContext(in_args.portConfigId, in_args.bufferSizeFrames, nullptr,
|
||||
&context);
|
||||
!status.isOk()) {
|
||||
return status;
|
||||
}
|
||||
|
@ -501,8 +503,16 @@ ndk::ScopedAStatus Module::openOutputStream(const OpenOutputStreamArguments& in_
|
|||
<< " has COMPRESS_OFFLOAD flag set, requires offload info";
|
||||
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
|
||||
}
|
||||
const bool isNonBlocking = isBitPositionFlagSet(port->flags.get<AudioIoFlags::Tag::output>(),
|
||||
AudioOutputFlags::NON_BLOCKING);
|
||||
if (isNonBlocking && in_args.callback == nullptr) {
|
||||
LOG(ERROR) << __func__ << ": port id " << port->id
|
||||
<< " has NON_BLOCKING flag set, requires async callback";
|
||||
return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
|
||||
}
|
||||
StreamContext context;
|
||||
if (auto status = createStreamContext(in_args.portConfigId, in_args.bufferSizeFrames, &context);
|
||||
if (auto status = createStreamContext(in_args.portConfigId, in_args.bufferSizeFrames,
|
||||
isNonBlocking ? in_args.callback : nullptr, &context);
|
||||
!status.isOk()) {
|
||||
return status;
|
||||
}
|
||||
|
|
|
@ -178,13 +178,18 @@ StreamInWorkerLogic::Status StreamInWorkerLogic::cycle() {
|
|||
}
|
||||
break;
|
||||
case Tag::drain:
|
||||
if (mState == StreamDescriptor::State::ACTIVE) {
|
||||
usleep(1000); // Simulate a blocking call into the driver.
|
||||
populateReply(&reply, mIsConnected);
|
||||
// Can switch the state to ERROR if a driver error occurs.
|
||||
mState = StreamDescriptor::State::DRAINING;
|
||||
if (command.get<Tag::drain>() == StreamDescriptor::DrainMode::DRAIN_UNSPECIFIED) {
|
||||
if (mState == StreamDescriptor::State::ACTIVE) {
|
||||
usleep(1000); // Simulate a blocking call into the driver.
|
||||
populateReply(&reply, mIsConnected);
|
||||
// Can switch the state to ERROR if a driver error occurs.
|
||||
mState = StreamDescriptor::State::DRAINING;
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
}
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
LOG(WARNING) << __func__
|
||||
<< ": invalid drain mode: " << toString(command.get<Tag::drain>());
|
||||
}
|
||||
break;
|
||||
case Tag::standby:
|
||||
|
@ -262,11 +267,31 @@ bool StreamInWorkerLogic::read(size_t clientSize, StreamDescriptor::Reply* reply
|
|||
const std::string StreamOutWorkerLogic::kThreadName = "writer";
|
||||
|
||||
StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
|
||||
if (mState == StreamDescriptor::State::DRAINING) {
|
||||
if (mState == StreamDescriptor::State::DRAINING ||
|
||||
mState == StreamDescriptor::State::TRANSFERRING) {
|
||||
if (auto stateDurationMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - mTransientStateStart);
|
||||
stateDurationMs >= mTransientStateDelayMs) {
|
||||
mState = StreamDescriptor::State::IDLE;
|
||||
if (mAsyncCallback == nullptr) {
|
||||
// In blocking mode, mState can only be DRAINING.
|
||||
mState = StreamDescriptor::State::IDLE;
|
||||
} else {
|
||||
// In a real implementation, the driver should notify the HAL about
|
||||
// drain or transfer completion. In the stub, we switch unconditionally.
|
||||
if (mState == StreamDescriptor::State::DRAINING) {
|
||||
mState = StreamDescriptor::State::IDLE;
|
||||
ndk::ScopedAStatus status = mAsyncCallback->onDrainReady();
|
||||
if (!status.isOk()) {
|
||||
LOG(ERROR) << __func__ << ": error from onDrainReady: " << status;
|
||||
}
|
||||
} else {
|
||||
mState = StreamDescriptor::State::ACTIVE;
|
||||
ndk::ScopedAStatus status = mAsyncCallback->onTransferReady();
|
||||
if (!status.isOk()) {
|
||||
LOG(ERROR) << __func__ << ": error from onTransferReady: " << status;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mTransientStateDelayMs.count() != 0) {
|
||||
LOG(DEBUG) << __func__ << ": switched to state " << toString(mState)
|
||||
<< " after a timeout";
|
||||
|
@ -298,40 +323,57 @@ StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
|
|||
case Tag::getStatus:
|
||||
populateReply(&reply, mIsConnected);
|
||||
break;
|
||||
case Tag::start:
|
||||
case Tag::start: {
|
||||
bool commandAccepted = true;
|
||||
switch (mState) {
|
||||
case StreamDescriptor::State::STANDBY:
|
||||
mState = StreamDescriptor::State::IDLE;
|
||||
populateReply(&reply, mIsConnected);
|
||||
break;
|
||||
case StreamDescriptor::State::PAUSED:
|
||||
mState = StreamDescriptor::State::ACTIVE;
|
||||
populateReply(&reply, mIsConnected);
|
||||
break;
|
||||
case StreamDescriptor::State::DRAIN_PAUSED:
|
||||
switchToTransientState(StreamDescriptor::State::DRAINING);
|
||||
populateReply(&reply, mIsConnected);
|
||||
break;
|
||||
case StreamDescriptor::State::TRANSFER_PAUSED:
|
||||
switchToTransientState(StreamDescriptor::State::TRANSFERRING);
|
||||
break;
|
||||
default:
|
||||
populateReplyWrongState(&reply, command);
|
||||
commandAccepted = false;
|
||||
}
|
||||
break;
|
||||
if (commandAccepted) {
|
||||
populateReply(&reply, mIsConnected);
|
||||
}
|
||||
} break;
|
||||
case Tag::burst:
|
||||
if (const int32_t fmqByteCount = command.get<Tag::burst>(); fmqByteCount >= 0) {
|
||||
LOG(DEBUG) << __func__ << ": '" << toString(command.getTag()) << "' command for "
|
||||
<< fmqByteCount << " bytes";
|
||||
if (mState !=
|
||||
StreamDescriptor::State::ERROR) { // BURST can be handled in all valid states
|
||||
if (mState != StreamDescriptor::State::ERROR &&
|
||||
mState != StreamDescriptor::State::TRANSFERRING &&
|
||||
mState != StreamDescriptor::State::TRANSFER_PAUSED) {
|
||||
if (!write(fmqByteCount, &reply)) {
|
||||
mState = StreamDescriptor::State::ERROR;
|
||||
}
|
||||
if (mState == StreamDescriptor::State::STANDBY ||
|
||||
mState == StreamDescriptor::State::DRAIN_PAUSED) {
|
||||
mState = StreamDescriptor::State::PAUSED;
|
||||
mState == StreamDescriptor::State::DRAIN_PAUSED ||
|
||||
mState == StreamDescriptor::State::PAUSED) {
|
||||
if (mAsyncCallback == nullptr ||
|
||||
mState != StreamDescriptor::State::DRAIN_PAUSED) {
|
||||
mState = StreamDescriptor::State::PAUSED;
|
||||
} else {
|
||||
mState = StreamDescriptor::State::TRANSFER_PAUSED;
|
||||
}
|
||||
} else if (mState == StreamDescriptor::State::IDLE ||
|
||||
mState == StreamDescriptor::State::DRAINING) {
|
||||
mState = StreamDescriptor::State::ACTIVE;
|
||||
} // When in 'ACTIVE' and 'PAUSED' do not need to change the state.
|
||||
mState == StreamDescriptor::State::DRAINING ||
|
||||
mState == StreamDescriptor::State::ACTIVE) {
|
||||
if (mAsyncCallback == nullptr || reply.fmqByteCount == fmqByteCount) {
|
||||
mState = StreamDescriptor::State::ACTIVE;
|
||||
} else {
|
||||
switchToTransientState(StreamDescriptor::State::TRANSFERRING);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
}
|
||||
|
@ -340,13 +382,23 @@ StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
|
|||
}
|
||||
break;
|
||||
case Tag::drain:
|
||||
if (mState == StreamDescriptor::State::ACTIVE) {
|
||||
usleep(1000); // Simulate a blocking call into the driver.
|
||||
populateReply(&reply, mIsConnected);
|
||||
// Can switch the state to ERROR if a driver error occurs.
|
||||
switchToTransientState(StreamDescriptor::State::DRAINING);
|
||||
if (command.get<Tag::drain>() == StreamDescriptor::DrainMode::DRAIN_ALL ||
|
||||
command.get<Tag::drain>() == StreamDescriptor::DrainMode::DRAIN_EARLY_NOTIFY) {
|
||||
if (mState == StreamDescriptor::State::ACTIVE ||
|
||||
mState == StreamDescriptor::State::TRANSFERRING) {
|
||||
usleep(1000); // Simulate a blocking call into the driver.
|
||||
populateReply(&reply, mIsConnected);
|
||||
// Can switch the state to ERROR if a driver error occurs.
|
||||
switchToTransientState(StreamDescriptor::State::DRAINING);
|
||||
} else if (mState == StreamDescriptor::State::TRANSFER_PAUSED) {
|
||||
mState = StreamDescriptor::State::DRAIN_PAUSED;
|
||||
populateReply(&reply, mIsConnected);
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
}
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
LOG(WARNING) << __func__
|
||||
<< ": invalid drain mode: " << toString(command.get<Tag::drain>());
|
||||
}
|
||||
break;
|
||||
case Tag::standby:
|
||||
|
@ -359,20 +411,30 @@ StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
|
|||
populateReplyWrongState(&reply, command);
|
||||
}
|
||||
break;
|
||||
case Tag::pause:
|
||||
if (mState == StreamDescriptor::State::ACTIVE ||
|
||||
mState == StreamDescriptor::State::DRAINING) {
|
||||
populateReply(&reply, mIsConnected);
|
||||
mState = mState == StreamDescriptor::State::ACTIVE
|
||||
? StreamDescriptor::State::PAUSED
|
||||
: StreamDescriptor::State::DRAIN_PAUSED;
|
||||
} else {
|
||||
populateReplyWrongState(&reply, command);
|
||||
case Tag::pause: {
|
||||
bool commandAccepted = true;
|
||||
switch (mState) {
|
||||
case StreamDescriptor::State::ACTIVE:
|
||||
mState = StreamDescriptor::State::PAUSED;
|
||||
break;
|
||||
case StreamDescriptor::State::DRAINING:
|
||||
mState = StreamDescriptor::State::DRAIN_PAUSED;
|
||||
break;
|
||||
case StreamDescriptor::State::TRANSFERRING:
|
||||
mState = StreamDescriptor::State::TRANSFER_PAUSED;
|
||||
break;
|
||||
default:
|
||||
populateReplyWrongState(&reply, command);
|
||||
commandAccepted = false;
|
||||
}
|
||||
break;
|
||||
if (commandAccepted) {
|
||||
populateReply(&reply, mIsConnected);
|
||||
}
|
||||
} break;
|
||||
case Tag::flush:
|
||||
if (mState == StreamDescriptor::State::PAUSED ||
|
||||
mState == StreamDescriptor::State::DRAIN_PAUSED) {
|
||||
mState == StreamDescriptor::State::DRAIN_PAUSED ||
|
||||
mState == StreamDescriptor::State::TRANSFER_PAUSED) {
|
||||
populateReply(&reply, mIsConnected);
|
||||
mState = StreamDescriptor::State::IDLE;
|
||||
} else {
|
||||
|
|
|
@ -86,6 +86,7 @@ class Module : public BnModule {
|
|||
void cleanUpPatch(int32_t patchId);
|
||||
ndk::ScopedAStatus createStreamContext(
|
||||
int32_t in_portConfigId, int64_t in_bufferSizeFrames,
|
||||
std::shared_ptr<IStreamCallback> asyncCallback,
|
||||
::aidl::android::hardware::audio::core::StreamContext* out_context);
|
||||
ndk::ScopedAStatus findPortIdForNewStream(
|
||||
int32_t in_portConfigId, ::aidl::android::media::audio::common::AudioPort** port);
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include <aidl/android/hardware/audio/common/SourceMetadata.h>
|
||||
#include <aidl/android/hardware/audio/core/BnStreamIn.h>
|
||||
#include <aidl/android/hardware/audio/core/BnStreamOut.h>
|
||||
#include <aidl/android/hardware/audio/core/IStreamCallback.h>
|
||||
#include <aidl/android/hardware/audio/core/StreamDescriptor.h>
|
||||
#include <aidl/android/media/audio/common/AudioOffloadInfo.h>
|
||||
#include <fmq/AidlMessageQueue.h>
|
||||
|
@ -60,12 +61,14 @@ class StreamContext {
|
|||
|
||||
StreamContext() = default;
|
||||
StreamContext(std::unique_ptr<CommandMQ> commandMQ, std::unique_ptr<ReplyMQ> replyMQ,
|
||||
size_t frameSize, std::unique_ptr<DataMQ> dataMQ, int transientStateDelayMs)
|
||||
size_t frameSize, std::unique_ptr<DataMQ> dataMQ,
|
||||
std::shared_ptr<IStreamCallback> asyncCallback, int transientStateDelayMs)
|
||||
: mCommandMQ(std::move(commandMQ)),
|
||||
mInternalCommandCookie(std::rand()),
|
||||
mReplyMQ(std::move(replyMQ)),
|
||||
mFrameSize(frameSize),
|
||||
mDataMQ(std::move(dataMQ)),
|
||||
mAsyncCallback(asyncCallback),
|
||||
mTransientStateDelayMs(transientStateDelayMs) {}
|
||||
StreamContext(StreamContext&& other)
|
||||
: mCommandMQ(std::move(other.mCommandMQ)),
|
||||
|
@ -73,6 +76,7 @@ class StreamContext {
|
|||
mReplyMQ(std::move(other.mReplyMQ)),
|
||||
mFrameSize(other.mFrameSize),
|
||||
mDataMQ(std::move(other.mDataMQ)),
|
||||
mAsyncCallback(other.mAsyncCallback),
|
||||
mTransientStateDelayMs(other.mTransientStateDelayMs) {}
|
||||
StreamContext& operator=(StreamContext&& other) {
|
||||
mCommandMQ = std::move(other.mCommandMQ);
|
||||
|
@ -80,11 +84,13 @@ class StreamContext {
|
|||
mReplyMQ = std::move(other.mReplyMQ);
|
||||
mFrameSize = other.mFrameSize;
|
||||
mDataMQ = std::move(other.mDataMQ);
|
||||
mAsyncCallback = other.mAsyncCallback;
|
||||
mTransientStateDelayMs = other.mTransientStateDelayMs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void fillDescriptor(StreamDescriptor* desc);
|
||||
std::shared_ptr<IStreamCallback> getAsyncCallback() const { return mAsyncCallback; }
|
||||
CommandMQ* getCommandMQ() const { return mCommandMQ.get(); }
|
||||
DataMQ* getDataMQ() const { return mDataMQ.get(); }
|
||||
size_t getFrameSize() const { return mFrameSize; }
|
||||
|
@ -100,6 +106,7 @@ class StreamContext {
|
|||
std::unique_ptr<ReplyMQ> mReplyMQ;
|
||||
size_t mFrameSize;
|
||||
std::unique_ptr<DataMQ> mDataMQ;
|
||||
std::shared_ptr<IStreamCallback> mAsyncCallback;
|
||||
int mTransientStateDelayMs;
|
||||
};
|
||||
|
||||
|
@ -118,6 +125,7 @@ class StreamWorkerCommonLogic : public ::android::hardware::audio::common::Strea
|
|||
mCommandMQ(context.getCommandMQ()),
|
||||
mReplyMQ(context.getReplyMQ()),
|
||||
mDataMQ(context.getDataMQ()),
|
||||
mAsyncCallback(context.getAsyncCallback()),
|
||||
mTransientStateDelayMs(context.getTransientStateDelayMs()) {}
|
||||
std::string init() override;
|
||||
void populateReply(StreamDescriptor::Reply* reply, bool isConnected) const;
|
||||
|
@ -138,6 +146,7 @@ class StreamWorkerCommonLogic : public ::android::hardware::audio::common::Strea
|
|||
StreamContext::CommandMQ* mCommandMQ;
|
||||
StreamContext::ReplyMQ* mReplyMQ;
|
||||
StreamContext::DataMQ* mDataMQ;
|
||||
std::shared_ptr<IStreamCallback> mAsyncCallback;
|
||||
const std::chrono::duration<int, std::milli> mTransientStateDelayMs;
|
||||
std::chrono::time_point<std::chrono::steady_clock> mTransientStateStart;
|
||||
// We use an array and the "size" field instead of a vector to be able to detect
|
||||
|
|
|
@ -125,21 +125,21 @@ std::vector<AudioPort> ModuleConfig::getOutputMixPorts() const {
|
|||
return result;
|
||||
}
|
||||
|
||||
std::vector<AudioPort> ModuleConfig::getNonBlockingMixPorts(bool attachedOnly,
|
||||
bool singlePort) const {
|
||||
return findMixPorts(false /*isInput*/, singlePort, [&](const AudioPort& port) {
|
||||
return isBitPositionFlagSet(port.flags.get<AudioIoFlags::Tag::output>(),
|
||||
AudioOutputFlags::NON_BLOCKING) &&
|
||||
(!attachedOnly || !getAttachedSinkDevicesPortsForMixPort(port).empty());
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<AudioPort> ModuleConfig::getOffloadMixPorts(bool attachedOnly, bool singlePort) const {
|
||||
std::vector<AudioPort> result;
|
||||
const auto mixPorts = getMixPorts(false /*isInput*/);
|
||||
auto offloadPortIt = mixPorts.begin();
|
||||
while (offloadPortIt != mixPorts.end()) {
|
||||
offloadPortIt = std::find_if(offloadPortIt, mixPorts.end(), [&](const AudioPort& port) {
|
||||
return isBitPositionFlagSet(port.flags.get<AudioIoFlags::Tag::output>(),
|
||||
AudioOutputFlags::COMPRESS_OFFLOAD) &&
|
||||
(!attachedOnly || !getAttachedSinkDevicesPortsForMixPort(port).empty());
|
||||
});
|
||||
if (offloadPortIt == mixPorts.end()) break;
|
||||
result.push_back(*offloadPortIt++);
|
||||
if (singlePort) break;
|
||||
}
|
||||
return result;
|
||||
return findMixPorts(false /*isInput*/, singlePort, [&](const AudioPort& port) {
|
||||
return isBitPositionFlagSet(port.flags.get<AudioIoFlags::Tag::output>(),
|
||||
AudioOutputFlags::COMPRESS_OFFLOAD) &&
|
||||
(!attachedOnly || !getAttachedSinkDevicesPortsForMixPort(port).empty());
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<AudioPort> ModuleConfig::getAttachedDevicesPortsForMixPort(
|
||||
|
@ -343,6 +343,19 @@ static bool isDynamicProfile(const AudioProfile& profile) {
|
|||
profile.sampleRates.empty() || profile.channelMasks.empty();
|
||||
}
|
||||
|
||||
std::vector<AudioPort> ModuleConfig::findMixPorts(
|
||||
bool isInput, bool singlePort, std::function<bool(const AudioPort&)> pred) const {
|
||||
std::vector<AudioPort> result;
|
||||
const auto mixPorts = getMixPorts(isInput);
|
||||
for (auto mixPortIt = mixPorts.begin(); mixPortIt != mixPorts.end();) {
|
||||
mixPortIt = std::find_if(mixPortIt, mixPorts.end(), pred);
|
||||
if (mixPortIt == mixPorts.end()) break;
|
||||
result.push_back(*mixPortIt++);
|
||||
if (singlePort) break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<AudioPortConfig> ModuleConfig::generateAudioMixPortConfigs(
|
||||
const std::vector<AudioPort>& ports, bool isInput, bool singleProfile) const {
|
||||
std::vector<AudioPortConfig> result;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
@ -48,6 +49,8 @@ class ModuleConfig {
|
|||
std::vector<aidl::android::media::audio::common::AudioPort> getMixPorts(bool isInput) const {
|
||||
return isInput ? getInputMixPorts() : getOutputMixPorts();
|
||||
}
|
||||
std::vector<aidl::android::media::audio::common::AudioPort> getNonBlockingMixPorts(
|
||||
bool attachedOnly, bool singlePort) const;
|
||||
std::vector<aidl::android::media::audio::common::AudioPort> getOffloadMixPorts(
|
||||
bool attachedOnly, bool singlePort) const;
|
||||
|
||||
|
@ -121,6 +124,9 @@ class ModuleConfig {
|
|||
std::string toString() const;
|
||||
|
||||
private:
|
||||
std::vector<aidl::android::media::audio::common::AudioPort> findMixPorts(
|
||||
bool isInput, bool singlePort,
|
||||
std::function<bool(const aidl::android::media::audio::common::AudioPort&)> pred) const;
|
||||
std::vector<aidl::android::media::audio::common::AudioPortConfig> generateAudioMixPortConfigs(
|
||||
const std::vector<aidl::android::media::audio::common::AudioPort>& ports, bool isInput,
|
||||
bool singleProfile) const;
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue