MediaSyncEvent: Fix thread safety
Test: atest synchronizedrecordstate_tests
Test: atest MediaSyncEventTest#testSynchronizedRecord
Bug: 281168109
Change-Id: I75677947ab2b508d20d55256187830c3ace9c9e9
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 6b50693..a470478 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -90,6 +90,7 @@
#include <sounddose/SoundDoseManager.h>
#include <timing/MonotonicFrameCounter.h>
#include <timing/SyncEvent.h>
+#include <timing/SynchronizedRecordState.h>
#include "FastCapture.h"
#include "FastMixer.h"
diff --git a/services/audioflinger/RecordTracks.h b/services/audioflinger/RecordTracks.h
index 868acfc..d91a210 100644
--- a/services/audioflinger/RecordTracks.h
+++ b/services/audioflinger/RecordTracks.h
@@ -109,10 +109,8 @@
// be dropped and therefore not read by the application.
sp<audioflinger::SyncEvent> mSyncStartEvent;
- // number of captured frames to drop after the start sync event has been received.
- // when < 0, maximum frames to drop before starting capture even if sync event is
- // not received
- ssize_t mFramesToDrop;
+ audioflinger::SynchronizedRecordState
+ mSynchronizedRecordState{mSampleRate}; // sampleRate defined in base
// used by resampler to find source frames
ResamplerBufferProvider *mResamplerBufferProvider;
diff --git a/services/audioflinger/Threads.cpp b/services/audioflinger/Threads.cpp
index 06db915..b7f9d76 100644
--- a/services/audioflinger/Threads.cpp
+++ b/services/audioflinger/Threads.cpp
@@ -8469,7 +8469,11 @@
overrun = OVERRUN_FALSE;
}
- if (activeTrack->mFramesToDrop == 0) {
+ // MediaSyncEvent handling: Synchronize AudioRecord to AudioTrack completion.
+ const ssize_t framesToDrop =
+ activeTrack->mSynchronizedRecordState.updateRecordFrames(framesOut);
+ if (framesToDrop == 0) {
+ // no sync event, process normally, otherwise ignore.
if (framesOut > 0) {
activeTrack->mSink.frameCount = framesOut;
// Sanitize before releasing if the track has no access to the source data
@@ -8479,28 +8483,7 @@
}
activeTrack->releaseBuffer(&activeTrack->mSink);
}
- } else {
- // FIXME could do a partial drop of framesOut
- if (activeTrack->mFramesToDrop > 0) {
- activeTrack->mFramesToDrop -= (ssize_t)framesOut;
- if (activeTrack->mFramesToDrop <= 0) {
- activeTrack->clearSyncStartEvent();
- }
- } else {
- activeTrack->mFramesToDrop += framesOut;
- if (activeTrack->mFramesToDrop >= 0 || activeTrack->mSyncStartEvent == 0 ||
- activeTrack->mSyncStartEvent->isCancelled()) {
- ALOGW("Synced record %s, session %d, trigger session %d",
- (activeTrack->mFramesToDrop >= 0) ? "timed out" : "cancelled",
- activeTrack->sessionId(),
- (activeTrack->mSyncStartEvent != 0) ?
- activeTrack->mSyncStartEvent->triggerSession() :
- AUDIO_SESSION_NONE);
- activeTrack->clearSyncStartEvent();
- }
- }
}
-
if (framesOut == 0) {
break;
}
@@ -8833,20 +8816,10 @@
if (event == AudioSystem::SYNC_EVENT_NONE) {
recordTrack->clearSyncStartEvent();
} else if (event != AudioSystem::SYNC_EVENT_SAME) {
- recordTrack->mSyncStartEvent = mAudioFlinger->createSyncEvent(event,
- triggerSession,
- recordTrack->sessionId(),
- syncStartEventCallback,
- recordTrack);
- // Sync event can be cancelled by the trigger session if the track is not in a
- // compatible state in which case we start record immediately
- if (recordTrack->mSyncStartEvent->isCancelled()) {
- recordTrack->clearSyncStartEvent();
- } else {
- // do not wait for the event for more than AudioSystem::kSyncRecordStartTimeOutMs
- recordTrack->mFramesToDrop = -(ssize_t)
- ((AudioSystem::kSyncRecordStartTimeOutMs * recordTrack->mSampleRate) / 1000);
- }
+ recordTrack->mSynchronizedRecordState.startRecording(
+ mAudioFlinger->createSyncEvent(
+ event, triggerSession,
+ recordTrack->sessionId(), syncStartEventCallback, recordTrack));
}
{
diff --git a/services/audioflinger/Tracks.cpp b/services/audioflinger/Tracks.cpp
index d3bf699..00c88bc 100644
--- a/services/audioflinger/Tracks.cpp
+++ b/services/audioflinger/Tracks.cpp
@@ -1681,6 +1681,7 @@
{
for (auto it = mSyncEvents.begin(); it != mSyncEvents.end();) {
if ((*it)->type() == type) {
+ ALOGV("%s: triggering SyncEvent type %d", __func__, type);
(*it)->trigger();
it = mSyncEvents.erase(it);
} else {
@@ -1931,6 +1932,8 @@
}
}
+ ALOGV("%s: trackFramesReleased:%lld sinkFramesWritten:%lld setDrained: %d",
+ __func__, (long long)trackFramesReleased, (long long)sinkFramesWritten, drained);
mAudioTrackServerProxy->setDrained(drained);
// Set correction for flushed frames that are not accounted for in released.
local.mFlushed = mAudioTrackServerProxy->framesFlushed();
@@ -2505,7 +2508,6 @@
type, portId,
std::string(AMEDIAMETRICS_KEY_PREFIX_AUDIO_RECORD) + std::to_string(portId)),
mOverflow(false),
- mFramesToDrop(0),
mResamplerBufferProvider(NULL), // initialize in case of early constructor exit
mRecordBufferConverter(NULL),
mFlags(flags),
@@ -2707,28 +2709,24 @@
result.append("\n");
}
+// This is invoked by SyncEvent callback.
void AudioFlinger::RecordThread::RecordTrack::handleSyncStartEvent(
const sp<audioflinger::SyncEvent>& event)
{
- if (event == mSyncStartEvent) {
- ssize_t framesToDrop = 0;
- sp<ThreadBase> threadBase = mThread.promote();
- if (threadBase != 0) {
- // TODO: use actual buffer filling status instead of 2 buffers when info is available
- // from audio HAL
- framesToDrop = threadBase->mFrameCount * 2;
- }
- mFramesToDrop = framesToDrop;
+ size_t framesToDrop = 0;
+ sp<ThreadBase> threadBase = mThread.promote();
+ if (threadBase != 0) {
+ // TODO: use actual buffer filling status instead of 2 buffers when info is available
+ // from audio HAL
+ framesToDrop = threadBase->mFrameCount * 2;
}
+
+ mSynchronizedRecordState.onPlaybackFinished(event, framesToDrop);
}
void AudioFlinger::RecordThread::RecordTrack::clearSyncStartEvent()
{
- if (mSyncStartEvent != 0) {
- mSyncStartEvent->cancel();
- mSyncStartEvent.clear();
- }
- mFramesToDrop = 0;
+ mSynchronizedRecordState.clear();
}
void AudioFlinger::RecordThread::RecordTrack::updateTrackFrameInfo(
diff --git a/services/audioflinger/timing/SynchronizedRecordState.h b/services/audioflinger/timing/SynchronizedRecordState.h
new file mode 100644
index 0000000..f40d41b
--- /dev/null
+++ b/services/audioflinger/timing/SynchronizedRecordState.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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 "SyncEvent.h"
+
+#pragma push_macro("LOG_TAG")
+#undef LOG_TAG
+#define LOG_TAG "SynchronizedRecordState"
+
+namespace android::audioflinger {
+
+class SynchronizedRecordState {
+public:
+ explicit SynchronizedRecordState(uint32_t sampleRate)
+ : mSampleRate(sampleRate)
+ {}
+
+ void clear() {
+ std::lock_guard lg(mLock);
+ clear_l();
+ }
+
+ // Called by the RecordThread when recording is starting.
+ void startRecording(const sp<SyncEvent>& event) {
+ std::lock_guard lg(mLock);
+ mSyncStartEvent = event;
+ // Sync event can be cancelled by the trigger session if the track is not in a
+ // compatible state in which case we start record immediately
+ if (mSyncStartEvent->isCancelled()) {
+ clear_l();
+ } else {
+ mFramesToDrop = -(ssize_t)
+ ((AudioSystem::kSyncRecordStartTimeOutMs * mSampleRate) / 1000);
+ }
+ }
+
+ // Invoked by SyncEvent callback.
+ void onPlaybackFinished(const sp<SyncEvent>& event, size_t framesToDrop = 1) {
+ std::lock_guard lg(mLock);
+ if (event == mSyncStartEvent) {
+ mFramesToDrop = framesToDrop; // compute this
+ ALOGV("%s: framesToDrop:%zd", __func__, mFramesToDrop);
+ }
+ }
+
+ // Returns the current FramesToDrop counter
+ //
+ // if <0 waiting (drop the frames)
+ // if >0 draining (drop the frames)
+ // else if ==0 proceed to record.
+ ssize_t updateRecordFrames(size_t frames) {
+ std::lock_guard lg(mLock);
+ if (mFramesToDrop > 0) {
+ // we've been triggered, we count down for start delay
+ ALOGV("%s: trigger countdown %zd by %zu frames", __func__, mFramesToDrop, frames);
+ mFramesToDrop -= (ssize_t)frames;
+ if (mFramesToDrop <= 0) clear_l();
+ } else if (mFramesToDrop < 0) {
+ // we're waiting to be triggered.
+ // ALOGD("%s: timeout countup %zd with %zu frames", __func__, mFramesToDrop, frames);
+ mFramesToDrop += (ssize_t)frames;
+ if (mFramesToDrop >= 0 || !mSyncStartEvent || mSyncStartEvent->isCancelled()) {
+ ALOGW("Synced record %s, trigger session %d",
+ (mFramesToDrop >= 0) ? "timed out" : "cancelled",
+ (mSyncStartEvent) ? mSyncStartEvent->triggerSession()
+ : AUDIO_SESSION_NONE);
+ clear_l();
+ }
+ }
+ return mFramesToDrop;
+ }
+
+private:
+ const uint32_t mSampleRate;
+
+ std::mutex mLock;
+ // number of captured frames to drop after the start sync event has been received.
+ // when < 0, maximum frames to drop before starting capture even if sync event is
+ // not received
+ ssize_t mFramesToDrop GUARDED_BY(mLock) = 0;
+
+ // sync event triggering actual audio capture. Frames read before this event will
+ // be dropped and therefore not read by the application.
+ sp<SyncEvent> mSyncStartEvent GUARDED_BY(mLock);
+
+ void clear_l() REQUIRES(mLock) {
+ if (mSyncStartEvent) {
+ mSyncStartEvent->cancel();
+ mSyncStartEvent.clear();
+ }
+ mFramesToDrop = 0;
+ }
+};
+
+} // namespace android::audioflinger
+
+#pragma pop_macro("LOG_TAG")
diff --git a/services/audioflinger/timing/tests/Android.bp b/services/audioflinger/timing/tests/Android.bp
index c360799..d1e5563 100644
--- a/services/audioflinger/timing/tests/Android.bp
+++ b/services/audioflinger/timing/tests/Android.bp
@@ -51,4 +51,29 @@
"-Werror",
"-Wextra",
],
-}
\ No newline at end of file
+}
+
+cc_test {
+ name: "synchronizedrecordstate_tests",
+
+ host_supported: true,
+
+ srcs: [
+ "synchronizedrecordstate_tests.cpp"
+ ],
+
+ header_libs: [
+ "libaudioclient_headers",
+ ],
+
+ static_libs: [
+ "liblog",
+ "libutils", // RefBase
+ ],
+
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wextra",
+ ],
+ }
\ No newline at end of file
diff --git a/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp b/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp
new file mode 100644
index 0000000..ee5d269
--- /dev/null
+++ b/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "synchronizedrecordstate_tests"
+
+#include "../SynchronizedRecordState.h"
+
+#include <gtest/gtest.h>
+
+using namespace android;
+using namespace android::audioflinger;
+
+namespace {
+
+TEST(SynchronizedRecordStateTests, Basic) {
+ struct Cookie : public RefBase {};
+
+ // These variables are set by trigger().
+ bool triggered = false;
+ wp<SyncEvent> param;
+
+ constexpr auto type = AudioSystem::SYNC_EVENT_PRESENTATION_COMPLETE;
+ constexpr auto triggerSession = audio_session_t(10);
+ constexpr auto listenerSession = audio_session_t(11);
+ const SyncEventCallback callback =
+ [&](const wp<SyncEvent>& event) {
+ triggered = true;
+ param = event;
+ };
+ const auto cookie = sp<Cookie>::make();
+
+ // Check timeout.
+ SynchronizedRecordState recordState(48000 /* sampleRate */);
+ auto syncEvent = sp<SyncEvent>::make(
+ type,
+ triggerSession,
+ listenerSession,
+ callback,
+ cookie);
+ recordState.startRecording(syncEvent);
+ recordState.updateRecordFrames(2);
+ ASSERT_FALSE(triggered);
+ ASSERT_EQ(0, recordState.updateRecordFrames(1'000'000'000));
+ ASSERT_FALSE(triggered);
+ ASSERT_TRUE(syncEvent->isCancelled());
+
+ // Check count down after track is complete.
+ syncEvent = sp<SyncEvent>::make(
+ type,
+ triggerSession,
+ listenerSession,
+ callback,
+ cookie);
+ recordState.startRecording(syncEvent);
+ recordState.onPlaybackFinished(syncEvent, 10);
+ ASSERT_EQ(1, recordState.updateRecordFrames(9));
+ ASSERT_FALSE(triggered);
+ ASSERT_EQ(0, recordState.updateRecordFrames(2));
+ ASSERT_FALSE(triggered);
+ ASSERT_TRUE(syncEvent->isCancelled());
+}
+
+}