OSDN Git Service

Merge \\"audio_utils: Support more format conversions\\" into nyc-mr1-dev am: 712aefac45
authorGlenn Kasten <gkasten@google.com>
Tue, 12 Jul 2016 17:33:36 +0000 (17:33 +0000)
committerandroid-build-merger <android-build-merger@google.com>
Tue, 12 Jul 2016 17:33:36 +0000 (17:33 +0000)
am: 0d8f2fa3ae

Change-Id: Ic9f832249fe938e6080195191b1103cabe1e3c57

audio_utils/Android.mk
audio_utils/README.md [new file with mode: 0644]
audio_utils/fifo.c [deleted file]
audio_utils/fifo.cpp [new file with mode: 0644]
audio_utils/include/audio_utils/fifo.h
audio_utils/tests/Android.mk
audio_utils/tests/fifo_tests.cpp
audio_utils/tests/fifo_threads.cpp [new file with mode: 0644]
audio_utils/tests/getch.c [new symlink]
audio_utils/tests/getch.h [new symlink]

index 32c94e2..7085ef9 100644 (file)
@@ -8,7 +8,7 @@ LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES:= \
        channels.c \
        conversion.cpp \
-       fifo.c \
+       fifo.cpp \
        fixedfft.cpp.arm \
        format.c \
        limiter.c \
@@ -37,7 +37,7 @@ LOCAL_MODULE := libaudioutils
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := \
        channels.c \
-       fifo.c \
+       fifo.cpp \
        format.c \
        limiter.c \
        minifloat.c \
@@ -88,7 +88,7 @@ LOCAL_MODULE := libfifo
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_SRC_FILES := \
-       fifo.c \
+       fifo.cpp \
        primitives.c \
        roundup.c
 
diff --git a/audio_utils/README.md b/audio_utils/README.md
new file mode 100644 (file)
index 0000000..df78972
--- /dev/null
@@ -0,0 +1,8 @@
+# How to build and view documentation
+
+* doxygen Doxyfile
+* cd html
+* python -m SimpleHTTPServer
+* open in web browser
+  http://localhost:8000/classaudio__utils__fifo.html
+* when done: rm -rf html
diff --git a/audio_utils/fifo.c b/audio_utils/fifo.c
deleted file mode 100644 (file)
index c818a50..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2015 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 "audio_utils_fifo"
-
-#include <stdlib.h>
-#include <string.h>
-#include <audio_utils/fifo.h>
-#include <audio_utils/roundup.h>
-#include <cutils/atomic.h>
-#include <cutils/log.h>
-
-void audio_utils_fifo_init(struct audio_utils_fifo *fifo, size_t frameCount, size_t frameSize,
-        void *buffer)
-{
-    // We would need a 64-bit roundup to support larger frameCount.
-    ALOG_ASSERT(fifo != NULL && frameCount > 0 && frameSize > 0 && buffer != NULL);
-    fifo->mFrameCount = frameCount;
-    fifo->mFrameCountP2 = roundup(frameCount);
-    fifo->mFudgeFactor = fifo->mFrameCountP2 - fifo->mFrameCount;
-    fifo->mFrameSize = frameSize;
-    fifo->mBuffer = buffer;
-    fifo->mFront = 0;
-    fifo->mRear = 0;
-}
-
-void audio_utils_fifo_deinit(struct audio_utils_fifo *fifo __unused)
-{
-}
-
-// Return a new index as the sum of an old index (either mFront or mRear) and a specified increment.
-static inline int32_t audio_utils_fifo_sum(struct audio_utils_fifo *fifo, int32_t index,
-        uint32_t increment)
-{
-    if (fifo->mFudgeFactor) {
-        uint32_t mask = fifo->mFrameCountP2 - 1;
-        ALOG_ASSERT((index & mask) < fifo->mFrameCount);
-        ALOG_ASSERT(/*0 <= increment &&*/ increment <= fifo->mFrameCountP2);
-        if ((index & mask) + increment >= fifo->mFrameCount) {
-            increment += fifo->mFudgeFactor;
-        }
-        index += increment;
-        ALOG_ASSERT((index & mask) < fifo->mFrameCount);
-        return index;
-    } else {
-        return index + increment;
-    }
-}
-
-// Return the difference between two indices: rear - front, where 0 <= difference <= mFrameCount.
-static inline size_t audio_utils_fifo_diff(struct audio_utils_fifo *fifo, int32_t rear,
-        int32_t front)
-{
-    int32_t diff = rear - front;
-    if (fifo->mFudgeFactor) {
-        uint32_t mask = ~(fifo->mFrameCountP2 - 1);
-        int32_t genDiff = (rear & mask) - (front & mask);
-        if (genDiff != 0) {
-            ALOG_ASSERT(genDiff == (int32_t) fifo->mFrameCountP2);
-            diff -= fifo->mFudgeFactor;
-        }
-    }
-    // FIFO should not be overfull
-    ALOG_ASSERT(0 <= diff && diff <= (int32_t) fifo->mFrameCount);
-    return (size_t) diff;
-}
-
-ssize_t audio_utils_fifo_write(struct audio_utils_fifo *fifo, const void *buffer, size_t count)
-{
-    int32_t front = android_atomic_acquire_load(&fifo->mFront);
-    int32_t rear = fifo->mRear;
-    size_t availToWrite = fifo->mFrameCount - audio_utils_fifo_diff(fifo, rear, front);
-    if (availToWrite > count) {
-        availToWrite = count;
-    }
-    rear &= fifo->mFrameCountP2 - 1;
-    size_t part1 = fifo->mFrameCount - rear;
-    if (part1 > availToWrite) {
-        part1 = availToWrite;
-    }
-    if (part1 > 0) {
-        memcpy((char *) fifo->mBuffer + (rear * fifo->mFrameSize), buffer,
-                part1 * fifo->mFrameSize);
-        size_t part2 = availToWrite - part1;
-        if (part2 > 0) {
-            memcpy(fifo->mBuffer, (char *) buffer + (part1 * fifo->mFrameSize),
-                    part2 * fifo->mFrameSize);
-        }
-        android_atomic_release_store(audio_utils_fifo_sum(fifo, fifo->mRear, availToWrite),
-                &fifo->mRear);
-    }
-    return availToWrite;
-}
-
-ssize_t audio_utils_fifo_read(struct audio_utils_fifo *fifo, void *buffer, size_t count)
-{
-    int32_t rear = android_atomic_acquire_load(&fifo->mRear);
-    int32_t front = fifo->mFront;
-    size_t availToRead = audio_utils_fifo_diff(fifo, rear, front);
-    if (availToRead > count) {
-        availToRead = count;
-    }
-    front &= fifo->mFrameCountP2 - 1;
-    size_t part1 = fifo->mFrameCount - front;
-    if (part1 > availToRead) {
-        part1 = availToRead;
-    }
-    if (part1 > 0) {
-        memcpy(buffer, (char *) fifo->mBuffer + (front * fifo->mFrameSize),
-                part1 * fifo->mFrameSize);
-        size_t part2 = availToRead - part1;
-        if (part2 > 0) {
-            memcpy((char *) buffer + (part1 * fifo->mFrameSize), fifo->mBuffer,
-                    part2 * fifo->mFrameSize);
-        }
-        android_atomic_release_store(audio_utils_fifo_sum(fifo, fifo->mFront, availToRead),
-                &fifo->mFront);
-    }
-    return availToRead;
-}
diff --git a/audio_utils/fifo.cpp b/audio_utils/fifo.cpp
new file mode 100644 (file)
index 0000000..3574dbf
--- /dev/null
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 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 "audio_utils_fifo"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+// FIXME futex portion is not supported on Mac, should use the Mac alternative
+#ifdef __linux__
+#include <linux/futex.h>
+#include <sys/syscall.h>
+#else
+#define FUTEX_WAIT 0
+#define FUTEX_WAIT_PRIVATE 0
+#define FUTEX_WAKE 0
+#define FUTEX_WAKE_PRIVATE 0
+#endif
+
+#include <audio_utils/fifo.h>
+#include <audio_utils/roundup.h>
+#include <cutils/log.h>
+#include <utils/Errors.h>
+
+static int sys_futex(void *addr1, int op, int val1, struct timespec *timeout, void *addr2, int val3)
+{
+#ifdef __linux__
+    return syscall(SYS_futex, addr1, op, val1, timeout, addr2, val3);
+#else
+    (void) addr1;
+    (void) op;
+    (void) val1;
+    (void) timeout;
+    (void) addr2;
+    (void) val3;
+    errno = ENOSYS;
+    return -1;
+#endif
+}
+
+audio_utils_fifo_base::audio_utils_fifo_base(uint32_t frameCount)
+        __attribute__((no_sanitize("integer"))) :
+    mFrameCount(frameCount), mFrameCountP2(roundup(frameCount)),
+    mFudgeFactor(mFrameCountP2 - mFrameCount),
+    mIsPrivate(true),
+    mSharedRear(0), mThrottleFront(NULL)
+{
+    // actual upper bound on frameCount will depend on the frame size
+    LOG_ALWAYS_FATAL_IF(frameCount == 0 || frameCount > ((uint32_t) INT_MAX));
+}
+
+audio_utils_fifo_base::~audio_utils_fifo_base()
+{
+}
+
+uint32_t audio_utils_fifo_base::sum(uint32_t index, uint32_t increment)
+        __attribute__((no_sanitize("integer")))
+{
+    if (mFudgeFactor) {
+        uint32_t mask = mFrameCountP2 - 1;
+        ALOG_ASSERT((index & mask) < mFrameCount);
+        ALOG_ASSERT(increment <= mFrameCountP2);
+        if ((index & mask) + increment >= mFrameCount) {
+            increment += mFudgeFactor;
+        }
+        index += increment;
+        ALOG_ASSERT((index & mask) < mFrameCount);
+        return index;
+    } else {
+        return index + increment;
+    }
+}
+
+int32_t audio_utils_fifo_base::diff(uint32_t rear, uint32_t front, size_t *lost)
+        __attribute__((no_sanitize("integer")))
+{
+    uint32_t diff = rear - front;
+    if (mFudgeFactor) {
+        uint32_t mask = mFrameCountP2 - 1;
+        uint32_t rearMasked = rear & mask;
+        uint32_t frontMasked = front & mask;
+        if (rearMasked >= mFrameCount || frontMasked >= mFrameCount) {
+            return -EIO;
+        }
+        uint32_t genDiff = (rear & ~mask) - (front & ~mask);
+        if (genDiff != 0) {
+            if (genDiff > mFrameCountP2) {
+                if (lost != NULL) {
+                    // TODO provide a more accurate estimate
+                    *lost = (genDiff / mFrameCountP2) * mFrameCount;
+                }
+                return -EOVERFLOW;
+            }
+            diff -= mFudgeFactor;
+        }
+    }
+    // FIFO should not be overfull
+    if (diff > mFrameCount) {
+        if (lost != NULL) {
+            *lost = diff - mFrameCount;
+        }
+        return -EOVERFLOW;
+    }
+    return (int32_t) diff;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+audio_utils_fifo::audio_utils_fifo(uint32_t frameCount, uint32_t frameSize, void *buffer)
+        __attribute__((no_sanitize("integer"))) :
+    audio_utils_fifo_base(frameCount), mFrameSize(frameSize), mBuffer(buffer)
+{
+    // maximum value of frameCount * frameSize is INT_MAX (2^31 - 1), not 2^31, because we need to
+    // be able to distinguish successful and error return values from read and write.
+    LOG_ALWAYS_FATAL_IF(frameCount == 0 || frameSize == 0 || buffer == NULL ||
+            frameCount > ((uint32_t) INT_MAX) / frameSize);
+}
+
+audio_utils_fifo::~audio_utils_fifo()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+audio_utils_fifo_provider::audio_utils_fifo_provider() :
+    mObtained(0)
+{
+}
+
+audio_utils_fifo_provider::~audio_utils_fifo_provider()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+audio_utils_fifo_writer::audio_utils_fifo_writer(audio_utils_fifo& fifo) :
+    audio_utils_fifo_provider(), mFifo(fifo), mLocalRear(0),
+    mLowLevelArm(fifo.mFrameCount), mHighLevelTrigger(0), mArmed(false),
+    mEffectiveFrames(fifo.mFrameCount)
+{
+}
+
+audio_utils_fifo_writer::~audio_utils_fifo_writer()
+{
+}
+
+ssize_t audio_utils_fifo_writer::write(const void *buffer, size_t count, struct timespec *timeout)
+        __attribute__((no_sanitize("integer")))
+{
+    audio_utils_iovec iovec[2];
+    ssize_t availToWrite = obtain(iovec, count, timeout);
+    if (availToWrite > 0) {
+        memcpy((char *) mFifo.mBuffer + iovec[0].mOffset * mFifo.mFrameSize, buffer,
+                iovec[0].mLength * mFifo.mFrameSize);
+        if (iovec[1].mLength > 0) {
+            memcpy((char *) mFifo.mBuffer + iovec[1].mOffset * mFifo.mFrameSize,
+                    (char *) buffer + (iovec[0].mLength * mFifo.mFrameSize),
+                    iovec[1].mLength * mFifo.mFrameSize);
+        }
+        release(availToWrite);
+    }
+    return availToWrite;
+}
+
+ssize_t audio_utils_fifo_writer::obtain(audio_utils_iovec iovec[2], size_t count,
+        struct timespec *timeout)
+        __attribute__((no_sanitize("integer")))
+{
+    size_t availToWrite;
+    if (mFifo.mThrottleFront != NULL) {
+        uint32_t front;
+        for (;;) {
+            front = (uint32_t) atomic_load_explicit(mFifo.mThrottleFront,
+                    std::memory_order_acquire);
+            int32_t filled = mFifo.diff(mLocalRear, front, NULL /*lost*/);
+            if (filled < 0) {
+                mObtained = 0;
+                return (ssize_t) filled;
+            }
+            availToWrite = mEffectiveFrames > (uint32_t) filled ?
+                    mEffectiveFrames - (uint32_t) filled : 0;
+            // TODO pull out "count == 0"
+            if (count == 0 || availToWrite > 0 || timeout == NULL ||
+                    (timeout->tv_sec == 0 && timeout->tv_nsec == 0)) {
+                break;
+            }
+            int err = sys_futex(mFifo.mThrottleFront,
+                    mFifo.mIsPrivate ? FUTEX_WAIT_PRIVATE : FUTEX_WAIT, front, timeout, NULL, 0);
+            if (err < 0) {
+                switch (errno) {
+                case EWOULDBLOCK:
+                case EINTR:
+                case ETIMEDOUT:
+                    break;
+                default:
+                    LOG_ALWAYS_FATAL("unexpected err=%d errno=%d", err, errno);
+                    break;
+                }
+            }
+            timeout = NULL;
+        }
+    } else {
+        availToWrite = mEffectiveFrames;
+    }
+    if (availToWrite > count) {
+        availToWrite = count;
+    }
+    uint32_t rearMasked = mLocalRear & (mFifo.mFrameCountP2 - 1);
+    size_t part1 = mFifo.mFrameCount - rearMasked;
+    if (part1 > availToWrite) {
+        part1 = availToWrite;
+    }
+    size_t part2 = part1 > 0 ? availToWrite - part1 : 0;
+    iovec[0].mOffset = rearMasked;
+    iovec[0].mLength = part1;
+    iovec[1].mOffset = 0;
+    iovec[1].mLength = part2;
+    mObtained = availToWrite;
+    return availToWrite;
+}
+
+void audio_utils_fifo_writer::release(size_t count)
+        __attribute__((no_sanitize("integer")))
+{
+    if (count > 0) {
+        LOG_ALWAYS_FATAL_IF(count > mObtained);
+        if (mFifo.mThrottleFront != NULL) {
+            uint32_t front = (uint32_t) atomic_load_explicit(mFifo.mThrottleFront,
+                    std::memory_order_acquire);
+            int32_t filled = mFifo.diff(mLocalRear, front, NULL /*lost*/);
+            mLocalRear = mFifo.sum(mLocalRear, count);
+            atomic_store_explicit(&mFifo.mSharedRear, (uint_fast32_t) mLocalRear,
+                    std::memory_order_release);
+            if (filled >= 0) {
+                if (filled + count <= mLowLevelArm) {
+                    mArmed = true;
+                }
+                if (mArmed && filled + count >= mHighLevelTrigger) {
+                    int err = sys_futex(&mFifo.mSharedRear,
+                            mFifo.mIsPrivate ? FUTEX_WAKE_PRIVATE : FUTEX_WAKE,
+                            INT_MAX /*waiters*/, NULL, NULL, 0);
+                    // err is number of processes woken up
+                    if (err < 0) {
+                        LOG_ALWAYS_FATAL("%s: unexpected err=%d errno=%d", __func__, err, errno);
+                    }
+                    mArmed = false;
+                }
+            }
+        } else {
+            mLocalRear = mFifo.sum(mLocalRear, count);
+            atomic_store_explicit(&mFifo.mSharedRear, (uint_fast32_t) mLocalRear,
+                    std::memory_order_release);
+        }
+        mObtained -= count;
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+audio_utils_fifo_reader::audio_utils_fifo_reader(audio_utils_fifo& fifo, bool throttlesWriter) :
+    audio_utils_fifo_provider(), mFifo(fifo), mLocalFront(0), mSharedFront(0),
+    mHighLevelArm(0), mLowLevelTrigger(mFifo.mFrameCount), mArmed(false)
+{
+    if (throttlesWriter) {
+        LOG_ALWAYS_FATAL_IF(fifo.mThrottleFront != NULL);
+        fifo.mThrottleFront = &mSharedFront;
+    }
+}
+
+audio_utils_fifo_reader::~audio_utils_fifo_reader()
+{
+    // TODO Need a way to pass throttle capability to the another reader, should one reader exit.
+    if (mFifo.mThrottleFront == &mSharedFront) {
+        mFifo.mThrottleFront = NULL;
+    }
+}
+
+ssize_t audio_utils_fifo_reader::read(void *buffer, size_t count, struct timespec *timeout,
+        size_t *lost)
+        __attribute__((no_sanitize("integer")))
+{
+    audio_utils_iovec iovec[2];
+    ssize_t availToRead = obtain(iovec, count, timeout, lost);
+    if (availToRead > 0) {
+        memcpy(buffer, (char *) mFifo.mBuffer + iovec[0].mOffset * mFifo.mFrameSize,
+                iovec[0].mLength * mFifo.mFrameSize);
+        if (iovec[1].mLength > 0) {
+            memcpy((char *) buffer + (iovec[0].mLength * mFifo.mFrameSize),
+                    (char *) mFifo.mBuffer + iovec[1].mOffset * mFifo.mFrameSize,
+                    iovec[1].mLength * mFifo.mFrameSize);
+        }
+        release(availToRead);
+    }
+    return availToRead;
+}
+
+ssize_t audio_utils_fifo_reader::obtain(audio_utils_iovec iovec[2], size_t count,
+        struct timespec *timeout)
+        __attribute__((no_sanitize("integer")))
+{
+    return obtain(iovec, count, timeout, NULL);
+}
+
+void audio_utils_fifo_reader::release(size_t count)
+        __attribute__((no_sanitize("integer")))
+{
+    if (count > 0) {
+        LOG_ALWAYS_FATAL_IF(count > mObtained);
+        if (mFifo.mThrottleFront == &mSharedFront) {
+            uint32_t rear = (uint32_t) atomic_load_explicit(&mFifo.mSharedRear,
+                    std::memory_order_acquire);
+            int32_t filled = mFifo.diff(rear, mLocalFront, NULL /*lost*/);
+            mLocalFront = mFifo.sum(mLocalFront, count);
+            atomic_store_explicit(&mSharedFront, (uint_fast32_t) mLocalFront,
+                    std::memory_order_release);
+            if (filled >= 0) {
+                if (filled - count >= mHighLevelArm) {
+                    mArmed = true;
+                }
+                if (mArmed && filled - count <= mLowLevelTrigger) {
+                    int err = sys_futex(&mFifo.mSharedRear,
+                            mFifo.mIsPrivate ? FUTEX_WAKE_PRIVATE : FUTEX_WAKE,
+                            1 /*waiters*/, NULL, NULL, 0);
+                    // err is number of processes woken up
+                    if (err < 0 || err > 1) {
+                        LOG_ALWAYS_FATAL("%s: unexpected err=%d errno=%d", __func__, err, errno);
+                    }
+                    mArmed = false;
+                }
+            }
+        } else {
+            mLocalFront = mFifo.sum(mLocalFront, count);
+        }
+        mObtained -= count;
+    }
+}
+
+ssize_t audio_utils_fifo_reader::obtain(audio_utils_iovec iovec[2], size_t count,
+        struct timespec *timeout, size_t *lost)
+        __attribute__((no_sanitize("integer")))
+{
+    uint32_t rear;
+    for (;;) {
+        rear = (uint32_t) atomic_load_explicit(&mFifo.mSharedRear,
+                std::memory_order_acquire);
+        // TODO pull out "count == 0"
+        if (count == 0 || rear != mLocalFront || timeout == NULL ||
+                (timeout->tv_sec == 0 && timeout->tv_nsec == 0)) {
+            break;
+        }
+        int err = sys_futex(&mFifo.mSharedRear, mFifo.mIsPrivate ? FUTEX_WAIT_PRIVATE : FUTEX_WAIT,
+                rear, timeout, NULL, 0);
+        if (err < 0) {
+            switch (errno) {
+            case EWOULDBLOCK:
+            case EINTR:
+            case ETIMEDOUT:
+                break;
+            default:
+                LOG_ALWAYS_FATAL("unexpected err=%d errno=%d", err, errno);
+                break;
+            }
+        }
+        timeout = NULL;
+    }
+    int32_t filled = mFifo.diff(rear, mLocalFront, lost);
+    if (filled < 0) {
+        if (filled == -EOVERFLOW) {
+            mLocalFront = rear;
+        }
+        mObtained = 0;
+        return (ssize_t) filled;
+    }
+    size_t availToRead = (size_t) filled;
+    if (availToRead > count) {
+        availToRead = count;
+    }
+    uint32_t frontMasked = mLocalFront & (mFifo.mFrameCountP2 - 1);
+    size_t part1 = mFifo.mFrameCount - frontMasked;
+    if (part1 > availToRead) {
+        part1 = availToRead;
+    }
+    size_t part2 = part1 > 0 ? availToRead - part1 : 0;
+    iovec[0].mOffset = frontMasked;
+    iovec[0].mLength = part1;
+    iovec[1].mOffset = 0;
+    iovec[1].mLength = part2;
+    mObtained = availToRead;
+    return availToRead;
+}
index f882368..c896b62 100644 (file)
 #ifndef ANDROID_AUDIO_FIFO_H
 #define ANDROID_AUDIO_FIFO_H
 
+#include <atomic>
 #include <stdlib.h>
 
-// FIXME use atomic_int_least32_t and new atomic operations instead of legacy Android ones
-// #include <stdatomic.h>
-
-#ifdef __cplusplus
-extern "C" {
+#ifndef __cplusplus
+#error C API is no longer supported
 #endif
 
-// Single writer, single reader non-blocking FIFO.
-// Writer and reader must be in same process.
+// Base class for single writer, single-reader or multi-reader non-blocking FIFO.
+// The base class manipulates frame indices only, and has no knowledge of frame sizes or the buffer.
+
+class audio_utils_fifo_base {
+
+protected:
+    audio_utils_fifo_base(uint32_t frameCount);
+    /*virtual*/ ~audio_utils_fifo_base();
+
+/** Return a new index as the sum of a validated index and a specified increment.
+ *
+ * \param index     Caller should supply a validated mFront or mRear.
+ * \param increment Value to be added to the index <= mFrameCount.
+ *
+ * \return the sum of index plus increment.
+ */
+    uint32_t sum(uint32_t index, uint32_t increment);
+
+/** Return the difference between two indices: rear - front.
+ *
+ * \param rear     Caller should supply an unvalidated mRear.
+ * \param front    Caller should supply an unvalidated mFront.
+ * \param lost     If non-NULL, set to the approximate number of lost frames.
+ *
+ * \return the zero or positive difference <= mFrameCount, or a negative error code.
+ */
+    int32_t diff(uint32_t rear, uint32_t front, size_t *lost);
 
-// No user-serviceable parts within.
-struct audio_utils_fifo {
     // These fields are const after initialization
-    size_t     mFrameCount;   // max number of significant frames to be stored in the FIFO > 0
-    size_t     mFrameCountP2; // roundup(mFrameCount)
-    size_t     mFudgeFactor;  // mFrameCountP2 - mFrameCount, the number of "wasted" frames after
-                              // the end of mBuffer.  Only the indices are wasted, not any memory.
-    size_t     mFrameSize;    // size of each frame in bytes
-    void      *mBuffer;       // pointer to caller-allocated buffer of size mFrameCount frames
-
-    volatile int32_t mFront; // frame index of first frame slot available to read, or read index
-    volatile int32_t mRear;  // frame index of next frame slot available to write, or write index
+    const uint32_t mFrameCount;   // max number of significant frames to be stored in the FIFO > 0
+    const uint32_t mFrameCountP2; // roundup(mFrameCount)
+    const uint32_t mFudgeFactor;  // mFrameCountP2 - mFrameCount, the number of "wasted" frames
+                                  // after the end of mBuffer.  Only the indices are wasted, not any
+                                  // memory.
+
+    // TODO always true for now, will be extended later to support false
+    const bool mIsPrivate;        // whether reader and writer virtual address spaces are the same
+
+    std::atomic_uint_fast32_t mSharedRear;  // accessed by both sides using atomic operations
+
+    // Pointer to the mSharedFront of at most one reader that throttles the writer,
+    // or NULL for no throttling
+    std::atomic_uint_fast32_t *mThrottleFront;
 };
 
+////////////////////////////////////////////////////////////////////////////////
+
+// Same as above, but understands frame sizes and knows about the buffer but does not own it.
+// Writer and reader must be in same process.
+
+class audio_utils_fifo : audio_utils_fifo_base {
+
+    friend class audio_utils_fifo_reader;
+    friend class audio_utils_fifo_writer;
+
+public:
+
 /**
- * Initialize a FIFO object.
+ * Construct a FIFO object.
  *
- *  \param fifo        Pointer to the FIFO object.
  *  \param frameCount  Max number of significant frames to be stored in the FIFO > 0.
  *                     If writes and reads always use the same count, and that count is a divisor of
  *                     frameCount, then the writes and reads will never do a partial transfer.
- *  \param frameSize   Size of each frame in bytes.
+ *  \param frameSize   Size of each frame in bytes > 0, and frameSize * frameCount <= INT_MAX.
  *  \param buffer      Pointer to a caller-allocated buffer of frameCount frames.
  */
-void audio_utils_fifo_init(struct audio_utils_fifo *fifo, size_t frameCount, size_t frameSize,
-        void *buffer);
+    audio_utils_fifo(uint32_t frameCount, uint32_t frameSize, void *buffer);
 
-/**
- * De-initialize a FIFO object.
- *
- *  \param fifo        Pointer to the FIFO object.
- */
-void audio_utils_fifo_deinit(struct audio_utils_fifo *fifo);
+    /*virtual*/ ~audio_utils_fifo();
+
+private:
+
+    // These fields are const after initialization
+    const uint32_t mFrameSize;    // size of each frame in bytes
+    void * const   mBuffer;       // pointer to caller-allocated buffer of size mFrameCount frames
+};
+
+// Describes one virtually contiguous fragment of a logically contiguous slice.
+// Compare to struct iovec for readv(2) and writev(2).
+struct audio_utils_iovec {
+    uint32_t    mOffset;    // in frames, relative to mBuffer, undefined if mLength == 0
+    uint32_t    mLength;    // in frames
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Based on frameworks/av/include/media/AudioBufferProvider.h
+class audio_utils_fifo_provider {
+public:
+    audio_utils_fifo_provider();
+    virtual ~audio_utils_fifo_provider();
+
+// The count is the maximum number of desired frames, not the minimum number of desired frames.
+// See the high/low setpoints for something which is close to, but not the same as, a true minimum.
+
+// The timeout indicates the maximum time to wait for at least one frame, not for all frames.
+// NULL is equivalent to non-blocking.
+
+// Error codes for ssize_t return value:
+//  -EIO        corrupted indices (reader or writer)
+//  -EOVERFLOW  reader is not keeping up with writer (reader only)
+    virtual ssize_t obtain(audio_utils_iovec iovec[2], size_t count, struct timespec *timeout) = 0;
+
+    virtual void release(size_t count) = 0;
+
+protected:
+    // Number of frames obtained at most recent obtain(), less number of frames released
+    uint32_t    mObtained;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class audio_utils_fifo_writer : public audio_utils_fifo_provider {
+
+public:
+    audio_utils_fifo_writer(audio_utils_fifo& fifo);
+    virtual ~audio_utils_fifo_writer();
 
 /**
  * Write to FIFO.
  *
- *  \param fifo        Pointer to the FIFO object.
- *  \param buffer      Pointer to source buffer containing 'count' frames of data.
- *  \param count       Desired number of frames to write.
+ * \param buffer    Pointer to source buffer containing 'count' frames of data.
+ * \param count     Desired number of frames to write.
+ * \param timeout   NULL and zero fields are both non-blocking.
  *
  * \return actual number of frames written <= count.
  *
  * The actual transfer count may be zero if the FIFO is full,
  * or partial if the FIFO was almost full.
- * A negative return value indicates an error.  Currently there are no errors defined.
+ * A negative return value indicates an error.
  */
-ssize_t audio_utils_fifo_write(struct audio_utils_fifo *fifo, const void *buffer, size_t count);
+    ssize_t write(const void *buffer, size_t count, struct timespec *timeout = NULL);
+
+    // Implement audio_utils_fifo_provider
+    virtual ssize_t obtain(audio_utils_iovec iovec[2], size_t count, struct timespec *timeout);
+    virtual void release(size_t count);
+
+    // TODO add error checks and getters
+    void setHighLevelTrigger(uint32_t level) { mHighLevelTrigger = level; }
+    void setEffectiveFrames(uint32_t effectiveFrames) { mEffectiveFrames = effectiveFrames; }
+
+private:
+    audio_utils_fifo&   mFifo;
+
+    // Accessed by writer only using ordinary operations
+    uint32_t    mLocalRear; // frame index of next frame slot available to write, or write index
+
+    uint32_t    mLowLevelArm;       // arm if filled <= threshold
+    uint32_t    mHighLevelTrigger;  // trigger reader if armed and filled >= threshold
+    bool        mArmed;
+
+    uint32_t    mEffectiveFrames;   // current effective buffer size, <= mFifo.mFrameCount
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class audio_utils_fifo_reader : public audio_utils_fifo_provider {
+
+public:
+    audio_utils_fifo_reader(audio_utils_fifo& fifo, bool throttlesWriter);
+    virtual ~audio_utils_fifo_reader();
 
 /** Read from FIFO.
  *
- *  \param fifo        Pointer to the FIFO object.
- *  \param buffer      Pointer to destination buffer to be filled with up to 'count' frames of data.
- *  \param count       Desired number of frames to read.
+ * \param buffer    Pointer to destination buffer to be filled with up to 'count' frames of data.
+ * \param count     Desired number of frames to read.
+ * \param timeout   NULL and zero fields are both non-blocking.
+ * \param lost      If non-NULL, set to the approximate number of lost frames before re-sync.
  *
  * \return actual number of frames read <= count.
  *
  * The actual transfer count may be zero if the FIFO is empty,
  * or partial if the FIFO was almost empty.
- * A negative return value indicates an error.  Currently there are no errors defined.
+ * A negative return value indicates an error.
  */
-ssize_t audio_utils_fifo_read(struct audio_utils_fifo *fifo, void *buffer, size_t count);
+    ssize_t read(void *buffer, size_t count, struct timespec *timeout = NULL, size_t *lost = NULL);
 
-#ifdef __cplusplus
-}
-#endif
+    // Implement audio_utils_fifo_provider
+    virtual ssize_t obtain(audio_utils_iovec iovec[2], size_t count, struct timespec *timeout);
+    virtual void release(size_t count);
+
+    // Extended parameter list for reader only
+    ssize_t obtain(audio_utils_iovec iovec[2], size_t count, struct timespec *timeout,
+            size_t *lost);
+
+private:
+    audio_utils_fifo&   mFifo;
+
+    // Accessed by reader only using ordinary operations
+    uint32_t     mLocalFront;   // frame index of first frame slot available to read, or read index
+
+    // Accessed by a throttling reader and writer using atomic operations
+    std::atomic_uint_fast32_t mSharedFront;
+
+    uint32_t    mHighLevelArm;      // arm if filled >= threshold
+    uint32_t    mLowLevelTrigger;   // trigger writer if armed and filled <= threshold
+    bool        mArmed;
+};
 
 #endif  // !ANDROID_AUDIO_FIFO_H
index cd76cf0..7f9dd43 100644 (file)
@@ -51,9 +51,18 @@ LOCAL_CFLAGS := -Werror -Wall
 include $(BUILD_HOST_EXECUTABLE)
 
 include $(CLEAR_VARS)
+# TODO move getch.c and .h to a utility library
+LOCAL_SRC_FILES := fifo_threads.cpp getch.c
+LOCAL_MODULE := fifo_threads
+LOCAL_C_INCLUDES := $(call include-path-for, audio-utils)
+LOCAL_STATIC_LIBRARIES := libaudioutils liblog
+LOCAL_CFLAGS := -Werror -Wall
+include $(BUILD_HOST_EXECUTABLE)
+
+include $(CLEAR_VARS)
 LOCAL_SRC_FILES := limiter_tests.c
 LOCAL_MODULE := limiter_tests
 LOCAL_C_INCLUDES := $(call include-path-for, audio-utils)
 LOCAL_STATIC_LIBRARIES := libaudioutils
-LOCAL_CFLAGS := -Werror -Wall
+LOCAL_CFLAGS := -Werror -Wall -UNDEBUG
 include $(BUILD_HOST_EXECUTABLE)
index 99e73c9..f4fc1d3 100644 (file)
 // Test program for audio_utils FIFO library.
 // This only tests the single-threaded aspects, not the barriers.
 
+#include <errno.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <string.h>
 #include <audio_utils/fifo.h>
 #include <audio_utils/sndfile.h>
 
+#ifndef min
+#define min(x, y) (((x) < (y)) ? (x) : (y))
+#endif
+
 int main(int argc, char **argv)
 {
-    size_t frameCount = 256;
-    size_t maxFramesPerRead = 1;
-    size_t maxFramesPerWrite = 1;
+    size_t frameCount = 0;
+    size_t maxFramesPerRead = 0;
+    size_t maxFramesPerWrite = 0;
+    bool readerThrottlesWriter = true;
     int i;
     for (i = 1; i < argc; i++) {
         char *arg = argv[i];
         if (arg[0] != '-')
             break;
         switch (arg[1]) {
-        case 'c':   // FIFO frame count
+        case 'f':   // FIFO frame count
             frameCount = atoi(&arg[2]);
             break;
         case 'r':   // maximum frame count per read from FIFO
             maxFramesPerRead = atoi(&arg[2]);
             break;
+        case 't':   // disable throttling of writer by reader
+            readerThrottlesWriter = false;
+            break;
         case 'w':   // maximum frame count per write to FIFO
             maxFramesPerWrite = atoi(&arg[2]);
             break;
@@ -48,10 +57,19 @@ int main(int argc, char **argv)
             goto usage;
         }
     }
+    if (frameCount == 0) {
+        frameCount = 256;
+    }
+    if (maxFramesPerRead == 0) {
+        maxFramesPerRead = frameCount;
+    }
+    if (maxFramesPerWrite == 0) {
+        maxFramesPerWrite = frameCount;
+    }
 
     if (argc - i != 2) {
 usage:
-        fprintf(stderr, "usage: %s [-c#] in.wav out.wav\n", argv[0]);
+        fprintf(stderr, "usage: %s [-f#] [-r#] [-t] [-w#] in.wav out.wav\n", argv[0]);
         return EXIT_FAILURE;
     }
     char *inputFile = argv[i];
@@ -64,12 +82,11 @@ usage:
         perror(inputFile);
         return EXIT_FAILURE;
     }
-    // sf_readf_short() does conversion, so not strictly necessary to check the file format.
-    // But I want to do "cmp" on input and output files afterwards,
-    // and it is easier if they are all the same format.
-    // Enforcing that everything is 16-bit is convenient for this.
-    if ((sfinfoin.format & (SF_FORMAT_TYPEMASK | SF_FORMAT_SUBMASK)) !=
-            (SF_FORMAT_WAV | SF_FORMAT_PCM_16)) {
+    switch (sfinfoin.format & (SF_FORMAT_TYPEMASK | SF_FORMAT_SUBMASK)) {
+    case SF_FORMAT_WAV | SF_FORMAT_PCM_16:
+    case SF_FORMAT_WAV | SF_FORMAT_PCM_U8:
+        break;
+    default:
         fprintf(stderr, "%s: unsupported format\n", inputFile);
         sf_close(sfin);
         return EXIT_FAILURE;
@@ -87,15 +104,16 @@ usage:
     short *outputBuffer = new short[sfinfoin.frames * sfinfoin.channels];
     size_t framesWritten = 0;
     size_t framesRead = 0;
-    struct audio_utils_fifo fifo;
     short *fifoBuffer = new short[frameCount * sfinfoin.channels];
-    audio_utils_fifo_init(&fifo, frameCount, frameSize, fifoBuffer);
+    audio_utils_fifo fifo(frameCount, frameSize, fifoBuffer);
+    audio_utils_fifo_writer fifoWriter(fifo);
+    audio_utils_fifo_reader fifoReader(fifo, readerThrottlesWriter);
     int fifoWriteCount = 0, fifoReadCount = 0;
     int fifoFillLevel = 0, minFillLevel = INT_MAX, maxFillLevel = INT_MIN;
     for (;;) {
         size_t framesToWrite = sfinfoin.frames - framesWritten;
         size_t framesToRead = sfinfoin.frames - framesRead;
-        if (framesToWrite == 0 && framesToRead == 0) {
+        if (framesToWrite == 0 && (framesToRead == 0 || !readerThrottlesWriter)) {
             break;
         }
 
@@ -103,12 +121,17 @@ usage:
             framesToWrite = maxFramesPerWrite;
         }
         framesToWrite = rand() % (framesToWrite + 1);
-        ssize_t actualWritten = audio_utils_fifo_write(&fifo,
+        ssize_t actualWritten = fifoWriter.write(
                 &inputBuffer[framesWritten * sfinfoin.channels], framesToWrite);
+        //printf("wrote %d out of %d\n", (int) actualWritten, (int) framesToWrite);
         if (actualWritten < 0 || (size_t) actualWritten > framesToWrite) {
             fprintf(stderr, "write to FIFO failed\n");
             break;
         }
+        if (actualWritten < min((int) frameCount - fifoFillLevel, (int) framesToWrite)) {
+            fprintf(stderr, "only wrote %d when should have written min(%d, %d)\n",
+                    (int) actualWritten, (int) frameCount - fifoFillLevel, (int) framesToWrite);
+        }
         framesWritten += actualWritten;
         if (actualWritten > 0) {
             fifoWriteCount++;
@@ -116,19 +139,49 @@ usage:
         fifoFillLevel += actualWritten;
         if (fifoFillLevel > maxFillLevel) {
             maxFillLevel = fifoFillLevel;
-            if (maxFillLevel > (int) frameCount)
-                abort();
+            if (maxFillLevel > (int) frameCount) {
+                if (readerThrottlesWriter) {
+                    printf("maxFillLevel=%d > frameCount=%d\n", maxFillLevel, (int) frameCount);
+                    abort();
+                }
+            }
         }
 
         if (framesToRead > maxFramesPerRead) {
             framesToRead = maxFramesPerRead;
         }
         framesToRead = rand() % (framesToRead + 1);
-        ssize_t actualRead = audio_utils_fifo_read(&fifo,
+        ssize_t actualRead = fifoReader.read(
                 &outputBuffer[framesRead * sfinfoin.channels], framesToRead);
+        //printf("read %d out of %d\n", (int) actualRead, (int) framesToRead);
         if (actualRead < 0 || (size_t) actualRead > framesToRead) {
-            fprintf(stderr, "read from FIFO failed\n");
-            break;
+            switch (actualRead) {
+            case -EIO:
+                fprintf(stderr, "read from FIFO failed: corrupted indices\n");
+                abort();
+                break;
+            case -EOVERFLOW:
+                if (readerThrottlesWriter) {
+                    fprintf(stderr, "read from FIFO failed: unexpected overflow\n");
+                    abort();
+                }
+                printf("warning: reader lost frames\n");
+                actualRead = 0;
+                break;
+            default:
+                if (actualRead < 0) {
+                    fprintf(stderr, "read from FIFO failed: unexpected error code %d\n",
+                            (int) actualRead);
+                } else {
+                    fprintf(stderr, "read from FIFO failed: actualRead=%d > framesToRead=%d\n",
+                            (int) actualRead, (int) framesToRead);
+                }
+                abort();
+            }
+        }
+        if (actualRead < min(fifoFillLevel, (int) framesToRead)) {
+            //fprintf(stderr, "only read %d when should have read min(%d, %d)\n",
+            //        (int) actualRead, fifoFillLevel, (int) framesToRead);
         }
         framesRead += actualRead;
         if (actualRead > 0) {
@@ -137,14 +190,19 @@ usage:
         fifoFillLevel -= actualRead;
         if (fifoFillLevel < minFillLevel) {
             minFillLevel = fifoFillLevel;
-            if (minFillLevel < 0)
+            if (minFillLevel < 0) {
+                printf("minFillLevel=%d < 0\n", minFillLevel);
                 abort();
+            }
         }
     }
+    delete[] inputBuffer;
+    inputBuffer = NULL;
+    delete[] fifoBuffer;
+    fifoBuffer = NULL;
+
     printf("FIFO non-empty writes: %d, non-empty reads: %d\n", fifoWriteCount, fifoReadCount);
     printf("fill=%d, min=%d, max=%d\n", fifoFillLevel, minFillLevel, maxFillLevel);
-    audio_utils_fifo_deinit(&fifo);
-    delete[] fifoBuffer;
 
     SF_INFO sfinfoout;
     memset(&sfinfoout, 0, sizeof(sfinfoout));
@@ -157,9 +215,9 @@ usage:
         return EXIT_FAILURE;
     }
     sf_count_t actualWritten = sf_writef_short(sfout, outputBuffer, framesRead);
-    delete[] inputBuffer;
     delete[] outputBuffer;
-    delete[] fifoBuffer;
+    outputBuffer = NULL;
+
     if (actualWritten != (sf_count_t) framesRead) {
         fprintf(stderr, "%s: unexpected error\n", outputFile);
         sf_close(sfout);
diff --git a/audio_utils/tests/fifo_threads.cpp b/audio_utils/tests/fifo_threads.cpp
new file mode 100644 (file)
index 0000000..92a2eae
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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 <pthread.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <audio_utils/fifo.h>
+extern "C" {
+#include "getch.h"
+}
+
+struct Context {
+    audio_utils_fifo_writer *mInputWriter;
+    audio_utils_fifo_reader *mInputReader;
+    audio_utils_fifo_writer *mTransferWriter;
+    audio_utils_fifo_reader *mTransferReader;
+    audio_utils_fifo_writer *mOutputWriter;
+    audio_utils_fifo_reader *mOutputReader;
+};
+
+void *input_routine(void *arg)
+{
+    Context *context = (Context *) arg;
+    for (;;) {
+        struct timespec timeout;
+        timeout.tv_sec = 3;
+        timeout.tv_nsec = 0;
+        char buffer[4];
+        ssize_t actual = context->mInputReader->read(buffer, sizeof(buffer), &timeout);
+        if (actual == 0) {
+            (void) write(1, "t", 1);
+        } else if (actual > 0) {
+            actual = context->mTransferWriter->write(buffer, actual, &timeout);
+            //printf("transfer.write actual = %d\n", (int) actual);
+        } else {
+            printf("input.read actual = %d\n", (int) actual);
+        }
+    }
+    return NULL;
+}
+
+void *output_routine(void *arg)
+{
+    Context *context = (Context *) arg;
+    for (;;) {
+        struct timespec timeout;
+        timeout.tv_sec = 5;
+        timeout.tv_nsec = 0;
+        char buffer[4];
+        ssize_t actual = context->mTransferReader->read(buffer, sizeof(buffer), &timeout);
+        if (actual == 0) {
+            (void) write(1, "T", 1);
+        } else if (actual > 0) {
+            actual = context->mOutputWriter->write(buffer, actual, &timeout);
+            //printf("output.write actual = %d\n", (int) actual);
+        } else {
+            printf("transfer.read actual = %d\n", (int) actual);
+        }
+    }
+    return NULL;
+}
+
+int main(int argc, char **argv)
+{
+    set_conio_terminal_mode();
+    argc = argc + 0;
+    argv = &argv[0];
+
+    char inputBuffer[64];
+    audio_utils_fifo inputFifo(sizeof(inputBuffer) /*frameCount*/, 1 /*frameSize*/, inputBuffer);
+    audio_utils_fifo_writer inputWriter(inputFifo);
+    audio_utils_fifo_reader inputReader(inputFifo, true /*readerThrottlesWriter*/);
+    inputWriter.setHighLevelTrigger(3);
+
+    char transferBuffer[64];
+    audio_utils_fifo transferFifo(sizeof(transferBuffer) /*frameCount*/, 1 /*frameSize*/,
+            transferBuffer);
+    audio_utils_fifo_writer transferWriter(transferFifo);
+    audio_utils_fifo_reader transferReader(transferFifo, true /*readerThrottlesWriter*/);
+    transferWriter.setEffectiveFrames(2);
+
+    char outputBuffer[64];
+    audio_utils_fifo outputFifo(sizeof(outputBuffer) /*frameCount*/, 1 /*frameSize*/, outputBuffer);
+    audio_utils_fifo_writer outputWriter(outputFifo);
+    audio_utils_fifo_reader outputReader(outputFifo, true /*readerThrottlesWriter*/);
+
+    Context context;
+    context.mInputWriter = &inputWriter;
+    context.mInputReader = &inputReader;
+    context.mTransferWriter = &transferWriter;
+    context.mTransferReader = &transferReader;
+    context.mOutputWriter = &outputWriter;
+    context.mOutputReader = &outputReader;
+
+    pthread_t input_thread;
+    int ok = pthread_create(&input_thread, (const pthread_attr_t *) NULL, input_routine,
+            (void *) &context);
+    pthread_t output_thread;
+    ok = pthread_create(&output_thread, (const pthread_attr_t *) NULL, output_routine,
+            (void *) &context);
+    ok = ok + 0;
+
+    for (;;) {
+        char buffer[1];
+        struct timespec timeout;
+        timeout.tv_sec = 0;
+        timeout.tv_nsec = 0;
+        ssize_t actual = outputReader.read(buffer, sizeof(buffer), &timeout);
+        if (actual == 1) {
+            printf("%c", buffer[0]);
+            fflush(stdout);
+        } else if (actual != 0) {
+            printf("outputReader.read actual = %d\n", (int) actual);
+        }
+        if (kbhit()) {
+            int ch = getch();
+            if (ch <= 0 || ch == 3) {
+                break;
+            }
+            buffer[0] = ch;
+            actual = inputWriter.write(buffer, sizeof(buffer), &timeout);
+            if (actual != 1) {
+                printf("inputWriter.write actual = %d\n", (int) actual);
+            }
+        }
+    }
+    reset_terminal_mode();
+}
diff --git a/audio_utils/tests/getch.c b/audio_utils/tests/getch.c
new file mode 120000 (symlink)
index 0000000..472d85b
--- /dev/null
@@ -0,0 +1 @@
+../../../../frameworks/wilhelm/tests/sandbox/getch.c
\ No newline at end of file
diff --git a/audio_utils/tests/getch.h b/audio_utils/tests/getch.h
new file mode 120000 (symlink)
index 0000000..094c1b0
--- /dev/null
@@ -0,0 +1 @@
+../../../../frameworks/wilhelm/tests/sandbox/getch.h
\ No newline at end of file