Merge SP1A.210208.001
Change-Id: Ie8632686716c12045e9109964dce7799aab447e9
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8773c34
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+/doxygen/html
+/doxygen/man
+
+/build
+
+*.o
+*.so
+*.so.*
+*.a
+
+/libs
+/obj
+
+/examples/audio.raw
+/examples/pcm-readi
+/examples/pcm-writei
+
+/utils/tinyplay
+/utils/tinycap
+/utils/tinymix
+/utils/tinypcminfo
+
+/bazel*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..28667cb
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+os: linux
+dist: xenial
+sudo: false
+language: c
+compiler:
+        - gcc
+        - clang
+
+addons:
+    apt:
+        packages:
+            - doxygen
+            - graphviz
+            - python3-pip
+            - python3-setuptools
+
+before_script:
+    - pip3 install --upgrade pip
+    - pip3 install --user ninja
+    - pip3 install --user meson
+
+script:
+    - scripts/travis-build.sh
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..51025b8
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,68 @@
+cc_library {
+    name: "libtinyalsav2",
+    host_supported: true,
+    vendor_available: true,
+    srcs: [
+        "src/mixer.c",
+        "src/mixer_hw.c",
+        "src/mixer_plugin.c",
+        "src/pcm.c",
+        "src/pcm_hw.c",
+        "src/pcm_plugin.c",
+        "src/snd_card_plugin.c",
+    ],
+    cflags: ["-Werror", "-Wno-macro-redefined"],
+    export_include_dirs: ["include"],
+    local_include_dirs: ["include"],
+
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+
+    system_shared_libs: ["libc", "libdl"],
+
+    sanitize: {
+        integer_overflow: true,
+        misc_undefined: ["bounds"],
+        diag: {
+            integer_overflow: true,
+            misc_undefined: ["bounds"],
+        },
+    },
+}
+
+cc_binary {
+    name: "tinyplay2",
+    host_supported: true,
+    srcs: ["utils/tinyplay.c"],
+    shared_libs: ["libtinyalsav2"],
+    cflags: ["-Werror"],
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+}
+
+cc_binary {
+    name: "tinycap2",
+    srcs: ["utils/tinycap.c"],
+    shared_libs: ["libtinyalsav2"],
+    cflags: ["-Werror"],
+}
+
+cc_binary {
+    name: "tinymix2",
+    srcs: ["utils/tinymix.c"],
+    shared_libs: ["libtinyalsav2"],
+    cflags: ["-Werror", "-Wall"],
+}
+
+cc_binary {
+    name: "tinypcminfo2",
+    srcs: ["utils/tinypcminfo.c"],
+    shared_libs: ["libtinyalsav2"],
+    cflags: ["-Werror"],
+}
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..dba4ab5
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,57 @@
+#  BUILD
+#
+#  Copyright 2020, The Android Open Source Project
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#    * Redistributions of source code must retain the above copyright
+#      notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#    * Neither the name of The Android Open Source Project nor the names of
+#      its contributors may be used to endorse or promote products derived
+#      from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+#  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+#  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+#  DAMAGE.
+#
+
+cc_library(
+    name = "tinyalsa",
+    srcs = glob(["src/*.c"]),
+    includes = ["include"],
+    hdrs = glob([
+        "include/**/*.h",
+        "src/*.h",
+    ]),
+    visibility = ["//visibility:public"],
+)
+
+cc_test(
+    name = "tinyalsa_tests",
+    srcs = glob([
+        "tests/src/*.cc",
+        "tests/include/*.h",
+    ]),
+    includes = ["tests/include"],
+    deps = [
+        "//:tinyalsa",
+        "@googletest//:gtest_main"
+    ],
+    linkopts = [
+        "-ldl",
+    ],
+    copts = [
+        "-std=c++17",
+    ],
+)
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..84de10b
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,97 @@
+cmake_minimum_required(VERSION 3.1)
+
+execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/version.sh -s print
+    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+    OUTPUT_VARIABLE TINYALSA_VERSION)
+
+project("TinyALSA" VERSION ${TINYALSA_VERSION} LANGUAGES C)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_C_EXTENSIONS OFF)
+
+# Options
+option(BUILD_SHARED_LIBS "Build shared libraries" ON)
+option(TINYALSA_USES_PLUGINS "Whether or not to build with plugin support" OFF)
+option(TINYALSA_BUILD_EXAMPLES "Build examples" ON)
+option(TINYALSA_BUILD_UTILS "Build utility tools" ON)
+
+# Library
+add_library("tinyalsa"
+    "src/pcm.c"
+    "src/pcm_hw.c"
+    "src/pcm_plugin.c"
+    "src/snd_card_plugin.c"
+    "src/mixer.c"
+    "src/mixer_hw.c"
+    "src/mixer_plugin.c")
+
+set_property(TARGET "tinyalsa" PROPERTY PUBLIC_HEADER
+    "include/tinyalsa/attributes.h"
+    "include/tinyalsa/version.h"
+    "include/tinyalsa/asoundlib.h"
+    "include/tinyalsa/pcm.h"
+    "include/tinyalsa/plugin.h"
+    "include/tinyalsa/mixer.h")
+
+set_target_properties("tinyalsa" PROPERTIES
+    VERSION ${TinyALSA_VERSION}
+    SOVERSION ${TinyALSA_VERSION_MAJOR})
+
+target_include_directories("tinyalsa" PUBLIC
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+    $<INSTALL_INTERFACE:include>)
+target_compile_definitions("tinyalsa" PRIVATE
+    $<$<BOOL:${TINYALSA_USES_PLUGINS}>:TINYALSA_USES_PLUGINS>
+    PUBLIC _POSIX_C_SOURCE=200809L)
+target_link_libraries("tinyalsa" PUBLIC ${CMAKE_DL_LIBS})
+
+# Examples
+if(TINYALSA_BUILD_EXAMPLES)
+    set(TINYALSA_EXAMPLES pcm-readi pcm-writei)
+else()
+    set(TINYALSA_EXAMPLES)
+endif()
+
+foreach(EXAMPLE IN LISTS TINYALSA_EXAMPLES)
+    add_executable("${EXAMPLE}" "examples/${EXAMPLE}.c")
+    target_link_libraries("${EXAMPLE}" PRIVATE "tinyalsa")
+endforeach()
+
+# Utilities
+if(TINYALSA_BUILD_UTILS)
+    set(TINYALSA_UTILS tinyplay tinycap tinypcminfo tinymix tinywavinfo)
+else()
+    set(TINYALSA_UTILS)
+endif()
+
+foreach(UTIL IN LISTS TINYALSA_UTILS)
+    add_executable("${UTIL}" "utils/${UTIL}.c")
+    target_link_libraries("${UTIL}" PRIVATE "tinyalsa")
+endforeach()
+
+if(TINYALSA_BUILD_UTILS)
+    target_link_libraries("tinywavinfo" PRIVATE m)
+endif()
+
+# Add C warning flags
+include(CheckCCompilerFlag)
+foreach(FLAG IN ITEMS -Wall -Wextra -Wpedantic -Werror -Wfatal-errors)
+    string(TOUPPER "HAVE${FLAG}" HAVE_VAR)
+    string(REPLACE "-" "_" HAVE_VAR "${HAVE_VAR}")
+    check_c_compiler_flag("${FLAG}" "${HAVE_VAR}")
+    if("${${HAVE_VAR}}")
+        target_compile_options("tinyalsa" PRIVATE "${FLAG}")
+        foreach(UTIL IN LISTS TINYALSA_UTILS)
+            target_compile_options("${UTIL}" PRIVATE "${FLAG}")
+        endforeach()
+    endif()
+endforeach()
+
+# Install
+include(GNUInstallDirs)
+install(TARGETS "tinyalsa" ${TINYALSA_UTILS}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tinyalsa)
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..132d13f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+NOTICE
\ No newline at end of file
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..d97975c
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,3 @@
+third_party {
+  license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_BSD b/MODULE_LICENSE_BSD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_BSD
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5fabb31
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,40 @@
+export DESTDIR ?=
+export PREFIX ?= /usr/local
+
+export INCDIR ?= $(PREFIX)/include/tinyalsa
+export LIBDIR ?= $(PREFIX)/lib
+export BINDIR ?= $(PREFIX)/bin
+export MANDIR ?= $(PREFIX)/share/man
+
+export VERSIONSCRIPT = $(shell pwd)/scripts/version.sh
+
+export TINYALSA_VERSION_MAJOR = $(shell $(VERSIONSCRIPT) -s print major)
+export TINYALSA_VERSION       = $(shell $(VERSIONSCRIPT) -s print      )
+
+.PHONY: all
+all:
+	$(MAKE) -C src
+	$(MAKE) -C utils
+	$(MAKE) -C doxygen
+	$(MAKE) -C examples
+
+.PHONY: clean
+clean:
+	$(MAKE) -C src clean
+	$(MAKE) -C utils clean
+	$(MAKE) -C doxygen clean
+	$(MAKE) -C examples clean
+
+.PHONY: install
+install:
+	install -d $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/attributes.h $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/pcm.h $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/mixer.h $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/asoundlib.h $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/version.h $(DESTDIR)$(INCDIR)/
+	install include/tinyalsa/plugin.h $(DESTDIR)$(INCDIR)/
+	$(MAKE) -C src install
+	$(MAKE) -C utils install
+	$(MAKE) -C doxygen install
+
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..5debd99
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,25 @@
+Copyright 2011, The Android Open Source Project
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of The Android Open Source Project nor the names of
+      its contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..e1e3676
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+dvdli@google.com
+gkasten@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ed4203a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,103 @@
+TinyALSA
+========
+
+[![Build Status](https://travis-ci.org/tinyalsa/tinyalsa.svg?branch=master)](https://travis-ci.org/tinyalsa/tinyalsa)
+
+TinyALSA is a small library to interface with ALSA in the Linux kernel.
+
+The aims are:
+
+ - Provide a basic pcm and mixer API.
+ - If it's not absolutely needed, don't add it to the API.
+ - Avoid supporting complex and unnecessary operations, that could be
+   dealt with at a higher level.
+ - Provide comprehensive documentation.
+
+### Building
+
+TinyALSA supports these build systems:
+
+ - [CMake](https://en.wikipedia.org/wiki/CMake)
+ - [Make](https://en.wikipedia.org/wiki/Make_(software))
+ - [Meson](https://en.wikipedia.org/wiki/Meson_(software))
+ - [Soong](https://android.googlesource.com/platform/build/soong/+/refs/heads/master/README.md) for Android
+
+To build and install with Make, run the commands:
+
+```
+make
+sudo make install
+sudo ldconfig
+```
+
+### Installing
+
+TinyALSA is now available as a set of the following [Debian](https://en.wikipedia.org/wiki/Debian)
+packages from [launchpad](https://launchpad.net/~taylorcholberton/+archive/ubuntu/tinyalsa):
+
+| Package Name:   | Description:                                        |
+|-----------------|-----------------------------------------------------|
+| tinyalsa        | Contains tinyplay, tinycap, tinymix and tinypcminfo |
+| libtinyalsa     | Contains the shared library                         |
+| libtinyalsa-dev | Contains the static library and header files        |
+
+To install these packages, run the commands:
+
+```
+sudo apt-add-repository ppa:taylorcholberton/tinyalsa
+sudo apt-get update
+sudo apt-get install tinyalsa
+sudo apt-get install libtinyalsa-dev
+```
+
+### Documentation
+
+Once installed, the man pages are available via:
+
+```
+man tinyplay
+man tinycap
+man tinymix
+man tinypcminfo
+man libtinyalsa-pcm
+man libtinyalsa-mixer
+```
+
+### Test
+
+To test libtinyalsa, please follow the instructions,
+
+#### Setup Bazel build environment
+
+Visit [here](https://docs.bazel.build/versions/3.7.0/install.html) to get more info to setup Bazel environment.
+
+#### Insert loopback devices
+
+The test program does pcm_* operations on loopback devices. You have to insert loopback devices after your system boots up.
+
+```
+sudo modprobe snd-aloop
+sudo chmod 777 /dev/snd/*
+```
+
+#### Run test program
+
+```
+bazel test //:tinyalsa_tests --test_output=all
+```
+
+The default playback device is hw:2,0 and the default capture device is hw:2,1. If your loopback devices are not hw:2,0 and hw:2,1, you can specify the loopback device.
+
+```
+bazel test //:tinyalsa_tests --test_output=all \
+    --copt=-DTEST_LOOPBACK_CARD=[loopback card] \
+    --copt=-DTEST_LOOPBACK_PLAYBACK_DEVICE=[loopback playback device] \
+    --copt=-DTEST_LOOPBACK_CAPTURE_DEVICE=[loopback capture device]
+```
+
+#### Generate coverage report
+
+```
+bazel coverage //:tinyalsa_tests --combined_report=lcov --test_output=all
+genhtml bazel-out/_coverage/_coverage_report.dat -o tinyalsa_tests_coverage
+```
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..02b57bf
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,7 @@
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+
+git_repository(
+    name = "googletest",
+    remote = "https://github.com/google/googletest",
+    branch = "master",
+)
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..637ce56
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,54 @@
+tinyalsa (2.0.0) xenial; urgency=medium
+
+ * Miscellaneous bugs fixed.
+ * PCM plugin support.
+ * Add CMake build support.
+ * Add meson build support.
+ * tinyplay can now read from stdin.
+ * Improved versioning support for library.
+ * Improvements to pcm actions (prepare at open time and after overrun, etc.)
+ * Improvements/fixes to pcm_get_htimestamp().
+ * Fixes for the mixer percent functions.
+
+ -- Simon Wilson <ksattic@gmail.com>  Mon, 21 Sep 2020 11:27:00 -0600
+
+tinyalsa (1.1.1) xenial; urgency=medium
+
+  * Fixed some minor bugs
+  * Added some protection from integer overflow
+
+ -- Taylor Holberton <taylorcholberton@gmail.com>  Tue, 23 May 2017 21:21:10 -0800
+
+tinyalsa (1.1.0) xenial; urgency=medium
+
+  * Finished most of the PCM and mixer API documentation
+  * Added const specifiers where necessary
+  * Added pcm_readi() and pcm_writei()
+  * Deprecated pcm_read() and pcm_write()
+  * Added mixer_get_num_ctls_by_name()
+  * Added pcm_get_channels(), pcm_get_rate() and pcm_get_format()
+  * Made libtinyalsa.so.x a symbol link, using libtinyalsa.so.x.y.z as library name
+  * Added long option names in tinyplay
+  * Using amixer-style interface for tinymix
+
+ -- Taylor Holberton <taylorcholberton@gmail.com>  Thu, 01 Dec 2016 21:38:12 -0800
+
+tinyalsa (1.0.2) xenial; urgency=medium
+
+  * Removed install of libtinyalsa.so in package libtinyalsa
+
+ -- Taylor Holberton <taylorcholberton@gmail.com>  Sun, 02 Oct 2016 13:46:33 -0400
+
+tinyalsa (1.0.1) xenial; urgency=medium
+
+  * Added man pages for tinycap, tinyplay, tinymix and tinypcminfo.
+  * Fixed overwrite of shared library in package libtinyalsa-dev
+
+ -- Taylor Holberton <taylorcholberton@gmail.com>  Sun, 02 Oct 2016 12:24:28 -0400
+
+tinyalsa (1.0.0) xenial; urgency=medium
+
+  * Initial debian release.
+
+ -- Taylor Holberton <taylorcholberton@gmail.com>  Sat, 01 Oct 2016 20:31:04 -0400
+
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..66ca6f5
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,46 @@
+Source: tinyalsa
+Section: sound
+Priority: optional
+Maintainer: Taylor Holberton <taylorcholberton@gmail.com>
+Build-Depends: debhelper (>=7.0.50), doxygen
+Vcs-Git: git://github.com/tinyalsa/tinyalsa
+Vcs-Browser: https://github.com/tinyalsa/tinyalsa
+Standards-Version: 3.9.7
+
+Package: tinyalsa
+Architecture: any
+Section: sound
+Depends: libtinyalsa (= ${binary:Version}),
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: A collection of small programs to interface with ALSA in the Linux kernel.
+ TinyALSA is a lightweight interface to ALSA in the Linux kernel.
+
+Package: libtinyalsa
+Architecture: any
+Multi-Arch: same
+Section: libs
+Depends: ${misc:Depends},
+         ${shlibs:Depends}
+Description: A small C library for interfacing with ALSA in the Linux kernel.
+ The TinyALSA library is a lightweight, bare metal version of the ALSA library.
+
+Package: libtinyalsa-dev
+Architecture: any
+Multi-Arch: same
+Section: libdevel
+Depends: libtinyalsa,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: Development files for the TinyALSA library.
+ The TinyALSA library is a lightweight, bare metal version of the ALSA library.
+
+Package: libtinyalsa-dbg
+Architecture: any
+Multi-Arch: same
+Section: debug
+Depends: libtinyalsa,
+         ${misc:Depends}
+Description: Debug files for the TinyALSA library.
+ The TinyALSA library is a lightweight, bare metal version of the ALSA library.
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..6ae2c37
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,41 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: tinyalsa
+Source: https://github.com/tinyalsa/tinyalsa
+
+Files: *
+Copyright: 2016 Simon Wilson
+License: BSD-3-Clause
+
+Files: debian/*
+Copyright: 2016 Taylor Holberton <taylorcholberton@gmail.com>
+License: BSD-3-Clause
+
+License: BSD-3-Clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+ 3. Neither the name of the Android Open Source Project nor the names of
+    its contributors may be used to endorse or promote products derived
+    from this software without specific prior written permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
+ A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE HOLDERS OR
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
+# Please avoid picking licenses with terms that are more restrictive than the
+# packaged work, as it may make Debian's contributions unacceptable upstream.
diff --git a/debian/libtinyalsa-dev.dirs.in b/debian/libtinyalsa-dev.dirs.in
new file mode 100644
index 0000000..5c59953
--- /dev/null
+++ b/debian/libtinyalsa-dev.dirs.in
@@ -0,0 +1,2 @@
+/usr/lib/@DEB_HOST_MULTIARCH@
+/usr/include
diff --git a/debian/libtinyalsa-dev.install.in b/debian/libtinyalsa-dev.install.in
new file mode 100644
index 0000000..853d12c
--- /dev/null
+++ b/debian/libtinyalsa-dev.install.in
@@ -0,0 +1,4 @@
+debian/tmp/usr/include/* usr/include/
+debian/tmp/usr/lib/@DEB_HOST_MULTIARCH@/lib*.so usr/lib/@DEB_HOST_MULTIARCH@/
+debian/tmp/usr/lib/@DEB_HOST_MULTIARCH@/lib*.a usr/lib/@DEB_HOST_MULTIARCH@/
+debian/tmp/usr/share/man/man3 usr/share/man/
diff --git a/debian/libtinyalsa.dirs.in b/debian/libtinyalsa.dirs.in
new file mode 100644
index 0000000..6768aca
--- /dev/null
+++ b/debian/libtinyalsa.dirs.in
@@ -0,0 +1 @@
+/usr/lib/@DEB_HOST_MULTIARCH@
diff --git a/debian/libtinyalsa.install.in b/debian/libtinyalsa.install.in
new file mode 100644
index 0000000..a9ec3d3
--- /dev/null
+++ b/debian/libtinyalsa.install.in
@@ -0,0 +1 @@
+debian/tmp/usr/lib/@DEB_HOST_MULTIARCH@/*.so.* usr/lib/@DEB_HOST_MULTIARCH@/
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..7daeb27
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,33 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#DH_VERBOSE = 1
+
+DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+export DEB_HOST_MULTIARCH
+
+PREPROCESS_FILES := $(wildcard debian/*.in)
+
+$(PREPROCESS_FILES:.in=): %: %.in
+	sed 's,/@DEB_HOST_MULTIARCH@,$(DEB_HOST_MULTIARCH:%=/%),g' $< > $@
+
+.PHONY: override_dh_auto_clean
+override_dh_auto_clean:
+	dh_auto_clean
+	rm -rf $(PREPROCESS_FILES:.in=)
+
+.PHONY: override_dh_auto_install
+override_dh_auto_install: $(PREPROCESS_FILES:.in=)
+	dh_auto_install -- PREFIX=/usr
+
+.PHONY: override_dh_auto_test
+override_dh_auto_test:
+
+
+.PHONY: override_dh_strip
+override_dh_strip:
+	dh_strip --dbg-package=libtinyalsa-dbg
+
+%:
+	dh $@
+
diff --git a/debian/tinyalsa.dirs b/debian/tinyalsa.dirs
new file mode 100644
index 0000000..99b95ed
--- /dev/null
+++ b/debian/tinyalsa.dirs
@@ -0,0 +1,2 @@
+/usr/bin
+/usr/share
diff --git a/debian/tinyalsa.install b/debian/tinyalsa.install
new file mode 100644
index 0000000..438db01
--- /dev/null
+++ b/debian/tinyalsa.install
@@ -0,0 +1,8 @@
+debian/tmp/usr/bin/tinyplay usr/bin/
+debian/tmp/usr/bin/tinycap usr/bin/
+debian/tmp/usr/bin/tinymix usr/bin/
+debian/tmp/usr/bin/tinypcminfo usr/bin/
+debian/tmp/usr/share/man/man1/tinyplay.1 usr/share/man/man1/
+debian/tmp/usr/share/man/man1/tinycap.1 usr/share/man/man1/
+debian/tmp/usr/share/man/man1/tinymix.1 usr/share/man/man1/
+debian/tmp/usr/share/man/man1/tinypcminfo.1 usr/share/man/man1/
diff --git a/doxygen/Doxyfile b/doxygen/Doxyfile
new file mode 100644
index 0000000..a57ac00
--- /dev/null
+++ b/doxygen/Doxyfile
@@ -0,0 +1,2526 @@
+# Doxyfile 1.8.17
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "TinyALSA"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         = 1.1.0
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "A tiny library to interface with ALSA in the Linux kernel."
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       =
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION  = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = YES
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER         = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE  = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat
+# .inc files as Fortran files (default is PHP), and .f files as C (default is
+# Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL   = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# (including Cygwin) ands Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = YES
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = "./mainpage.h" \
+                         "../src" \
+                         "../include/tinyalsa"
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen
+# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f, *.for, *.tcl, *.vhd,
+# *.vhdl, *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS          = "*.c" \
+                         "*.h"
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           = "../examples"
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the
+# cost of reduced performance. This can be particularly helpful with template
+# rich C++ code for which doxygen's built-in parser lacks the necessary type
+# information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS          =
+
+# If clang assisted parsing is enabled you can provide the clang parser with the
+# path to the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files
+# were built. This is equivalent to specifying the "-p" option to a clang tool,
+# such as clang-check. These options will then be passed to the parser.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+
+CLANG_DATABASE_PATH    =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: https://developer.apple.com/xcode/), introduced with OSX
+# 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE      =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using JavaScript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when not enabling USE_PDFLATEX the default is latex when enabling
+# USE_PDFLATEX the default is pdflatex and when in the later case latex is
+# chosen this is overwritten by pdflatex. For specific output languages the
+# default can have been set differently, this depends on the implementation of
+# the output language.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# Note: This tag is used in the Makefile / make.bat.
+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
+# (.tex).
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
+# generate index for LaTeX. In case there is no backslash (\) as first character
+# it will be automatically added in the LaTeX code.
+# Note: This tag is used in the generated output file (.tex).
+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
+# The default value is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_MAKEINDEX_CMD    = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
+# path from which the emoji images will be read. If a relative path is entered,
+# it will be relative to the LATEX_OUTPUT directory. If left blank the
+# LATEX_OUTPUT directory will be used.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EMOJI_DIRECTORY  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# configuration file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's configuration file. A template extensions file can be
+# generated using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = YES
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
+# namespace members in file scope as well, matching the HTML output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_NS_MEMB_FILE_SCOPE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: YES.
+
+HAVE_DOT               = YES
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
+# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
+# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/doxygen/Makefile b/doxygen/Makefile
new file mode 100644
index 0000000..1cc4190
--- /dev/null
+++ b/doxygen/Makefile
@@ -0,0 +1,29 @@
+DESTDIR ?=
+PREFIX ?= /usr/local
+MANDIR ?= $(PREFIX)/share/man
+
+DOXYGEN := $(shell command -v doxygen 2> /dev/null)
+DOXYGENFLAGS =
+
+.PHONY: all
+all:
+ifndef DOXYGEN
+	$(warning "doxygen is not available please install it")
+else
+	$(DOXYGEN) $(DOXYGENFLAGS)
+endif
+
+.PHONY: clean
+clean:
+ifdef DOXYGEN
+	rm -Rf html
+	rm -Rf man
+endif
+
+.PHONY: install
+install:
+ifdef DOXYGEN
+	install -d $(DESTDIR)$(MANDIR)/man3
+	install man/man3/libtinyalsa-pcm.3 $(DESTDIR)$(MANDIR)/man3
+	install man/man3/libtinyalsa-mixer.3 $(DESTDIR)$(MANDIR)/man3
+endif
diff --git a/doxygen/mainpage.h b/doxygen/mainpage.h
new file mode 100644
index 0000000..cb7e7d3
--- /dev/null
+++ b/doxygen/mainpage.h
@@ -0,0 +1,10 @@
+/** @mainpage Preamble
+ *
+ * Welcome to the documentation for the TinyALSA project.
+ * <br><br>
+ * To start, you may either view the @ref libtinyalsa-pcm or @ref libtinyalsa-mixer.
+ * <br><br>
+ * If you find an error in the documentation or an area for improvement,
+ * open an issue or send a pull request to the <a href="https://github.com/tinyalsa/tinyalsa">github page</a>.
+ */
+
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..650966d
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,23 @@
+CROSS_COMPILE ?=
+
+CC = $(CROSS_COMPILE)gcc
+CFLAGS = -Wall -Wextra -Werror -Wfatal-errors -I ../include
+
+VPATH = ../src
+
+EXAMPLES += pcm-readi
+EXAMPLES += pcm-writei
+
+.PHONY: all
+all: $(EXAMPLES)
+
+pcm-readi pcm-writei: LDLIBS+=-ldl
+
+pcm-readi: pcm-readi.c -ltinyalsa
+
+pcm-writei: pcm-writei.c -ltinyalsa
+
+.PHONY: clean
+clean:
+	rm -f $(EXAMPLES)
+
diff --git a/examples/meson.build b/examples/meson.build
new file mode 100644
index 0000000..59a48a1
--- /dev/null
+++ b/examples/meson.build
@@ -0,0 +1,8 @@
+examples = ['pcm-readi', 'pcm-writei']
+
+foreach e : examples
+  executable(e, '@0@.c'.format(e),
+    include_directories: tinyalsa_includes,
+    link_with: tinyalsa,
+    install: false)
+endforeach
diff --git a/examples/pcm-readi.c b/examples/pcm-readi.c
new file mode 100644
index 0000000..8722b41
--- /dev/null
+++ b/examples/pcm-readi.c
@@ -0,0 +1,83 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <tinyalsa/pcm.h>
+
+static size_t read_frames(void **frames)
+{
+    unsigned int card = 0;
+    unsigned int device = 0;
+    int flags = PCM_IN;
+
+    const struct pcm_config config = {
+        .channels = 2,
+        .rate = 48000,
+        .format = PCM_FORMAT_S32_LE,
+        .period_size = 1024,
+        .period_count = 2,
+        .start_threshold = 1024,
+        .silence_threshold = 1024 * 2,
+        .stop_threshold = 1024 * 2
+    };
+
+    struct pcm *pcm = pcm_open(card, device, flags, &config);
+    if (pcm == NULL) {
+        fprintf(stderr, "failed to allocate memory for PCM\n");
+        return 0;
+    } else if (!pcm_is_ready(pcm)){
+        pcm_close(pcm);
+        fprintf(stderr, "failed to open PCM\n");
+        return 0;
+    }
+
+    unsigned int frame_size = pcm_frames_to_bytes(pcm, 1);
+    unsigned int frames_per_sec = pcm_get_rate(pcm);
+
+    *frames = malloc(frame_size * frames_per_sec);
+    if (*frames == NULL) {
+        fprintf(stderr, "failed to allocate frames\n");
+        pcm_close(pcm);
+        return 0;
+    }
+
+    int read_count = pcm_readi(pcm, *frames, frames_per_sec);
+
+    size_t byte_count = pcm_frames_to_bytes(pcm, read_count);
+
+    pcm_close(pcm);
+
+    return byte_count;
+}
+
+static int write_file(const void *frames, size_t size)
+{
+    FILE *output_file = fopen("audio.raw", "wb");
+    if (output_file == NULL) {
+        perror("failed to open 'audio.raw' for writing");
+        return EXIT_FAILURE;
+    }
+    fwrite(frames, 1, size, output_file);
+    fclose(output_file);
+    return 0;
+}
+
+int main(void)
+{
+    void *frames = NULL;
+    size_t size = 0;
+
+    size = read_frames(&frames);
+    if (size == 0) {
+        return EXIT_FAILURE;
+    }
+
+    if (write_file(frames, size) < 0) {
+        free(frames);
+        return EXIT_FAILURE;
+    }
+
+    free(frames);
+
+    return EXIT_SUCCESS;
+}
+
diff --git a/examples/pcm-writei.c b/examples/pcm-writei.c
new file mode 100644
index 0000000..19eafac
--- /dev/null
+++ b/examples/pcm-writei.c
@@ -0,0 +1,103 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <tinyalsa/pcm.h>
+
+static long int file_size(FILE * file)
+{
+    if (fseek(file, 0, SEEK_END) < 0) {
+        return -1;
+    }
+    long int file_size = ftell(file);
+    if (fseek(file, 0, SEEK_SET) < 0) {
+        return -1;
+    }
+    return file_size;
+}
+
+static size_t read_file(void ** frames){
+
+    FILE * input_file = fopen("audio.raw", "rb");
+    if (input_file == NULL) {
+        perror("failed to open 'audio.raw' for writing");
+        return 0;
+    }
+
+    long int size = file_size(input_file);
+    if (size < 0) {
+        perror("failed to get file size of 'audio.raw'");
+        fclose(input_file);
+        return 0;
+    }
+
+    *frames = malloc(size);
+    if (*frames == NULL) {
+        fprintf(stderr, "failed to allocate frames\n");
+        fclose(input_file);
+        return 0;
+    }
+
+    size = fread(*frames, 1, size, input_file);
+
+    fclose(input_file);
+
+    return size;
+}
+
+static int write_frames(const void * frames, size_t byte_count){
+
+    unsigned int card = 0;
+    unsigned int device = 0;
+    int flags = PCM_OUT;
+
+    const struct pcm_config config = {
+        .channels = 2,
+        .rate = 48000,
+        .format = PCM_FORMAT_S32_LE,
+        .period_size = 1024,
+        .period_count = 2,
+        .start_threshold = 1024,
+        .silence_threshold = 1024 * 2,
+        .stop_threshold = 1024 * 2
+    };
+
+    struct pcm * pcm = pcm_open(card, device, flags, &config);
+    if (pcm == NULL) {
+        fprintf(stderr, "failed to allocate memory for PCM\n");
+        return -1;
+    } else if (!pcm_is_ready(pcm)){
+        pcm_close(pcm);
+        fprintf(stderr, "failed to open PCM\n");
+        return -1;
+    }
+
+    unsigned int frame_count = pcm_bytes_to_frames(pcm, byte_count);
+
+    int err = pcm_writei(pcm, frames, frame_count);
+    if (err < 0) {
+      printf("error: %s\n", pcm_get_error(pcm));
+    }
+
+    pcm_close(pcm);
+
+    return 0;
+}
+
+int main(void)
+{
+    void *frames;
+    size_t size;
+
+    size = read_file(&frames);
+    if (size == 0) {
+        return EXIT_FAILURE;
+    }
+
+    if (write_frames(frames, size) < 0) {
+        return EXIT_FAILURE;
+    }
+
+    free(frames);
+    return EXIT_SUCCESS;
+}
+
diff --git a/include/tinyalsa/asoundlib.h b/include/tinyalsa/asoundlib.h
new file mode 100644
index 0000000..bae0d03
--- /dev/null
+++ b/include/tinyalsa/asoundlib.h
@@ -0,0 +1,37 @@
+/* asoundlib.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#ifndef TINYALSA_ASOUNDLIB_H
+#define TINYALSA_ASOUNDLIB_H
+
+#include "mixer.h"
+#include "pcm.h"
+#include "version.h"
+
+#endif
+
diff --git a/include/tinyalsa/attributes.h b/include/tinyalsa/attributes.h
new file mode 100644
index 0000000..f465ba1
--- /dev/null
+++ b/include/tinyalsa/attributes.h
@@ -0,0 +1,44 @@
+#ifndef TINYALSA_ATTRIBUTES_H
+#define TINYALSA_ATTRIBUTES_H
+
+/** @defgroup libtinyalsa-attributes
+ * @brief GCC attributes to issue diagnostics
+ * when the library is being used incorrectly.
+ * */
+
+// FIXME: Disable the deprecated attribute in Android temporarily. pcm_read/write are marked as
+//   deprecated functions in the latest tinyalsa in GitHub. However, there are lots of libraries in
+//   Android using these functions and building with -Werror flags. Besides build breakage, the
+//   behavior and interface of the successors, pcm_readi/writei, are also changed. Once all have
+//   been cleaned up, we will enable this again.
+#if defined(__GNUC__) && !defined(ANDROID)
+
+/** Issues a warning when a function is being
+ * used that is now deprecated.
+ * @ingroup libtinyalsa-attributes
+ * */
+#define TINYALSA_DEPRECATED __attribute__((deprecated))
+
+/** Issues a warning when a return code of
+ * a function is not checked.
+ * @ingroup libtinyalsa-attributes
+ * */
+#define TINYALSA_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+
+#else /* __GNUC__ && !ANDROID */
+
+/** This is just a placeholder for compilers
+ * that aren't GCC or Clang.
+ * @ingroup libtinyalsa-attributes
+ * */
+#define TINYALSA_DEPRECATED
+
+/** This is just a placeholder for compilers
+ * that aren't GCC or Clang.
+ * @ingroup libtinyalsa-attributes
+ * */
+#define TINYALSA_WARN_UNUSED_RESULT
+
+#endif /* __GNUC__ && !ANDROID */
+
+#endif /* TINYALSA_ATTRIBUTES_H */
diff --git a/include/tinyalsa/interval.h b/include/tinyalsa/interval.h
new file mode 100644
index 0000000..068571d
--- /dev/null
+++ b/include/tinyalsa/interval.h
@@ -0,0 +1,54 @@
+/* interval.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#ifndef TINYALSA_INTERVAL_H
+#define TINYALSA_INTERVAL_H
+
+#include <stdlib.h>
+#include <unistd.h>
+
+/** A closed range signed interval. */
+
+struct tinyalsa_signed_interval {
+	/** The maximum value of the interval */
+	ssize_t max;
+	/** The minimum value of the interval */
+	ssize_t min;
+};
+
+/** A closed range unsigned interval. */
+
+struct tinyalsa_unsigned_interval {
+	/** The maximum value of the interval */
+	size_t max;
+	/** The minimum value of the interval */
+	size_t min;
+};
+
+#endif /* TINYALSA_INTERVAL_H */
+
diff --git a/include/tinyalsa/limits.h b/include/tinyalsa/limits.h
new file mode 100644
index 0000000..6f788c2
--- /dev/null
+++ b/include/tinyalsa/limits.h
@@ -0,0 +1,62 @@
+/* limits.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#ifndef TINYALSA_LIMITS_H
+#define TINYALSA_LIMITS_H
+
+#include <tinyalsa/interval.h>
+
+#include <limits.h>
+#include <stdint.h>
+
+#define TINYALSA_SIGNED_INTERVAL_MAX SSIZE_MAX
+#define TINYALSA_SIGNED_INTERVAL_MIN SSIZE_MIN
+
+#define TINYALSA_UNSIGNED_INTERVAL_MAX SIZE_MAX
+#define TINYALSA_UNSIGNED_INTERVAL_MIN SIZE_MIN
+
+#define TINYALSA_CHANNELS_MAX 32U
+#define TINYALSA_CHANNELS_MIN 1U
+
+#define TINYALSA_FRAMES_MAX (ULONG_MAX / (TINYALSA_CHANNELS_MAX * 4))
+#define TINYALSA_FRAMES_MIN 0U
+
+#if TINYALSA_FRAMES_MAX > TINYALSA_UNSIGNED_INTERVAL_MAX
+#error "Frames max exceeds measurable value."
+#endif
+
+#if TINYALSA_FRAMES_MIN < TINYALSA_UNSIGNED_INTERVAL_MIN
+#error "Frames min exceeds measurable value."
+#endif
+
+extern const struct tinyalsa_unsigned_interval tinyalsa_channels_limit;
+
+extern const struct tinyalsa_unsigned_interval tinyalsa_frames_limit;
+
+#endif /* TINYALSA_LIMITS_H */
+
diff --git a/include/tinyalsa/meson.build b/include/tinyalsa/meson.build
new file mode 100644
index 0000000..d14b35b
--- /dev/null
+++ b/include/tinyalsa/meson.build
@@ -0,0 +1,10 @@
+tinyalsa_headers = [
+  'asoundlib.h',
+  'interval.h',
+  'limits.h',
+  'mixer.h',
+  'pcm.h',
+  'version.h'
+]
+
+install_headers(tinyalsa_headers, subdir: 'tinyalsa')
diff --git a/include/tinyalsa/mixer.h b/include/tinyalsa/mixer.h
new file mode 100644
index 0000000..7d0580f
--- /dev/null
+++ b/include/tinyalsa/mixer.h
@@ -0,0 +1,164 @@
+/* mixer.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+/** @file */
+
+/** @defgroup libtinyalsa-mixer Mixer Interface
+ * @brief All macros, structures and functions that make up the mixer interface.
+ */
+
+#ifndef TINYALSA_MIXER_H
+#define TINYALSA_MIXER_H
+
+#include <sys/time.h>
+#include <stddef.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+struct mixer;
+
+struct mixer_ctl;
+
+// mixer_ctl_event is a mirroring structure of snd_ctl_event
+struct mixer_ctl_event {
+    int type;
+    union {
+        struct {
+            unsigned int mask;
+            struct {
+                unsigned int numid;
+                int iface;
+                unsigned int device;
+                unsigned int subdevice;
+                unsigned char name[44];
+                unsigned int index;
+            } id;
+        } element;
+        unsigned char data[60];
+    } data;
+};
+
+/** Mixer control type.
+ * @ingroup libtinyalsa-mixer
+ */
+enum mixer_ctl_type {
+    /** boolean control type */
+    MIXER_CTL_TYPE_BOOL,
+    /** integer control type */
+    MIXER_CTL_TYPE_INT,
+    /** an enumerated control type */
+    MIXER_CTL_TYPE_ENUM,
+    MIXER_CTL_TYPE_BYTE,
+    MIXER_CTL_TYPE_IEC958,
+    /** a 64 bit integer control type */
+    MIXER_CTL_TYPE_INT64,
+    /** unknown control type */
+    MIXER_CTL_TYPE_UNKNOWN,
+    /** end of the enumeration (not a control type) */
+    MIXER_CTL_TYPE_MAX,
+};
+
+struct mixer *mixer_open(unsigned int card);
+
+void mixer_close(struct mixer *mixer);
+
+int mixer_add_new_ctls(struct mixer *mixer);
+
+const char *mixer_get_name(const struct mixer *mixer);
+
+unsigned int mixer_get_num_ctls(const struct mixer *mixer);
+
+unsigned int mixer_get_num_ctls_by_name(const struct mixer *mixer, const char *name);
+
+const struct mixer_ctl *mixer_get_ctl_const(const struct mixer *mixer, unsigned int id);
+
+struct mixer_ctl *mixer_get_ctl(struct mixer *mixer, unsigned int id);
+
+struct mixer_ctl *mixer_get_ctl_by_name(struct mixer *mixer, const char *name);
+
+struct mixer_ctl *mixer_get_ctl_by_name_and_index(struct mixer *mixer,
+                                                  const char *name,
+                                                  unsigned int index);
+
+int mixer_subscribe_events(struct mixer *mixer, int subscribe);
+
+int mixer_wait_event(struct mixer *mixer, int timeout);
+
+unsigned int mixer_ctl_get_id(const struct mixer_ctl *ctl);
+
+const char *mixer_ctl_get_name(const struct mixer_ctl *ctl);
+
+enum mixer_ctl_type mixer_ctl_get_type(const struct mixer_ctl *ctl);
+
+const char *mixer_ctl_get_type_string(const struct mixer_ctl *ctl);
+
+unsigned int mixer_ctl_get_num_values(const struct mixer_ctl *ctl);
+
+unsigned int mixer_ctl_get_num_enums(const struct mixer_ctl *ctl);
+
+const char *mixer_ctl_get_enum_string(struct mixer_ctl *ctl, unsigned int enum_id);
+
+/* Some sound cards update their controls due to external events,
+ * such as HDMI EDID byte data changing when an HDMI cable is
+ * connected. This API allows the count of elements to be updated.
+ */
+void mixer_ctl_update(struct mixer_ctl *ctl);
+
+int mixer_ctl_is_access_tlv_rw(const struct mixer_ctl *ctl);
+
+/* Set and get mixer controls */
+int mixer_ctl_get_percent(const struct mixer_ctl *ctl, unsigned int id);
+
+int mixer_ctl_set_percent(struct mixer_ctl *ctl, unsigned int id, int percent);
+
+int mixer_ctl_get_value(const struct mixer_ctl *ctl, unsigned int id);
+
+int mixer_ctl_get_array(const struct mixer_ctl *ctl, void *array, size_t count);
+
+int mixer_ctl_set_value(struct mixer_ctl *ctl, unsigned int id, int value);
+
+int mixer_ctl_set_array(struct mixer_ctl *ctl, const void *array, size_t count);
+
+int mixer_ctl_set_enum_by_string(struct mixer_ctl *ctl, const char *string);
+
+/* Determine range of integer mixer controls */
+int mixer_ctl_get_range_min(const struct mixer_ctl *ctl);
+
+int mixer_ctl_get_range_max(const struct mixer_ctl *ctl);
+
+int mixer_read_event(struct mixer *mixer, struct mixer_ctl_event *event);
+
+int mixer_consume_event(struct mixer *mixer);
+#if defined(__cplusplus)
+}  /* extern "C" */
+#endif
+
+#endif
+
diff --git a/include/tinyalsa/pcm.h b/include/tinyalsa/pcm.h
new file mode 100644
index 0000000..6569146
--- /dev/null
+++ b/include/tinyalsa/pcm.h
@@ -0,0 +1,373 @@
+/* pcm.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+/** @file */
+
+/** @defgroup libtinyalsa-pcm PCM Interface
+ * @brief All macros, structures and functions that make up the PCM interface.
+ */
+
+#ifndef TINYALSA_PCM_H
+#define TINYALSA_PCM_H
+
+#include <tinyalsa/attributes.h>
+
+#include <sys/time.h>
+#include <stddef.h>
+
+/** A flag that specifies that the PCM is an output.
+ * May not be bitwise AND'd with @ref PCM_IN.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_OUT 0x00000000
+
+/** Specifies that the PCM is an input.
+ * May not be bitwise AND'd with @ref PCM_OUT.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_IN 0x10000000
+
+/** Specifies that the PCM will use mmap read and write methods.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_MMAP 0x00000001
+
+/** Specifies no interrupt requests.
+ * May only be bitwise AND'd with @ref PCM_MMAP.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_NOIRQ 0x00000002
+
+/** When set, calls to @ref pcm_write
+ * for a playback stream will not attempt
+ * to restart the stream in the case of an
+ * underflow, but will return -EPIPE instead.
+ * After the first -EPIPE error, the stream
+ * is considered to be stopped, and a second
+ * call to pcm_write will attempt to restart
+ * the stream.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_NORESTART 0x00000004
+
+/** Specifies monotonic timestamps.
+ * Used in @ref pcm_open.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_MONOTONIC 0x00000008
+
+/** If used with @ref pcm_open and @ref pcm_params_get,
+ * it will not cause the function to block if
+ * the PCM is not available. It will also cause
+ * the functions @ref pcm_readi and @ref pcm_writei
+ * to exit if they would cause the caller to wait.
+ * @ingroup libtinyalsa-pcm
+ * */
+#define PCM_NONBLOCK 0x00000010
+
+/** Means a PCM is opened
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_OPEN 0x00
+
+/** Means a PCM HW_PARAMS is set
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_SETUP 0x01
+
+/** Means a PCM is prepared
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_PREPARED 0x02
+
+/** For inputs, this means the PCM is recording audio samples.
+ * For outputs, this means the PCM is playing audio samples.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_RUNNING 0x03
+
+/** For inputs, this means an overrun occured.
+ * For outputs, this means an underrun occured.
+ */
+#define PCM_STATE_XRUN 0x04
+
+/** For outputs, this means audio samples are played.
+ * A PCM is in a draining state when it is coming to a stop.
+ */
+#define PCM_STATE_DRAINING 0x05
+
+/** Means a PCM is suspended.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_SUSPENDED 0x07
+
+/** Means a PCM has been disconnected.
+ * @ingroup libtinyalsa-pcm
+ */
+#define PCM_STATE_DISCONNECTED 0x08
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+/** Audio sample format of a PCM.
+ * The first letter specifiers whether the sample is signed or unsigned.
+ * The letter 'S' means signed. The letter 'U' means unsigned.
+ * The following number is the amount of bits that the sample occupies in memory.
+ * Following the underscore, specifiers whether the sample is big endian or little endian.
+ * The letters 'LE' mean little endian.
+ * The letters 'BE' mean big endian.
+ * This enumeration is used in the @ref pcm_config structure.
+ * @ingroup libtinyalsa-pcm
+ */
+enum pcm_format {
+
+/* Note: This section must stay in the same
+ * order for binary compatibility with older
+ * versions of TinyALSA. */
+
+    PCM_FORMAT_INVALID = -1,
+    /** Signed 16-bit, little endian */
+    PCM_FORMAT_S16_LE = 0,
+    /** Signed, 32-bit, little endian */
+    PCM_FORMAT_S32_LE,
+    /** Signed, 8-bit */
+    PCM_FORMAT_S8,
+    /** Signed, 24-bit (32-bit in memory), little endian */
+    PCM_FORMAT_S24_LE,
+    /** Signed, 24-bit, little endian */
+    PCM_FORMAT_S24_3LE,
+
+/* End of compatibility section. */
+
+    /** Signed, 16-bit, big endian */
+    PCM_FORMAT_S16_BE,
+    /** Signed, 24-bit (32-bit in memory), big endian */
+    PCM_FORMAT_S24_BE,
+    /** Signed, 24-bit, big endian */
+    PCM_FORMAT_S24_3BE,
+    /** Signed, 32-bit, big endian */
+    PCM_FORMAT_S32_BE,
+    /** Max of the enumeration list, not an actual format. */
+    PCM_FORMAT_MAX
+};
+
+/** A bit mask of 256 bits (32 bytes) that describes some hardware parameters of a PCM */
+struct pcm_mask {
+    /** bits of the bit mask */
+    unsigned int bits[32 / sizeof(unsigned int)];
+};
+
+/** Encapsulates the hardware and software parameters of a PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm_config {
+    /** The number of channels in a frame */
+    unsigned int channels;
+    /** The number of frames per second */
+    unsigned int rate;
+    /** The number of frames in a period */
+    unsigned int period_size;
+    /** The number of periods in a PCM */
+    unsigned int period_count;
+    /** The sample format of a PCM */
+    enum pcm_format format;
+    /* Values to use for the ALSA start, stop and silence thresholds, and
+     * silence size.  Setting any one of these values to 0 will cause the
+     * default tinyalsa values to be used instead.
+     * Tinyalsa defaults are as follows.
+     *
+     * start_threshold   : period_count * period_size
+     * stop_threshold    : period_count * period_size
+     * silence_threshold : 0
+     * silence_size      : 0
+     */
+    /** The minimum number of frames required to start the PCM */
+    unsigned int start_threshold;
+    /** The minimum number of frames required to stop the PCM */
+    unsigned int stop_threshold;
+    /** The minimum number of frames to silence the PCM */
+    unsigned int silence_threshold;
+    /** The number of frames to overwrite the playback buffer when the playback underrun is greater
+     * than the silence threshold */
+    unsigned int silence_size;
+
+    unsigned int avail_min;
+};
+
+/** Enumeration of a PCM's hardware parameters.
+ * Each of these parameters is either a mask or an interval.
+ * @ingroup libtinyalsa-pcm
+ */
+enum pcm_param
+{
+    /** A mask that represents the type of read or write method available (e.g. interleaved, mmap). */
+    PCM_PARAM_ACCESS,
+    /** A mask that represents the @ref pcm_format available (e.g. @ref PCM_FORMAT_S32_LE) */
+    PCM_PARAM_FORMAT,
+    /** A mask that represents the subformat available */
+    PCM_PARAM_SUBFORMAT,
+    /** An interval representing the range of sample bits available (e.g. 8 to 32) */
+    PCM_PARAM_SAMPLE_BITS,
+    /** An interval representing the range of frame bits available (e.g. 8 to 64) */
+    PCM_PARAM_FRAME_BITS,
+    /** An interval representing the range of channels available (e.g. 1 to 2) */
+    PCM_PARAM_CHANNELS,
+    /** An interval representing the range of rates available (e.g. 44100 to 192000) */
+    PCM_PARAM_RATE,
+    PCM_PARAM_PERIOD_TIME,
+    /** The number of frames in a period */
+    PCM_PARAM_PERIOD_SIZE,
+    /** The number of bytes in a period */
+    PCM_PARAM_PERIOD_BYTES,
+    /** The number of periods for a PCM */
+    PCM_PARAM_PERIODS,
+    PCM_PARAM_BUFFER_TIME,
+    PCM_PARAM_BUFFER_SIZE,
+    PCM_PARAM_BUFFER_BYTES,
+    PCM_PARAM_TICK_TIME,
+}; /* enum pcm_param */
+
+struct pcm_params;
+
+struct pcm_params *pcm_params_get(unsigned int card, unsigned int device,
+                                  unsigned int flags);
+
+void pcm_params_free(struct pcm_params *pcm_params);
+
+const struct pcm_mask *pcm_params_get_mask(const struct pcm_params *pcm_params, enum pcm_param param);
+
+unsigned int pcm_params_get_min(const struct pcm_params *pcm_params, enum pcm_param param);
+
+unsigned int pcm_params_get_max(const struct pcm_params *pcm_params, enum pcm_param param);
+
+/* Converts the pcm parameters to a human readable string.
+ * The string parameter is a caller allocated buffer of size bytes,
+ * which is then filled up to size - 1 and null terminated,
+ * if size is greater than zero.
+ * The return value is the number of bytes copied to string
+ * (not including null termination) if less than size; otherwise,
+ * the number of bytes required for the buffer.
+ */
+int pcm_params_to_string(struct pcm_params *params, char *string, unsigned int size);
+
+/* Returns 1 if the pcm_format is present (format bit set) in
+ * the pcm_params structure; 0 otherwise, or upon unrecognized format.
+ */
+int pcm_params_format_test(struct pcm_params *params, enum pcm_format format);
+
+struct pcm;
+
+struct pcm *pcm_open(unsigned int card,
+                     unsigned int device,
+                     unsigned int flags,
+                     const struct pcm_config *config);
+
+struct pcm *pcm_open_by_name(const char *name,
+                             unsigned int flags,
+                             const struct pcm_config *config);
+
+int pcm_close(struct pcm *pcm);
+
+int pcm_is_ready(const struct pcm *pcm);
+
+unsigned int pcm_get_channels(const struct pcm *pcm);
+
+const struct pcm_config * pcm_get_config(const struct pcm *pcm);
+
+unsigned int pcm_get_rate(const struct pcm *pcm);
+
+enum pcm_format pcm_get_format(const struct pcm *pcm);
+
+int pcm_get_file_descriptor(const struct pcm *pcm);
+
+const char *pcm_get_error(const struct pcm *pcm);
+
+int pcm_set_config(struct pcm *pcm, const struct pcm_config *config);
+
+unsigned int pcm_format_to_bits(enum pcm_format format);
+
+unsigned int pcm_get_buffer_size(const struct pcm *pcm);
+
+unsigned int pcm_frames_to_bytes(const struct pcm *pcm, unsigned int frames);
+
+unsigned int pcm_bytes_to_frames(const struct pcm *pcm, unsigned int bytes);
+
+int pcm_get_htimestamp(struct pcm *pcm, unsigned int *avail, struct timespec *tstamp);
+
+unsigned int pcm_get_subdevice(const struct pcm *pcm);
+
+int pcm_writei(struct pcm *pcm, const void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;
+
+int pcm_readi(struct pcm *pcm, void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;
+
+int pcm_write(struct pcm *pcm, const void *data, unsigned int count) TINYALSA_DEPRECATED;
+
+int pcm_read(struct pcm *pcm, void *data, unsigned int count) TINYALSA_DEPRECATED;
+
+int pcm_mmap_write(struct pcm *pcm, const void *data, unsigned int count);
+
+int pcm_mmap_read(struct pcm *pcm, void *data, unsigned int count);
+
+int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset, unsigned int *frames);
+
+int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames);
+
+int pcm_mmap_avail(struct pcm *pcm);
+
+int pcm_mmap_get_hw_ptr(struct pcm* pcm, unsigned int *hw_ptr, struct timespec *tstamp);
+
+int pcm_get_poll_fd(struct pcm *pcm);
+
+int pcm_link(struct pcm *pcm1, struct pcm *pcm2);
+
+int pcm_unlink(struct pcm *pcm);
+
+int pcm_prepare(struct pcm *pcm);
+
+int pcm_start(struct pcm *pcm);
+
+int pcm_stop(struct pcm *pcm);
+
+int pcm_wait(struct pcm *pcm, int timeout);
+
+long pcm_get_delay(struct pcm *pcm);
+
+int pcm_ioctl(struct pcm *pcm, int code, ...) TINYALSA_DEPRECATED;
+
+#if defined(__cplusplus)
+}  /* extern "C" */
+#endif
+
+#endif
+
diff --git a/include/tinyalsa/plugin.h b/include/tinyalsa/plugin.h
new file mode 100644
index 0000000..b2f97b9
--- /dev/null
+++ b/include/tinyalsa/plugin.h
@@ -0,0 +1,269 @@
+/* plugin.h
+** Copyright (c) 2019-2020, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#ifndef TINYALSA_PLUGIN_H
+#define TINYALSA_PLUGIN_H
+
+#include <poll.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include <sound/asound.h>
+
+/* static initializers */
+
+#define SND_VALUE_ENUM(etexts, eitems)    \
+    {.texts = etexts, .items = eitems}
+
+#define SND_VALUE_BYTES(csize)    \
+    {.size = csize }
+
+#define SND_VALUE_INTEGER(icount, imin, imax, istep) \
+    {.count = icount, .min = imin, .max = imax, .step = istep }
+
+#define SND_VALUE_TLV_BYTES(csize, cget, cput)       \
+    {.size = csize, .get = cget, .put = cput }
+
+/* pointer based initializers */
+#define INIT_SND_CONTROL_INTEGER(c, cname, cget, cput, cint, pval, pdata)   \
+    {                                                                       \
+        c->iface = SNDRV_CTL_ELEM_IFACE_MIXER;                              \
+        c->access = SNDRV_CTL_ELEM_ACCESS_READWRITE;                        \
+        c->type = SNDRV_CTL_ELEM_TYPE_INTEGER;                              \
+        c->name = cname; c->value = &cint; c->get = cget; c->put = cput;    \
+        c->private_value = pval; c->private_data = pdata;                   \
+    }
+
+#define INIT_SND_CONTROL_BYTES(c, cname, cget, cput, cint, pval, pdata)     \
+    {                                                                       \
+        c->iface = SNDRV_CTL_ELEM_IFACE_MIXER;                              \
+        c->access = SNDRV_CTL_ELEM_ACCESS_READWRITE;                        \
+        c->type = SNDRV_CTL_ELEM_TYPE_BYTES;                                \
+        c->name = cname; c->value = &cint; c->get = cget; c->put = cput;    \
+        c->private_value = pval; c->private_data = pdata;                   \
+    }
+
+#define INIT_SND_CONTROL_ENUM(c, cname, cget, cput, cenum, pval, pdata)     \
+    {                                                                       \
+        c->iface = SNDRV_CTL_ELEM_IFACE_MIXER;                              \
+        c->access = SNDRV_CTL_ELEM_ACCESS_READWRITE;                        \
+        c->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;                           \
+        c->name = cname; c->value = cenum; c->get = cget; c->put = cput;    \
+        c->private_value = pval; c->private_data = pdata;                   \
+    }
+
+#define INIT_SND_CONTROL_TLV_BYTES(c, cname, cbytes, priv_val, priv_data)  \
+    {                                                                      \
+        c->iface = SNDRV_CTL_ELEM_IFACE_MIXER;                             \
+        c->access = SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE;                   \
+        c->type = SNDRV_CTL_ELEM_TYPE_BYTES;                               \
+        c->name = cname; c->value = &cbytes;                               \
+        c->private_value = priv_val; c->private_data = priv_data;          \
+    }
+
+struct mixer_plugin;
+struct pcm_plugin;
+struct snd_node;
+
+/** Operations that are required to be registered by the plugin.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm_plugin_ops {
+    /** open the pcm plugin */
+    int (*open) (struct pcm_plugin **plugin, unsigned int card,
+                 unsigned int device, unsigned int flags);
+    /** close the pcm plugin */
+    int (*close) (struct pcm_plugin *plugin);
+    /** Set the PCM hardware parameters to the plugin */
+    int (*hw_params) (struct pcm_plugin *plugin,
+                      struct snd_pcm_hw_params *params);
+    /** Set the PCM software parameters to the plugin */
+    int (*sw_params) (struct pcm_plugin *plugin,
+                      struct snd_pcm_sw_params *params);
+    /** Synchronize the pointer */
+    int (*sync_ptr) (struct pcm_plugin *plugin,
+                     struct snd_pcm_sync_ptr *sync_ptr);
+    /** Write frames to plugin to be rendered to output */
+    int (*writei_frames) (struct pcm_plugin *plugin,
+                          struct snd_xferi *x);
+    /** Read frames from plugin captured from input */
+    int (*readi_frames) (struct pcm_plugin *plugin,
+                         struct snd_xferi *x);
+    /** Obtain the timestamp for the PCM */
+    int (*ttstamp) (struct pcm_plugin *plugin,
+                    int *tstamp);
+    /** Prepare the plugin for data transfer */
+    int (*prepare) (struct pcm_plugin *plugin);
+    /** Start data transfer from/to the plugin */
+    int (*start) (struct pcm_plugin *plugin);
+    /** Drop pcm frames */
+    int (*drop) (struct pcm_plugin *plugin);
+    /** Any custom or alsa specific ioctl implementation */
+    int (*ioctl) (struct pcm_plugin *plugin,
+                  int cmd, void *arg);
+    void *(*mmap) (struct pcm_plugin *plugin, void *addr, size_t length,
+                   int prot, int flags, off_t offset);
+    int (*munmap) (struct pcm_plugin *plugin, void *addr, size_t length);
+    int (*poll) (struct pcm_plugin *plugin, struct pollfd *pfd, nfds_t nfds,
+                 int timeout);
+};
+
+/** Minimum and maximum values for hardware parameter constraints.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm_plugin_min_max {
+    /** Minimum value for the hardware parameter */
+    unsigned int min;
+    /** Maximum value for the hardware parameter */
+    unsigned int max;
+};
+
+/** Encapsulate the hardware parameter constraints
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm_plugin_hw_constraints {
+    /** Value for SNDRV_PCM_HW_PARAM_ACCESS param */
+    uint64_t access;
+    /** Value for SNDRV_PCM_HW_PARAM_FORMAT param.
+     * As of this implementation ALSA supports 52 formats */
+    uint64_t format;
+    /** Value for SNDRV_PCM_HW_PARAM_SAMPLE_BITS param */
+    struct pcm_plugin_min_max bit_width;
+    /** Value for SNDRV_PCM_HW_PARAM_CHANNELS param */
+    struct pcm_plugin_min_max channels;
+    /** Value for SNDRV_PCM_HW_PARAM_RATE param */
+    struct pcm_plugin_min_max rate;
+    /** Value for SNDRV_PCM_HW_PARAM_PERIODS param */
+    struct pcm_plugin_min_max periods;
+    /** Value for SNDRV_PCM_HW_PARAM_PERIOD_BYTES param */
+    struct pcm_plugin_min_max period_bytes;
+};
+
+struct pcm_plugin {
+    /** Card number for the pcm device */
+    unsigned int card;
+    /** device number for the pcm device */
+    unsigned int device;
+    /** pointer to the contraints registered by the plugin */
+    struct pcm_plugin_hw_constraints *constraints;
+    /** Indicates read/write mode, etc.. */
+    int mode;
+    /* Pointer to hold the plugin's private data */
+    void *priv;
+    /* Tracks the plugin state */
+    unsigned int state;
+};
+
+typedef void (*mixer_event_callback)(struct mixer_plugin *);
+
+struct mixer_plugin_ops {
+    int (*open) (struct mixer_plugin **plugin, unsigned int card);
+    void (*close) (struct mixer_plugin **plugin);
+    int (*subscribe_events) (struct mixer_plugin *plugin,
+                             mixer_event_callback event_cb);
+    ssize_t (*read_event) (struct mixer_plugin *plugin,
+                           struct snd_ctl_event *ev, size_t size);
+};
+
+struct snd_control {
+    snd_ctl_elem_iface_t iface;
+    unsigned int access;
+    const char *name;
+    snd_ctl_elem_type_t type;
+    void *value;
+    int (*get) (struct mixer_plugin *plugin,
+                struct snd_control *control,
+                struct snd_ctl_elem_value *ev);
+    int (*put) (struct mixer_plugin *plugin,
+                struct snd_control *control,
+                struct snd_ctl_elem_value *ev);
+    uint32_t private_value;
+    void *private_data;
+};
+
+struct mixer_plugin {
+    unsigned int card;
+    void *priv;
+
+    int eventfd;
+    int subscribed;
+    int event_cnt;
+
+    struct snd_control *controls;
+    unsigned int num_controls;
+};
+
+struct snd_value_enum {
+    unsigned int items;
+    char **texts;
+};
+
+struct snd_value_bytes {
+    unsigned int size;
+};
+
+struct snd_value_tlv_bytes {
+    unsigned int size;
+    int (*get) (struct mixer_plugin *plugin,
+                struct snd_control *control,
+                struct snd_ctl_tlv *tlv);
+    int (*put) (struct mixer_plugin *plugin,
+                struct snd_control *control,
+                struct snd_ctl_tlv *tlv);
+};
+
+struct snd_value_int {
+    unsigned int count;
+    int min;
+    int max;
+    int step;
+};
+
+/** Operations defined by the plugin.
+ * */
+struct snd_node_ops {
+    /** Function pointer to get card definition */
+    void* (*open_card)(unsigned int card);
+    /** Function pointer to release card definition */
+    void (*close_card)(void *card);
+    /** Get interger type properties from device definition */
+    int (*get_int)(void *node, const char *prop, int *val);
+    /** Get string type properties from device definition */
+    int (*get_str)(void *node, const char *prop, char **val);
+    /** Function pointer to get mixer definition */
+    void* (*get_mixer)(void *card);
+    /** Function pointer to get PCM definition */
+    void* (*get_pcm)(void *card, unsigned int id);
+    /** Reserved for other nodes such as compress */
+    void* reserved[4];
+};
+
+#endif /* end of TINYALSA_PLUGIN_H */
diff --git a/include/tinyalsa/version.h b/include/tinyalsa/version.h
new file mode 100644
index 0000000..f7fa5db
--- /dev/null
+++ b/include/tinyalsa/version.h
@@ -0,0 +1,58 @@
+/* version.h
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#ifndef TINYALSA_VERSION_H
+#define TINYALSA_VERSION_H
+
+/* Macros for expanding the version numbers into string literals */
+#define TINYALSA_VERSION_STR_EX(number) #number
+#define TINYALSA_VERSION_STR(number) TINYALSA_VERSION_STR_EX (number)
+
+#define TINYALSA_VERSION_MAJOR 2
+
+#define TINYALSA_VERSION_MINOR 0
+
+#define TINYALSA_VERSION_PATCH 0
+
+/* The final version number is constructed based on minor, major and patch */
+#define TINYALSA_VERSION \
+    ((unsigned long) \
+    ((TINYALSA_VERSION_MAJOR << 16)   | \
+     (TINYALSA_VERSION_MINOR << 8 )   | \
+     (TINYALSA_VERSION_PATCH      )))
+
+/* The version string is constructed by concatenating individual ver. strings */
+#define TINYALSA_VERSION_STRING \
+    TINYALSA_VERSION_STR (TINYALSA_VERSION_MAJOR) \
+    "." \
+    TINYALSA_VERSION_STR (TINYALSA_VERSION_MINOR) \
+    "." \
+    TINYALSA_VERSION_STR (TINYALSA_VERSION_PATCH)
+
+#endif /* TINYALSA_VERSION_H */
+
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..213f8c8
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,38 @@
+project ('tinyalsa', 'c',
+  version : run_command(find_program('scripts/version.sh'), 'print', '-s').stdout().strip(),
+  meson_version : '>= 0.48.0')
+
+tinyalsa_includes = include_directories('.', 'include')
+
+cc = meson.get_compiler('c')
+
+# Dependency on libdl
+dl_dep = cc.find_library('dl')
+
+tinyalsa = library('tinyalsa',
+  'src/mixer.c', 'src/pcm.c', 'src/pcm_hw.c', 'src/pcm_plugin.c', 'src/snd_card_plugin.c', 'src/mixer_hw.c', 'src/mixer_plugin.c',
+  include_directories: tinyalsa_includes,
+  version: meson.project_version(),
+  install: true,
+  dependencies: dl_dep)
+
+# For use as a Meson subproject
+tinyalsa_dep = declare_dependency(link_with: tinyalsa,
+  include_directories: include_directories('include'))
+
+if not get_option('docs').disabled()
+  # subdir('docs') # FIXME
+endif
+
+if not get_option('examples').disabled()
+  subdir('examples')
+endif
+
+subdir('include/tinyalsa')
+
+if not get_option('utils').disabled()
+  subdir('utils')
+endif
+
+pkg = import('pkgconfig')
+pkg.generate(tinyalsa, description: 'TinyALSA Library')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..f2e2bc2
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,6 @@
+option('docs', type: 'feature', value: 'auto', yield: true,
+  description : 'Generate documentation with Doxygen')
+option('examples', type: 'feature', value: 'auto', yield: true,
+  description : 'Build examples')
+option('utils', type: 'feature', value: 'auto', yield: true,
+  description : 'Build utility tools')
diff --git a/scripts/travis-build.sh b/scripts/travis-build.sh
new file mode 100755
index 0000000..2bfc4d2
--- /dev/null
+++ b/scripts/travis-build.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -e
+set -u
+
+ROOT=$(pwd)
+
+make
+make clean
+
+mkdir cmake-build
+cd cmake-build
+cmake ..
+cmake --build .
+cd ..
+
+$HOME/.local/bin/meson . meson-build
+cd meson-build
+ninja
+cd ..
+
+${ROOT}/scripts/version.sh check
diff --git a/scripts/version.sh b/scripts/version.sh
new file mode 100755
index 0000000..9ed27a9
--- /dev/null
+++ b/scripts/version.sh
@@ -0,0 +1,260 @@
+#!/bin/sh
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Project configuration variables
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+VERSION_FILE="include/tinyalsa/version.h"
+CHANGELOG_FILE="debian/changelog"
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Scripts internal variables
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+LF="\n"
+PARAMS=""
+DRYRUN=0
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Helper functions
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+die()
+{
+  echo "Error: $@" 1>&2
+  exit 1
+}
+
+print_usage()
+{
+  echo
+  echo "Usage: $0 [OPTIONS] ACTION"
+  echo
+  echo "Available options:"
+  echo "  -s,--script   Format output in \"script\" mode (no trailing newline)."
+  echo "  -d,--dry-run  Does not commit anything to any file, just prints."
+  echo
+  echo "Available actions:"
+  echo "  print   [minor|major|patch]  Print the current version."
+  echo "  release [minor|major|patch]  Bump the specified version part"
+  echo "  check                        Check the changelog latest released"
+  echo "                               version against the version file."
+  echo
+  echo "Please run this script from the project root folder."
+  echo
+}
+
+check_files()
+{
+  [ -f ${VERSION_FILE}   ] || die "No ${VERSION_FILE} found!";
+  [ -f ${CHANGELOG_FILE} ] || die "No ${CHANGELOG_FILE} found!"
+}
+
+# Gets a part of the version from the project version file (version.h).
+# Takes one argument: the matching version identifier in the version file, e.g.
+#   TINYALSA_VERSION_MAJOR
+get_version_part()
+{
+  set -- "$1" "$(grep -m 1 "^#define\([ \t]*\)$1" ${VERSION_FILE} | sed 's/[^0-9]*//g')"
+
+  if [ -z "$2" ]; then
+    die "Could not get $1 from ${VERSION_FILE}"
+  fi
+
+  echo "$2"
+}
+
+
+# Gets the complete version from the version file.
+# Sets VERSION_MAJOR, VERSION_MINOR and VERSION_PATCH globals
+get_version()
+{
+  VERSION_MAJOR=$(get_version_part "TINYALSA_VERSION_MAJOR")
+  VERSION_MINOR=$(get_version_part "TINYALSA_VERSION_MINOR")
+  VERSION_PATCH=$(get_version_part "TINYALSA_VERSION_PATCH")
+}
+
+# Commits the new version part to the version file.
+# Takes two arguments: the version part identifier in the version file and the
+#   new version number. If no arguments, do nothing.
+commit_version_part()
+{
+  if [ -z $1 ] || [ -z $2 ]; then
+    return 0
+  fi
+
+  sed -i "s/\(^#define[ \t]*$1\)[ \t]*\([0-9]*\)/\1 $2/g" ${VERSION_FILE} \
+    || die "Could not commit version for $1";
+
+  [ $(get_version_part $1) = "$2" ] || die "Version check after commit failed for $1"
+
+  return 0;
+}
+
+# Commits the new version to the version file.
+# Takes three arguments, the new version numbers for major, minor and patch
+commit_version()
+{
+  commit_version_part "TINYALSA_VERSION_PATCH" $1
+  commit_version_part "TINYALSA_VERSION_MINOR" $2
+  commit_version_part "TINYALSA_VERSION_MAJOR" $3
+
+  return 0
+}
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Actions implementations / functions
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+print_version()
+{
+  if [ -z $1 ]; then
+    printf "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}${LF}"
+  else
+    case "$1" in
+      major)
+        printf "${VERSION_MAJOR}${LF}"
+        ;;
+      minor)
+        printf "${VERSION_MINOR}${LF}"
+        ;;
+      patch)
+        printf "${VERSION_PATCH}${LF}"
+        ;;
+      *)
+        die "Unknown part \"$1\" (must be one of minor, major and patch)."
+        ;;
+    esac
+  fi
+
+  return 0
+}
+
+bump_version()
+{
+  case "${1:-patch}" in
+    major)
+      VERSION_MAJOR=$((VERSION_MAJOR+1))
+      VERSION_MINOR=0
+      VERSION_PATCH=0
+    ;;
+    minor)
+      VERSION_MINOR=$((VERSION_MINOR+1))
+      VERSION_PATCH=0
+    ;;
+    patch)
+      VERSION_PATCH=$((VERSION_PATCH+1))
+    ;;
+    *)
+      die "Unknown part \"$1\" (must be one of minor, major and patch)."
+    ;;
+  esac
+
+  if [ ${DRYRUN} -ne 1 ]; then
+    commit_version ${VERSION_PATCH} ${VERSION_MINOR} ${VERSION_MAJOR}
+  fi
+
+  print_version
+  return 0
+}
+
+check_version()
+{
+  # set $1 to log version, and $2 to ref version
+  set -- \
+    "$(grep -m 1 "^tinyalsa (" ${CHANGELOG_FILE}| sed "s/[^0-9.]*//g")" \
+    "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
+
+  if [ "$1" != "$2" ]; then
+    die "Changelog version ($1) does not match package version ($2)."
+  fi
+
+  printf "Changelog version ($1) OK!${LF}"
+  return 0
+}
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Command Line parsing
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+parse_command()
+{
+  if [ "$#" -eq "0" ]; then
+    print_usage
+    exit 1
+  fi
+
+  case "$1" in
+    print)
+      get_version
+      print_version "$2"
+      exit $?
+      ;;
+    release)
+      get_version
+      bump_version "$2"
+      exit $?
+      ;;
+    check)
+      get_version
+      check_version
+      exit $?
+      ;;
+    *)
+      die "Unsupported action \"$1\"."
+      ;;
+  esac
+}
+
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+#
+#   Main
+#
+# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+set -e
+trap "set +e" 0
+
+# Checking parameters
+if [ "$#" -eq "0" ]; then
+  print_usage
+  exit 0
+fi
+
+while [ "$#" -ne "0" ]; do
+  case "$1" in
+    -s|--script)
+      unset LF
+      shift
+      ;;
+    -d|--dry-run)
+      DRYRUN=1
+      shift
+      ;;
+    --)
+      shift
+      break
+      ;;
+    -*|--*=)
+      die "Unsupported flag \"$1\"."
+      ;;
+    *)
+      PARAMS="$PARAMS ${1}"
+      shift
+      ;;
+  esac
+done
+
+# set positional arguments in their proper place
+set -- "${PARAMS}"
+
+check_files
+parse_command ${PARAMS}
+
+# The script should never reach this place.
+die "Internal error. Please report this."
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..aaa84b8
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,69 @@
+DESTDIR ?=
+PREFIX ?= /usr/local
+LIBDIR ?= $(PREFIX)/lib
+BINDIR ?= $(PREFIX)/bin
+ifdef DEB_HOST_MULTIARCH
+LIBDIR := $(LIBDIR)/$(DEB_HOST_MULTIARCH)
+endif
+
+CC = $(CROSS_COMPILE)gcc
+AR = $(CROSS_COMPILE)ar
+LD = $(CROSS_COMPILE)gcc
+
+WARNINGS = -Wall -Wextra -Werror -Wfatal-errors
+INCLUDE_DIRS = -I ../include
+override CFLAGS := $(WARNINGS) $(INCLUDE_DIRS) -fPIC $(CFLAGS)
+
+VPATH = ../include/tinyalsa
+OBJECTS = limits.o mixer.o pcm.o pcm_plugin.o pcm_hw.o snd_card_plugin.o mixer_plugin.o mixer_hw.o
+
+LIBVERSION_MAJOR = $(TINYALSA_VERSION_MAJOR)
+LIBVERSION = $(TINYALSA_VERSION)
+
+.PHONY: all
+all: libtinyalsa.a libtinyalsa.so
+
+pcm.o: pcm.c limits.h pcm.h pcm_io.h plugin.h snd_card_plugin.h
+
+pcm_plugin.o: pcm_plugin.c asoundlib.h pcm_io.h plugin.h snd_card_plugin.h
+
+pcm_hw.o: pcm_hw.c asoundlib.h pcm_io.h
+
+limits.o: limits.c limits.h
+
+mixer.o: mixer.c mixer.h mixer_io.h plugin.h
+
+snd_card_plugin.o: snd_card_plugin.c plugin.h snd_card_plugin.h
+
+mixer_plugin.o: mixer_plugin.c mixer_io.h plugin.h snd_card_plugin.h
+
+mixer_hw.o: mixer_hw.c mixer_io.h
+
+libtinyalsa.a: $(OBJECTS)
+	$(AR) $(ARFLAGS) $@ $^
+
+libtinyalsa.so: libtinyalsa.so.$(LIBVERSION_MAJOR)
+	ln -sf $< $@
+
+libtinyalsa.so.$(LIBVERSION_MAJOR): libtinyalsa.so.$(LIBVERSION)
+	ln -sf $< $@
+
+libtinyalsa.so.$(LIBVERSION): $(OBJECTS)
+	$(LD) $(LDFLAGS) -shared -Wl,-soname,libtinyalsa.so.$(LIBVERSION_MAJOR) $^ -o $@
+
+.PHONY: clean
+clean:
+	rm -f libtinyalsa.a
+	rm -f libtinyalsa.so
+	rm -f libtinyalsa.so.$(LIBVERSION_MAJOR)
+	rm -f libtinyalsa.so.$(LIBVERSION)
+	rm -f $(OBJECTS)
+
+.PHONY: install
+install: libtinyalsa.a libtinyalsa.so.$(LIBVERSION_MAJOR)
+	install -d $(DESTDIR)$(LIBDIR)/
+	install libtinyalsa.a $(DESTDIR)$(LIBDIR)/
+	install libtinyalsa.so.$(LIBVERSION) $(DESTDIR)$(LIBDIR)/
+	ln -sf libtinyalsa.so.$(LIBVERSION) $(DESTDIR)$(LIBDIR)/libtinyalsa.so.$(LIBVERSION_MAJOR)
+	ln -sf libtinyalsa.so.$(LIBVERSION_MAJOR) $(DESTDIR)$(LIBDIR)/libtinyalsa.so
+
diff --git a/src/limits.c b/src/limits.c
new file mode 100644
index 0000000..25bd352
--- /dev/null
+++ b/src/limits.c
@@ -0,0 +1,12 @@
+#include <tinyalsa/limits.h>
+
+const struct tinyalsa_unsigned_interval tinyalsa_channels_limit = {
+    .max = TINYALSA_CHANNELS_MAX,
+    .min = TINYALSA_CHANNELS_MIN
+};
+
+const struct tinyalsa_unsigned_interval tinyalsa_frames_limit = {
+    .max = TINYALSA_FRAMES_MAX,
+    .min = TINYALSA_FRAMES_MIN
+};
+
diff --git a/src/mixer.c b/src/mixer.c
new file mode 100644
index 0000000..a45502e
--- /dev/null
+++ b/src/mixer.c
@@ -0,0 +1,1294 @@
+/* mixer.c
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <ctype.h>
+#include <limits.h>
+#include <time.h>
+#include <poll.h>
+
+#include <sys/ioctl.h>
+
+#include <linux/ioctl.h>
+
+#ifndef __force
+#define __force
+#endif
+
+#ifndef __bitwise
+#define __bitwise
+#endif
+
+#ifndef __user
+#define __user
+#endif
+
+#include <sound/asound.h>
+
+#include <tinyalsa/mixer.h>
+#include <tinyalsa/plugin.h>
+
+#include "mixer_io.h"
+
+/** A mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer_ctl {
+    /** The mixer that the mixer control belongs to */
+    struct mixer *mixer;
+    /** Information on the control's value (i.e. type, number of values) */
+    struct snd_ctl_elem_info info;
+    /** A list of string representations of enumerated values (only valid for enumerated controls) */
+    char **ename;
+    /** Pointer to the group that the control belongs to */
+    struct mixer_ctl_group *grp;
+};
+
+struct mixer_ctl_group {
+    /** A continuous array of mixer controls */
+    struct mixer_ctl *ctl;
+    /** The number of mixer controls */
+    unsigned int count;
+    /** The number of events associated with this group */
+    unsigned int event_cnt;
+    /** The operations corresponding to this group */
+    const struct mixer_ops *ops;
+    /** Private data for storing group specific data */
+    void *data;
+};
+
+/** A mixer handle.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer {
+    /** File descriptor for the card */
+    int fd;
+    /** Card information */
+    struct snd_ctl_card_info card_info;
+    /* Hardware (kernel interface) mixer control group */
+    struct mixer_ctl_group *h_grp;
+    /* Virtual (Plugin interface) mixer control group */
+    struct mixer_ctl_group *v_grp;
+    /* Total count of mixer controls from both  groups */
+    unsigned int total_count;
+    /* Flag to track if card information is already retrieved */
+    bool is_card_info_retrieved;
+
+};
+
+static void mixer_cleanup_control(struct mixer_ctl *ctl)
+{
+    unsigned int m;
+
+    if (ctl->ename) {
+        unsigned int max = ctl->info.value.enumerated.items;
+        for (m = 0; m < max; m++)
+            free(ctl->ename[m]);
+        free(ctl->ename);
+    }
+}
+
+static void mixer_grp_close(struct mixer *mixer, struct mixer_ctl_group *grp)
+{
+    unsigned int n;
+
+    if (!grp)
+        return;
+
+    if (grp->ctl) {
+        for (n = 0; n < grp->count; n++)
+            mixer_cleanup_control(&grp->ctl[n]);
+        free(grp->ctl);
+    }
+
+    free(grp);
+
+    mixer->is_card_info_retrieved = false;
+}
+
+/** Closes a mixer returned by @ref mixer_open.
+ * @param mixer A mixer handle.
+ * @ingroup libtinyalsa-mixer
+ */
+void mixer_close(struct mixer *mixer)
+{
+    if (!mixer)
+        return;
+
+    if (mixer->fd >= 0 && mixer->h_grp)
+        mixer->h_grp->ops->close(mixer->h_grp->data);
+    mixer_grp_close(mixer, mixer->h_grp);
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp)
+        mixer->v_grp->ops->close(mixer->v_grp->data);
+    mixer_grp_close(mixer, mixer->v_grp);
+#endif
+
+    free(mixer);
+
+    /* TODO: verify frees */
+}
+
+static void *mixer_realloc_z(void *ptr, size_t curnum, size_t newnum, size_t size)
+{
+        int8_t *newp;
+
+        newp = realloc(ptr, size * newnum);
+        if (!newp)
+            return NULL;
+
+        memset(newp + (curnum * size), 0, (newnum - curnum) * size);
+        return newp;
+}
+
+static int add_controls(struct mixer *mixer, struct mixer_ctl_group *grp)
+{
+    struct snd_ctl_elem_list elist;
+    struct snd_ctl_elem_id *eid = NULL;
+    struct mixer_ctl *ctl;
+    const unsigned int old_count = grp->count;
+    unsigned int new_count;
+    unsigned int n;
+
+    memset(&elist, 0, sizeof(elist));
+    if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_LIST, &elist) < 0)
+        goto fail;
+
+    if (old_count == elist.count)
+        return 0; /* no new controls return unchanged */
+
+    if (old_count > elist.count)
+        return -1; /* driver has removed controls - this is bad */
+
+    ctl = mixer_realloc_z(grp->ctl, old_count, elist.count,
+                          sizeof(struct mixer_ctl));
+    if (!ctl)
+        goto fail;
+
+    grp->ctl = ctl;
+
+    /* ALSA drivers are not supposed to remove or re-order controls that
+     * have already been created so we know that any new controls must
+     * be after the ones we have already collected
+     */
+    new_count = elist.count;
+    elist.space = new_count - old_count; /* controls we haven't seen before */
+    elist.offset = old_count; /* first control we haven't seen */
+
+    eid = calloc(elist.space, sizeof(struct snd_ctl_elem_id));
+    if (!eid)
+        goto fail;
+
+    elist.pids = eid;
+
+    if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_LIST, &elist) < 0)
+        goto fail;
+
+    for (n = old_count; n < new_count; n++) {
+        struct snd_ctl_elem_info *ei = &grp->ctl[n].info;
+        ei->id.numid = eid[n - old_count].numid;
+        if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_INFO, ei) < 0)
+            goto fail_extend;
+        ctl[n].mixer = mixer;
+        ctl[n].grp = grp;
+    }
+
+    grp->count = new_count;
+    mixer->total_count += (new_count - old_count);
+
+    free(eid);
+    return 0;
+
+fail_extend:
+    /* cleanup the control we failed on but leave the ones that were already
+     * added. Also no advantage to shrinking the resized memory block, we
+     * might want to extend the controls again later
+     */
+    mixer_cleanup_control(&ctl[n]);
+
+    grp->count = n;   /* keep controls we successfully added */
+    mixer->total_count += (n - old_count);
+    /* fall through... */
+fail:
+    free(eid);
+    return -1;
+}
+
+static int mixer_grp_open(struct mixer *mixer, unsigned int card, bool is_hw)
+{
+    struct mixer_ctl_group *grp = NULL;
+    const struct mixer_ops *ops = NULL;
+    void *data = NULL;
+    int fd, ret;
+
+    grp = calloc(1, sizeof(*grp));
+    if (!grp)
+        return -ENOMEM;
+
+    if (is_hw) {
+        mixer->fd = -1;
+        fd = mixer_hw_open(card, &data, &ops);
+        if (fd < 0) {
+            ret = fd;
+            goto err_open;
+        }
+        mixer->fd = fd;
+        mixer->h_grp = grp;
+    }
+#ifdef TINYALSA_USES_PLUGINS
+    else {
+        ret = mixer_plugin_open(card, &data, &ops);
+        if (ret < 0)
+            goto err_open;
+        mixer->v_grp = grp;
+    }
+#endif
+    grp->ops = ops;
+    grp->data = data;
+
+    if (!mixer->is_card_info_retrieved) {
+        ret = grp->ops->ioctl(data, SNDRV_CTL_IOCTL_CARD_INFO,
+                              &mixer->card_info);
+        if (ret < 0)
+            goto err_card_info;
+        mixer->is_card_info_retrieved = true;
+    }
+
+    ret = add_controls(mixer, grp);
+    if (ret < 0)
+        goto err_card_info;
+
+    return 0;
+
+err_card_info:
+    grp->ops->close(grp->data);
+
+err_open:
+    free(grp);
+    return ret;
+
+}
+
+/** Opens a mixer for a given card.
+ * @param card The card to open the mixer for.
+ * @returns An initialized mixer handle.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer *mixer_open(unsigned int card)
+{
+    struct mixer *mixer = NULL;
+    int h_status, v_status = -1;
+
+    mixer = calloc(1, sizeof(*mixer));
+    if (!mixer)
+        goto fail;
+
+    h_status = mixer_grp_open(mixer, card, true);
+
+#ifdef TINYALSA_USES_PLUGINS
+    v_status = mixer_grp_open(mixer, card, false);
+#endif
+
+    /* both hw and virtual should fail for mixer_open to fail */
+    if (h_status < 0 && v_status < 0)
+        goto fail;
+
+    return mixer;
+
+fail:
+    if (mixer)
+        mixer_close(mixer);
+
+    return NULL;
+}
+
+/** Some controls may not be present at boot time, e.g. controls from runtime
+ * loadable DSP firmware. This function adds any new controls that have appeared
+ * since mixer_open() or the last call to this function. This assumes a well-
+ * behaved codec driver that does not delete controls that already exists, so
+ * any added controls must be after the last one we already saw. Scanning only
+ * the new controls is much faster than calling mixer_close() then mixer_open()
+ * to re-scan all controls.
+ *
+ * NOTE: this invalidates any struct mixer_ctl pointers previously obtained
+ * from mixer_get_ctl() and mixer_get_ctl_by_name(). Either refresh all your
+ * stored pointers after calling mixer_update_ctls(), or (better) do not
+ * store struct mixer_ctl pointers, instead lookup the control by name or
+ * id only when you are about to use it. The overhead of lookup by id
+ * using mixer_get_ctl() is negligible.
+ * @param mixer An initialized mixer handle.
+ * @returns 0 on success, -1 on failure
+ */
+int mixer_add_new_ctls(struct mixer *mixer)
+{
+    int rc1 = 0, rc2 = 0;
+
+    if (!mixer)
+        return 0;
+
+    /* add the h_grp controls */
+    if (mixer->h_grp)
+        rc1 = add_controls(mixer, mixer->h_grp);
+
+#ifdef TINYALSA_USES_PLUGINS
+    /* add the v_grp controls */
+    if (mixer->v_grp)
+        rc2 = add_controls(mixer, mixer->v_grp);
+#endif
+
+    if (rc1 < 0)
+        return rc1;
+    if (rc2 < 0)
+        return rc2;
+
+    return 0;
+}
+
+/** Gets the name of the mixer's card.
+ * @param mixer An initialized mixer handle.
+ * @returns The name of the mixer's card.
+ * @ingroup libtinyalsa-mixer
+ */
+const char *mixer_get_name(const struct mixer *mixer)
+{
+    return (const char *)mixer->card_info.name;
+}
+
+/** Gets the number of mixer controls for a given mixer.
+ * @param mixer An initialized mixer handle.
+ * @returns The number of mixer controls for the given mixer.
+ * @ingroup libtinyalsa-mixer
+ */
+unsigned int mixer_get_num_ctls(const struct mixer *mixer)
+{
+    if (!mixer)
+        return 0;
+
+    return mixer->total_count;
+}
+
+/** Gets the number of mixer controls, that go by a specified name, for a given mixer.
+ * @param mixer An initialized mixer handle.
+ * @param name The name of the mixer control
+ * @returns The number of mixer controls, specified by @p name, for the given mixer.
+ * @ingroup libtinyalsa-mixer
+ */
+unsigned int mixer_get_num_ctls_by_name(const struct mixer *mixer, const char *name)
+{
+    struct mixer_ctl_group *grp;
+    unsigned int n;
+    unsigned int count = 0;
+    struct mixer_ctl *ctl;
+
+    if (!mixer)
+        return 0;
+
+    if (mixer->h_grp) {
+        grp = mixer->h_grp;
+        ctl = grp->ctl;
+
+        for (n = 0; n < grp->count; n++)
+            if (!strcmp(name, (char*) ctl[n].info.id.name))
+                count++;
+    }
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp) {
+        grp = mixer->v_grp;
+        ctl = grp->ctl;
+
+        for (n = 0; n < grp->count; n++)
+            if (!strcmp(name, (char*) ctl[n].info.id.name))
+                count++;
+    }
+#endif
+
+    return count;
+}
+
+/** Subscribes for the mixer events.
+ * @param mixer A mixer handle.
+ * @param subscribe value indicating subscribe or unsubscribe for events
+ * @returns On success, zero.
+ *  On failure, non-zero.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_subscribe_events(struct mixer *mixer, int subscribe)
+{
+    struct mixer_ctl_group *grp;
+
+    if (mixer->h_grp) {
+        grp = mixer->h_grp;
+        if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_SUBSCRIBE_EVENTS, &subscribe) < 0)
+            return -1;
+    }
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp) {
+        grp = mixer->v_grp;
+        if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_SUBSCRIBE_EVENTS, &subscribe) < 0)
+            return -1;
+    }
+#endif
+    return 0;
+}
+
+/** Wait for mixer events.
+ * @param mixer A mixer handle.
+ * @param timeout timout value
+ * @returns On success, 1.
+ *  On failure, -errno.
+ *  On timeout, 0
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_wait_event(struct mixer *mixer, int timeout)
+{
+    struct pollfd *pfd;
+    struct mixer_ctl_group *grp;
+    int count = 0, num_fds = 0, i, ret = 0;
+
+    if (mixer->fd >= 0)
+        num_fds++;
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp)
+        num_fds++;
+#endif
+
+    pfd = (struct pollfd *)calloc(num_fds, sizeof(struct pollfd));
+    if (!pfd)
+        return -ENOMEM;
+
+    if (mixer->fd >= 0) {
+        pfd[count].fd = mixer->fd;
+        pfd[count].events = POLLIN | POLLOUT | POLLERR | POLLNVAL;
+        count++;
+    }
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp) {
+        grp = mixer->v_grp;
+        if (!grp->ops->get_poll_fd(grp->data, pfd, count)) {
+            pfd[count].events = POLLIN | POLLERR | POLLNVAL;
+            count++;
+        }
+    }
+#endif
+
+    if (!count)
+        goto exit;
+
+    for (;;) {
+        int err;
+        err = poll(pfd, count, timeout);
+        if (err < 0) {
+            ret = -errno;
+            goto exit;
+        }
+        if (!err)
+            goto exit;
+
+        for (i = 0; i < count; i++) {
+            if (pfd[i].revents & (POLLERR | POLLNVAL)) {
+                ret = -EIO;
+                goto exit;
+            }
+            if (pfd[i].revents & (POLLIN | POLLOUT)) {
+                if ((i == 0) && mixer->fd >= 0) {
+                    grp = mixer->h_grp;
+                    grp->event_cnt++;
+                }
+#ifdef TINYALSA_USES_PLUGINS
+                 else {
+                    grp = mixer->v_grp;
+                    grp->event_cnt++;
+                }
+#endif
+                ret = 1;
+                goto exit;
+            }
+        }
+    }
+exit:
+    free(pfd);
+    return ret;
+}
+
+/** Consume a mixer event.
+ * If mixer_subscribe_events has been called,
+ * mixer_wait_event will identify when a control value has changed.
+ * This function will clear a single event from the mixer so that
+ * further events can be alerted.
+ *
+ * @param mixer A mixer handle.
+ * @returns 1 on success. 0, if no pending event. -errno on failure.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_consume_event(struct mixer *mixer)
+{
+    struct mixer_ctl_event ev;
+
+    return mixer_read_event(mixer, &ev);
+}
+
+/** Read a mixer control event.
+ * Try to read an control event from mixer.
+ *
+ * @param mixer A mixer handle.
+ * @param event Output parameter. If there is an event read form the mixer, this function will fill
+ * the event data into it.
+ * @returns 1 on success. 0, if no pending event. -errno on failure.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_read_event(struct mixer *mixer, struct mixer_ctl_event *event)
+{
+    struct mixer_ctl_group *grp = NULL;
+    struct snd_ctl_event ev;
+    ssize_t bytes = 0;
+
+    if (!mixer || !event) {
+        return -EINVAL;
+    }
+
+    if (mixer->h_grp) {
+        if (mixer->h_grp->event_cnt > 0) {
+            grp = mixer->h_grp;
+        }
+    }
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp) {
+        if (mixer->v_grp->event_cnt > 0) {
+            grp = mixer->v_grp;
+        }
+    }
+#endif
+    if (grp) {
+        grp->event_cnt--;
+        bytes = grp->ops->read_event(grp->data, &ev, sizeof(ev));
+
+        if (bytes < 0) {
+            return -errno;
+        }
+
+        if (bytes == sizeof(*event)) {
+            memcpy(event, &ev, sizeof(*event));
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+static unsigned int mixer_grp_get_count(struct mixer_ctl_group *grp)
+{
+    if (!grp)
+        return 0;
+
+    return grp->count;
+}
+
+/** Gets a mixer control handle, by the mixer control's id.
+ * For non-const access, see @ref mixer_get_ctl
+ * @param mixer An initialized mixer handle.
+ * @param id The control's id in the given mixer.
+ * @returns A handle to the mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+const struct mixer_ctl *mixer_get_ctl_const(const struct mixer *mixer, unsigned int id)
+{
+    unsigned int h_count;
+
+    if (!mixer || (id >= mixer->total_count))
+        return NULL;
+
+    h_count = mixer_grp_get_count(mixer->h_grp);
+
+    if (id < h_count)
+        return mixer->h_grp->ctl + id;
+#ifdef TINYALSA_USES_PLUGINS
+    else {
+        unsigned int v_count = mixer_grp_get_count(mixer->v_grp);
+	    if ((id - h_count) < v_count)
+            return mixer->v_grp->ctl + (id - h_count);
+    }
+#endif
+
+    return NULL;
+}
+
+/** Gets a mixer control handle, by the mixer control's id.
+ * For const access, see @ref mixer_get_ctl_const
+ * @param mixer An initialized mixer handle.
+ * @param id The control's id in the given mixer.
+ * @returns A handle to the mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer_ctl *mixer_get_ctl(struct mixer *mixer, unsigned int id)
+{
+    unsigned int h_count;
+
+    if (!mixer || (id >= mixer->total_count))
+        return NULL;
+
+    h_count = mixer_grp_get_count(mixer->h_grp);
+
+    if (id < h_count)
+        return mixer->h_grp->ctl + id;
+#ifdef TINYALSA_USES_PLUGINS
+    else {
+        unsigned int v_count = mixer_grp_get_count(mixer->v_grp);
+	    if ((id - h_count) < v_count)
+            return mixer->v_grp->ctl + (id - h_count);
+    }
+#endif
+    return NULL;
+}
+
+/** Gets the first instance of mixer control handle, by the mixer control's name.
+ * @param mixer An initialized mixer handle.
+ * @param name The control's name in the given mixer.
+ * @returns A handle to the mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer_ctl *mixer_get_ctl_by_name(struct mixer *mixer, const char *name)
+{
+    return mixer_get_ctl_by_name_and_index(mixer, name, 0);
+}
+
+/** Gets an instance of mixer control handle, by the mixer control's name and index.
+ *  For instance, if two controls have the name of 'Volume', then and index of 1 would return the second control.
+ * @param mixer An initialized mixer handle.
+ * @param name The control's name in the given mixer.
+ * @param index The control's index.
+ * @returns A handle to the mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+struct mixer_ctl *mixer_get_ctl_by_name_and_index(struct mixer *mixer,
+                                                  const char *name,
+                                                  unsigned int index)
+{
+    struct mixer_ctl_group *grp;
+    unsigned int n;
+    struct mixer_ctl *ctl;
+
+    if (!mixer)
+        return NULL;
+
+    if (mixer->h_grp) {
+        grp = mixer->h_grp;
+        ctl = grp->ctl;
+
+        for (n = 0; n < grp->count; n++)
+            if (!strcmp(name, (char*) ctl[n].info.id.name))
+                if (index-- == 0)
+                    return ctl + n;
+    }
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (mixer->v_grp) {
+        grp = mixer->v_grp;
+        ctl = grp->ctl;
+
+        for (n = 0; n < grp->count; n++)
+            if (!strcmp(name, (char*) ctl[n].info.id.name))
+                if (index-- == 0)
+                    return ctl + n;
+    }
+#endif
+    return NULL;
+}
+
+/** Updates the control's info.
+ * This is useful for a program that may be idle for a period of time.
+ * @param ctl An initialized control handle.
+ * @ingroup libtinyalsa-mixer
+ */
+void mixer_ctl_update(struct mixer_ctl *ctl)
+{
+    struct mixer_ctl_group *grp;
+
+    if (!ctl)
+        return;
+
+    grp  = ctl->grp;
+    grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_INFO, &ctl->info);
+}
+
+/** Checks the control for TLV Read/Write access.
+ * @param ctl An initialized control handle.
+ * @returns On success, non-zero.
+ *  On failure, zero.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_is_access_tlv_rw(const struct mixer_ctl *ctl)
+{
+    return (ctl->info.access & SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE);
+}
+
+/** Gets the control's ID.
+ * @param ctl An initialized control handle.
+ * @returns On success, the control's ID is returned.
+ *  On error, UINT_MAX is returned instead.
+ * @ingroup libtinyalsa-mixer
+ */
+unsigned int mixer_ctl_get_id(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return UINT_MAX;
+
+    /* numid values start at 1, return a 0-base value that
+     * can be passed to mixer_get_ctl()
+     */
+    return ctl->info.id.numid - 1;
+}
+
+/** Gets the name of the control.
+ * @param ctl An initialized control handle.
+ * @returns On success, the name of the control.
+ *  On error, NULL.
+ * @ingroup libtinyalsa-mixer
+ */
+const char *mixer_ctl_get_name(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return NULL;
+
+    return (const char *)ctl->info.id.name;
+}
+
+/** Gets the value type of the control.
+ * @param ctl An initialized control handle
+ * @returns On success, the type of mixer control.
+ *  On failure, it returns @ref MIXER_CTL_TYPE_UNKNOWN
+ * @ingroup libtinyalsa-mixer
+ */
+enum mixer_ctl_type mixer_ctl_get_type(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return MIXER_CTL_TYPE_UNKNOWN;
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:    return MIXER_CTL_TYPE_BOOL;
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:    return MIXER_CTL_TYPE_INT;
+    case SNDRV_CTL_ELEM_TYPE_ENUMERATED: return MIXER_CTL_TYPE_ENUM;
+    case SNDRV_CTL_ELEM_TYPE_BYTES:      return MIXER_CTL_TYPE_BYTE;
+    case SNDRV_CTL_ELEM_TYPE_IEC958:     return MIXER_CTL_TYPE_IEC958;
+    case SNDRV_CTL_ELEM_TYPE_INTEGER64:  return MIXER_CTL_TYPE_INT64;
+    default:                             return MIXER_CTL_TYPE_UNKNOWN;
+    };
+}
+
+/** Gets the string that describes the value type of the control.
+ * @param ctl An initialized control handle
+ * @returns On success, a string describing type of mixer control.
+ * @ingroup libtinyalsa-mixer
+ */
+const char *mixer_ctl_get_type_string(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return "";
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:    return "BOOL";
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:    return "INT";
+    case SNDRV_CTL_ELEM_TYPE_ENUMERATED: return "ENUM";
+    case SNDRV_CTL_ELEM_TYPE_BYTES:      return "BYTE";
+    case SNDRV_CTL_ELEM_TYPE_IEC958:     return "IEC958";
+    case SNDRV_CTL_ELEM_TYPE_INTEGER64:  return "INT64";
+    default:                             return "Unknown";
+    };
+}
+
+/** Gets the number of values in the control.
+ * @param ctl An initialized control handle
+ * @returns The number of values in the mixer control
+ * @ingroup libtinyalsa-mixer
+ */
+unsigned int mixer_ctl_get_num_values(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return 0;
+
+    return ctl->info.count;
+}
+
+static int percent_to_int(const struct snd_ctl_elem_info *ei, int percent)
+{
+    if ((percent > 100) || (percent < 0)) {
+        return -EINVAL;
+    }
+
+    int range = (ei->value.integer.max - ei->value.integer.min);
+
+    return ei->value.integer.min + (range * percent) / 100;
+}
+
+static int int_to_percent(const struct snd_ctl_elem_info *ei, int value)
+{
+    int range = (ei->value.integer.max - ei->value.integer.min);
+
+    if (range == 0)
+        return 0;
+
+    return ((value - ei->value.integer.min) * 100) / range;
+}
+
+/** Gets a percentage representation of a specified control value.
+ * @param ctl An initialized control handle.
+ * @param id The index of the value within the control.
+ * @returns On success, the percentage representation of the control value.
+ *  On failure, -EINVAL is returned.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_get_percent(const struct mixer_ctl *ctl, unsigned int id)
+{
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_INTEGER))
+        return -EINVAL;
+
+    return int_to_percent(&ctl->info, mixer_ctl_get_value(ctl, id));
+}
+
+/** Sets the value of a control by percent, specified by the value index.
+ * @param ctl An initialized control handle.
+ * @param id The index of the value to set
+ * @param percent A percentage value between 0 and 100.
+ * @returns On success, zero is returned.
+ *  On failure, non-zero is returned.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_set_percent(struct mixer_ctl *ctl, unsigned int id, int percent)
+{
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_INTEGER))
+        return -EINVAL;
+
+    return mixer_ctl_set_value(ctl, id, percent_to_int(&ctl->info, percent));
+}
+
+/** Gets the value of a control.
+ * @param ctl An initialized control handle.
+ * @param id The index of the control value.
+ * @returns On success, the specified value is returned.
+ *  On failure, -EINVAL is returned.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_get_value(const struct mixer_ctl *ctl, unsigned int id)
+{
+    struct mixer_ctl_group *grp;
+    struct snd_ctl_elem_value ev;
+    int ret;
+
+    if (!ctl || (id >= ctl->info.count))
+        return -EINVAL;
+
+    grp = ctl->grp;
+    memset(&ev, 0, sizeof(ev));
+    ev.id.numid = ctl->info.id.numid;
+    ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_READ, &ev);
+    if (ret < 0)
+        return ret;
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+        return !!ev.value.integer.value[id];
+
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:
+        return ev.value.integer.value[id];
+
+    case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+        return ev.value.enumerated.item[id];
+
+    case SNDRV_CTL_ELEM_TYPE_BYTES:
+        return ev.value.bytes.data[id];
+
+    default:
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+/** Gets the contents of a control's value array.
+ * @param ctl An initialized control handle.
+ * @param array A pointer to write the array data to.
+ *  The size of this array must be equal to the number of items in the array
+ *  multiplied by the size of each item.
+ * @param count The number of items in the array.
+ *  This parameter must match the number of items in the control.
+ *  The number of items in the control may be accessed via @ref mixer_ctl_get_num_values
+ * @returns On success, zero.
+ *  On failure, non-zero.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_get_array(const struct mixer_ctl *ctl, void *array, size_t count)
+{
+    struct mixer_ctl_group *grp;
+    struct snd_ctl_elem_value ev;
+    int ret = 0;
+    size_t size;
+    void *source;
+
+    if (!ctl || !count || !array)
+        return -EINVAL;
+
+    grp = ctl->grp;
+
+    if (count > ctl->info.count)
+        return -EINVAL;
+
+    memset(&ev, 0, sizeof(ev));
+    ev.id.numid = ctl->info.id.numid;
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:
+        ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_READ, &ev);
+        if (ret < 0)
+            return ret;
+        size = sizeof(ev.value.integer.value[0]);
+        source = ev.value.integer.value;
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_BYTES:
+        /* check if this is new bytes TLV */
+        if (mixer_ctl_is_access_tlv_rw(ctl)) {
+            struct snd_ctl_tlv *tlv;
+            int ret;
+
+            if (count > SIZE_MAX - sizeof(*tlv))
+                return -EINVAL;
+
+            tlv = calloc(1, sizeof(*tlv) + count);
+            if (!tlv)
+                return -ENOMEM;
+
+            tlv->numid = ctl->info.id.numid;
+            tlv->length = count;
+            ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_TLV_READ, tlv);
+
+            source = tlv->tlv;
+            memcpy(array, source, count);
+
+            free(tlv);
+
+            return ret;
+        } else {
+            ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_READ, &ev);
+            if (ret < 0)
+                return ret;
+            size = sizeof(ev.value.bytes.data[0]);
+            source = ev.value.bytes.data;
+            break;
+        }
+
+    case SNDRV_CTL_ELEM_TYPE_IEC958:
+        size = sizeof(ev.value.iec958);
+        source = &ev.value.iec958;
+        break;
+
+    default:
+        return -EINVAL;
+    }
+
+    memcpy(array, source, size * count);
+
+    return 0;
+}
+
+/** Sets the value of a control, specified by the value index.
+ * @param ctl An initialized control handle.
+ * @param id The index of the value within the control.
+ * @param value The value to set.
+ *  This must be in a range specified by @ref mixer_ctl_get_range_min
+ *  and @ref mixer_ctl_get_range_max.
+ * @returns On success, zero is returned.
+ *  On failure, non-zero is returned.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_set_value(struct mixer_ctl *ctl, unsigned int id, int value)
+{
+    struct mixer_ctl_group *grp;
+    struct snd_ctl_elem_value ev;
+    int ret;
+
+    if (!ctl || (id >= ctl->info.count))
+        return -EINVAL;
+
+    grp = ctl->grp;
+    memset(&ev, 0, sizeof(ev));
+    ev.id.numid = ctl->info.id.numid;
+    ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_READ, &ev);
+    if (ret < 0)
+        return ret;
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+        ev.value.integer.value[id] = !!value;
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:
+        if ((value < mixer_ctl_get_range_min(ctl)) ||
+            (value > mixer_ctl_get_range_max(ctl))) {
+            return -EINVAL;
+        }
+
+        ev.value.integer.value[id] = value;
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+        ev.value.enumerated.item[id] = value;
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_BYTES:
+        ev.value.bytes.data[id] = value;
+        break;
+
+    default:
+        return -EINVAL;
+    }
+
+    return grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_WRITE, &ev);
+}
+
+/** Sets the contents of a control's value array.
+ * @param ctl An initialized control handle.
+ * @param array The array containing control values.
+ * @param count The number of values in the array.
+ *  This must match the number of values in the control.
+ *  The number of values in a control may be accessed via @ref mixer_ctl_get_num_values
+ * @returns On success, zero.
+ *  On failure, non-zero.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_set_array(struct mixer_ctl *ctl, const void *array, size_t count)
+{
+    struct mixer_ctl_group *grp;
+    struct snd_ctl_elem_value ev;
+    size_t size;
+    void *dest;
+
+    if ((!ctl) || !count || !array)
+        return -EINVAL;
+
+    grp = ctl->grp;
+
+    if (count > ctl->info.count)
+        return -EINVAL;
+
+    memset(&ev, 0, sizeof(ev));
+    ev.id.numid = ctl->info.id.numid;
+
+    switch (ctl->info.type) {
+    case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:
+        size = sizeof(ev.value.integer.value[0]);
+        dest = ev.value.integer.value;
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_BYTES:
+        /* check if this is new bytes TLV */
+        if (mixer_ctl_is_access_tlv_rw(ctl)) {
+            struct snd_ctl_tlv *tlv;
+            int ret = 0;
+
+            if (count > SIZE_MAX - sizeof(*tlv))
+                return -EINVAL;
+
+            tlv = calloc(1, sizeof(*tlv) + count);
+            if (!tlv)
+                return -ENOMEM;
+
+            tlv->numid = ctl->info.id.numid;
+            tlv->length = count;
+            memcpy(tlv->tlv, array, count);
+
+            ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_TLV_WRITE, tlv);
+            free(tlv);
+
+            return ret;
+        } else {
+            size = sizeof(ev.value.bytes.data[0]);
+            dest = ev.value.bytes.data;
+        }
+        break;
+
+    case SNDRV_CTL_ELEM_TYPE_IEC958:
+        size = sizeof(ev.value.iec958);
+        dest = &ev.value.iec958;
+        break;
+
+    default:
+        return -EINVAL;
+    }
+
+    memcpy(dest, array, size * count);
+
+    return grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_WRITE, &ev);
+}
+
+/** Gets the minimum value of an control.
+ * The control must have an integer type.
+ * The type of the control can be checked with @ref mixer_ctl_get_type.
+ * @param ctl An initialized control handle.
+ * @returns On success, the minimum value of the control.
+ *  On failure, -EINVAL.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_get_range_min(const struct mixer_ctl *ctl)
+{
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_INTEGER))
+        return -EINVAL;
+
+    return ctl->info.value.integer.min;
+}
+
+/** Gets the maximum value of an control.
+ * The control must have an integer type.
+ * The type of the control can be checked with @ref mixer_ctl_get_type.
+ * @param ctl An initialized control handle.
+ * @returns On success, the maximum value of the control.
+ *  On failure, -EINVAL.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_get_range_max(const struct mixer_ctl *ctl)
+{
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_INTEGER))
+        return -EINVAL;
+
+    return ctl->info.value.integer.max;
+}
+
+/** Get the number of enumerated items in the control.
+ * @param ctl An initialized control handle.
+ * @returns The number of enumerated items in the control.
+ * @ingroup libtinyalsa-mixer
+ */
+unsigned int mixer_ctl_get_num_enums(const struct mixer_ctl *ctl)
+{
+    if (!ctl)
+        return 0;
+
+    return ctl->info.value.enumerated.items;
+}
+
+int mixer_ctl_fill_enum_string(struct mixer_ctl *ctl)
+{
+    struct mixer_ctl_group *grp = ctl->grp;
+    struct snd_ctl_elem_info tmp;
+    unsigned int m;
+    char **enames;
+
+    if (ctl->ename) {
+        return 0;
+    }
+
+    enames = calloc(ctl->info.value.enumerated.items, sizeof(char*));
+    if (!enames)
+        goto fail;
+    for (m = 0; m < ctl->info.value.enumerated.items; m++) {
+        memset(&tmp, 0, sizeof(tmp));
+        tmp.id.numid = ctl->info.id.numid;
+        tmp.value.enumerated.item = m;
+        if (grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_INFO, &tmp) < 0)
+            goto fail;
+        enames[m] = strdup(tmp.value.enumerated.name);
+        if (!enames[m])
+            goto fail;
+    }
+    ctl->ename = enames;
+    return 0;
+
+fail:
+    if (enames) {
+        for (m = 0; m < ctl->info.value.enumerated.items; m++) {
+            if (enames[m]) {
+                free(enames[m]);
+            }
+        }
+        free(enames);
+    }
+    return -1;
+}
+
+/** Gets the string representation of an enumerated item.
+ * @param ctl An initialized control handle.
+ * @param enum_id The index of the enumerated value.
+ * @returns A string representation of the enumerated item.
+ * @ingroup libtinyalsa-mixer
+ */
+const char *mixer_ctl_get_enum_string(struct mixer_ctl *ctl,
+                                      unsigned int enum_id)
+{
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_ENUMERATED) ||
+        (enum_id >= ctl->info.value.enumerated.items) ||
+        mixer_ctl_fill_enum_string(ctl) != 0)
+        return NULL;
+
+    return (const char *)ctl->ename[enum_id];
+}
+
+/** Set an enumeration value by string value.
+ * @param ctl An enumerated mixer control.
+ * @param string The string representation of an enumeration.
+ * @returns On success, zero.
+ *  On failure, zero.
+ * @ingroup libtinyalsa-mixer
+ */
+int mixer_ctl_set_enum_by_string(struct mixer_ctl *ctl, const char *string)
+{
+    struct mixer_ctl_group *grp;
+    unsigned int i, num_enums;
+    struct snd_ctl_elem_value ev;
+    int ret;
+
+    if (!ctl || (ctl->info.type != SNDRV_CTL_ELEM_TYPE_ENUMERATED) ||
+        mixer_ctl_fill_enum_string(ctl) != 0)
+        return -EINVAL;
+
+    grp = ctl->grp;
+    num_enums = ctl->info.value.enumerated.items;
+    for (i = 0; i < num_enums; i++) {
+        if (!strcmp(string, ctl->ename[i])) {
+            memset(&ev, 0, sizeof(ev));
+            ev.value.enumerated.item[0] = i;
+            ev.id.numid = ctl->info.id.numid;
+            ret = grp->ops->ioctl(grp->data, SNDRV_CTL_IOCTL_ELEM_WRITE, &ev);
+            if (ret < 0)
+                return ret;
+            return 0;
+        }
+    }
+
+    return -EINVAL;
+}
diff --git a/src/mixer_hw.c b/src/mixer_hw.c
new file mode 100644
index 0000000..da5a390
--- /dev/null
+++ b/src/mixer_hw.c
@@ -0,0 +1,121 @@
+/* mixer_hw.c
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <ctype.h>
+#include <poll.h>
+
+#include <sys/ioctl.h>
+
+#include <linux/ioctl.h>
+#include <sound/asound.h>
+
+#include "mixer_io.h"
+
+/** Store the hardware (kernel interface) mixer data */
+struct mixer_hw_data {
+    /* Card number for the mixer */
+    unsigned int card;
+    /* File descriptor of the mixer device node */
+    int fd;
+};
+
+static void mixer_hw_close(void *data)
+{
+    struct mixer_hw_data *hw_data = data;
+
+    if (!hw_data)
+        return;
+
+    if (hw_data->fd >= 0)
+        close(hw_data->fd);
+
+    hw_data->fd = -1;
+    free(hw_data);
+    hw_data = NULL;
+}
+
+static int mixer_hw_ioctl(void *data, unsigned int cmd, ...)
+{
+    struct mixer_hw_data *hw_data = data;
+    va_list ap;
+    void *arg;
+
+    va_start(ap, cmd);
+    arg = va_arg(ap, void *);
+    va_end(ap);
+
+    return ioctl(hw_data->fd, cmd, arg);
+}
+
+static ssize_t mixer_hw_read_event(void *data, struct snd_ctl_event *ev,
+                                   size_t size)
+{
+    struct mixer_hw_data *hw_data = data;
+
+    return read(hw_data->fd, ev, size);
+}
+
+static const struct mixer_ops mixer_hw_ops = {
+    .close = mixer_hw_close,
+    .ioctl = mixer_hw_ioctl,
+    .read_event = mixer_hw_read_event,
+};
+
+int mixer_hw_open(unsigned int card, void **data,
+                  const struct mixer_ops **ops)
+{
+    struct mixer_hw_data *hw_data;
+    int fd;
+    char fn[256];
+
+    snprintf(fn, sizeof(fn), "/dev/snd/controlC%u", card);
+    fd = open(fn, O_RDWR);
+    if (fd < 0)
+        return fd;
+
+    hw_data = calloc(1, sizeof(*hw_data));
+    if (!hw_data)
+        return -1;
+
+    hw_data->card = card;
+    hw_data->fd = fd;
+    *data = hw_data;
+    *ops = &mixer_hw_ops;
+
+    return fd;
+}
diff --git a/src/mixer_io.h b/src/mixer_io.h
new file mode 100644
index 0000000..bb3bc44
--- /dev/null
+++ b/src/mixer_io.h
@@ -0,0 +1,51 @@
+/* mixer_io.h
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#ifndef TINYALSA_SRC_MIXER_H
+#define TINYALSA_SRC_MIXER_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sound/asound.h>
+
+struct mixer_ops;
+
+int mixer_hw_open(unsigned int card, void **data,
+                  const struct mixer_ops **ops);
+int mixer_plugin_open(unsigned int card, void **data,
+                      const struct mixer_ops **ops);
+
+struct mixer_ops {
+    void (*close) (void *data);
+    int (*get_poll_fd) (void *data, struct pollfd *pfd, int count);
+    ssize_t (*read_event) (void *data, struct snd_ctl_event *ev, size_t size);
+    int (*ioctl) (void *data, unsigned int cmd, ...);
+};
+
+#endif /* TINYALSA_SRC_MIXER_H */
diff --git a/src/mixer_plugin.c b/src/mixer_plugin.c
new file mode 100644
index 0000000..34117a9
--- /dev/null
+++ b/src/mixer_plugin.c
@@ -0,0 +1,475 @@
+/* mixer_plugin.c
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#include <tinyalsa/plugin.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <ctype.h>
+#include <poll.h>
+#include <dlfcn.h>
+#include <string.h>
+#include <sys/eventfd.h>
+#include <sys/ioctl.h>
+
+#include <linux/ioctl.h>
+#include <sound/asound.h>
+
+#include "snd_card_plugin.h"
+#include "mixer_io.h"
+
+/** Encapulates the mixer plugin specific data */
+struct mixer_plug_data {
+    /** Card number associated with the plugin */
+    int card;
+    /** Device node for mixer */
+    void *mixer_node;
+    /** Pointer to the plugin's ops */
+    const struct mixer_plugin_ops *ops;
+    /** Pointer to plugin responsible to service the controls */
+    struct mixer_plugin *plugin;
+    /** Handle to the plugin library */
+    void *dl_hdl;
+};
+
+static int mixer_plug_get_elem_id(struct mixer_plug_data *plug_data,
+                struct snd_ctl_elem_id *id, unsigned int offset)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+
+    if (offset >= plugin->num_controls) {
+        fprintf(stderr, "%s: invalid offset %u\n",
+				__func__, offset);
+        return -EINVAL;
+    }
+
+    ctl = plugin->controls + offset;
+    id->numid = offset;
+    id->iface = ctl->iface;
+
+    strncpy((char *)id->name, (char *)ctl->name,
+            sizeof(id->name));
+
+    return 0;
+}
+
+static int mixer_plug_info_enum(struct snd_control *ctl,
+                struct snd_ctl_elem_info *einfo)
+{
+    struct snd_value_enum *val = ctl->value;
+
+    einfo->count = 1;
+    einfo->value.enumerated.items = val->items;
+
+    if (einfo->value.enumerated.item >= val->items)
+        return -EINVAL;
+
+    strncpy(einfo->value.enumerated.name,
+            val->texts[einfo->value.enumerated.item],
+            sizeof(einfo->value.enumerated.name));
+
+    return 0;
+}
+
+static int mixer_plug_info_bytes(struct snd_control *ctl,
+                struct snd_ctl_elem_info *einfo)
+{
+    struct snd_value_bytes *val;
+    struct snd_value_tlv_bytes *val_tlv;
+
+    if (ctl->access & SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE) {
+        val_tlv = ctl->value;
+        einfo->count = val_tlv->size;
+    } else {
+        val = ctl->value;
+        einfo->count = val->size;
+    }
+
+    return 0;
+}
+
+static int mixer_plug_info_integer(struct snd_control *ctl,
+                struct snd_ctl_elem_info *einfo)
+{
+    struct snd_value_int *val = ctl->value;
+
+    einfo->count = val->count;
+    einfo->value.integer.min = val->min;
+    einfo->value.integer.max = val->max;
+    einfo->value.integer.step = val->step;
+
+    return 0;
+}
+
+void mixer_plug_notifier_cb(struct mixer_plugin *plugin)
+{
+    plugin->event_cnt++;
+    eventfd_write(plugin->eventfd, 1);
+}
+
+/* In consume_event/read, do not call eventfd_read until all events are read from list.
+   This will make poll getting unblocked until all events are read */
+static ssize_t mixer_plug_read_event(void *data, struct snd_ctl_event *ev, size_t size)
+{
+    struct mixer_plug_data *plug_data = data;
+    struct mixer_plugin *plugin = plug_data->plugin;
+    eventfd_t evfd;
+    ssize_t result = 0;
+
+    result = plug_data->ops->read_event(plugin, ev, size);
+
+    if (result > 0) {
+        plugin->event_cnt -=  result / sizeof(struct snd_ctl_event);
+        if (plugin->event_cnt == 0)
+            eventfd_read(plugin->eventfd, &evfd);
+    }
+
+    return result;
+}
+
+static int mixer_plug_subscribe_events(struct mixer_plug_data *plug_data,
+                int *subscribe)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    eventfd_t evfd;
+
+    if (*subscribe < 0 || *subscribe > 1) {
+        *subscribe = plugin->subscribed;
+        return -EINVAL;
+    }
+
+    if (*subscribe && !plugin->subscribed) {
+        plug_data->ops->subscribe_events(plugin, &mixer_plug_notifier_cb);
+    } else if (plugin->subscribed && !*subscribe) {
+        plug_data->ops->subscribe_events(plugin, NULL);
+
+        if (plugin->event_cnt)
+            eventfd_read(plugin->eventfd, &evfd);
+
+        plugin->event_cnt = 0;
+    }
+
+    plugin->subscribed = *subscribe;
+    return 0;
+}
+
+static int mixer_plug_get_poll_fd(void *data, struct pollfd *pfd, int count)
+{
+    struct mixer_plug_data *plug_data = data;
+    struct mixer_plugin *plugin = plug_data->plugin;
+
+    if (plugin->eventfd != -1) {
+        pfd[count].fd = plugin->eventfd;
+        return 0;
+    }
+    return -ENODEV;
+}
+
+static int mixer_plug_tlv_write(struct mixer_plug_data *plug_data,
+                struct snd_ctl_tlv *tlv)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+    struct snd_value_tlv_bytes *val_tlv;
+
+    ctl = plugin->controls + tlv->numid;
+    val_tlv = ctl->value;
+
+    return val_tlv->put(plugin, ctl, tlv);
+}
+
+static int mixer_plug_tlv_read(struct mixer_plug_data *plug_data,
+                struct snd_ctl_tlv *tlv)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+    struct snd_value_tlv_bytes *val_tlv;
+
+    ctl = plugin->controls + tlv->numid;
+    val_tlv = ctl->value;
+
+    return val_tlv->get(plugin, ctl, tlv);
+}
+
+static int mixer_plug_elem_write(struct mixer_plug_data *plug_data,
+                struct snd_ctl_elem_value *ev)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+    int ret;
+
+    ret = mixer_plug_get_elem_id(plug_data, &ev->id, ev->id.numid);
+    if (ret < 0)
+        return ret;
+
+    ctl = plugin->controls + ev->id.numid;
+
+    return ctl->put(plugin, ctl, ev);
+}
+
+static int mixer_plug_elem_read(struct mixer_plug_data *plug_data,
+                struct snd_ctl_elem_value *ev)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+    int ret;
+
+    ret = mixer_plug_get_elem_id(plug_data, &ev->id, ev->id.numid);
+    if (ret < 0)
+        return ret;
+
+    ctl = plugin->controls + ev->id.numid;
+
+    return ctl->get(plugin, ctl, ev);
+
+}
+
+static int mixer_plug_get_elem_info(struct mixer_plug_data *plug_data,
+                struct snd_ctl_elem_info *einfo)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    struct snd_control *ctl;
+    int ret;
+
+    ret = mixer_plug_get_elem_id(plug_data, &einfo->id,
+                    einfo->id.numid);
+    if (ret < 0)
+        return ret;
+
+    ctl = plugin->controls + einfo->id.numid;
+    einfo->type = ctl->type;
+    einfo->access = ctl->access;
+
+    switch (einfo->type) {
+    case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+        ret = mixer_plug_info_enum(ctl, einfo);
+        if (ret < 0)
+            return ret;
+        break;
+    case SNDRV_CTL_ELEM_TYPE_BYTES:
+        ret = mixer_plug_info_bytes(ctl, einfo);
+        if (ret < 0)
+            return ret;
+        break;
+    case SNDRV_CTL_ELEM_TYPE_INTEGER:
+        ret = mixer_plug_info_integer(ctl, einfo);
+        if (ret < 0)
+            return ret;
+        break;
+    default:
+        fprintf(stderr,"%s: unknown type %d\n",
+				__func__, einfo->type);
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+static int mixer_plug_get_elem_list(struct mixer_plug_data *plug_data,
+                struct snd_ctl_elem_list *elist)
+{
+    struct mixer_plugin *plugin = plug_data->plugin;
+    unsigned int avail;
+    struct snd_ctl_elem_id *id;
+    int ret;
+
+    elist->count = plugin->num_controls;
+    elist->used = 0;
+    avail = elist->space;
+
+    while (avail > 0) {
+        id = elist->pids + elist->used;
+        ret = mixer_plug_get_elem_id(plug_data, id, elist->used);
+        if (ret < 0)
+            return ret;
+
+        avail--;
+        elist->used++;
+    }
+
+    return 0;
+}
+
+static int mixer_plug_get_card_info(struct mixer_plug_data *plug_data,
+                struct snd_ctl_card_info *card_info)
+{
+    /*TODO: Fill card_info here from snd-card-def */
+    memset(card_info, 0, sizeof(*card_info));
+    card_info->card = plug_data->card;
+
+    return 0;
+}
+
+static void mixer_plug_close(void *data)
+{
+    struct mixer_plug_data *plug_data = data;
+    struct mixer_plugin *plugin = plug_data->plugin;
+    eventfd_t evfd;
+
+    if (plugin->event_cnt)
+        eventfd_read(plugin->eventfd, &evfd);
+
+    plug_data->ops->close(&plugin);
+    dlclose(plug_data->dl_hdl);
+
+    free(plug_data);
+    plug_data = NULL;
+}
+
+static int mixer_plug_ioctl(void *data, unsigned int cmd, ...)
+{
+    struct mixer_plug_data *plug_data = data;
+    int ret;
+    va_list ap;
+    void *arg;
+
+    va_start(ap, cmd);
+    arg = va_arg(ap, void *);
+    va_end(ap);
+
+    switch (cmd) {
+    case SNDRV_CTL_IOCTL_CARD_INFO:
+        ret = mixer_plug_get_card_info(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_ELEM_LIST:
+        ret = mixer_plug_get_elem_list(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_ELEM_INFO:
+        ret = mixer_plug_get_elem_info(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_ELEM_READ:
+        ret = mixer_plug_elem_read(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_ELEM_WRITE:
+        ret = mixer_plug_elem_write(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_TLV_READ:
+        ret = mixer_plug_tlv_read(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_TLV_WRITE:
+        ret = mixer_plug_tlv_write(plug_data, arg);
+        break;
+    case SNDRV_CTL_IOCTL_SUBSCRIBE_EVENTS:
+        ret = mixer_plug_subscribe_events(plug_data, arg);
+        break;
+    default:
+        /* TODO: plugin should support ioctl */
+        ret = -EFAULT;
+        break;
+    }
+
+    return ret;
+}
+
+static const struct mixer_ops mixer_plug_ops = {
+    .close = mixer_plug_close,
+    .read_event = mixer_plug_read_event,
+    .get_poll_fd = mixer_plug_get_poll_fd,
+    .ioctl = mixer_plug_ioctl,
+};
+
+int mixer_plugin_open(unsigned int card, void **data,
+                      const struct mixer_ops **ops)
+{
+    struct mixer_plug_data *plug_data;
+    struct mixer_plugin *plugin = NULL;
+    void *dl_hdl;
+    char *so_name;
+    int ret;
+
+    plug_data = calloc(1, sizeof(*plug_data));
+    if (!plug_data)
+        return -ENOMEM;
+
+    plug_data->mixer_node = snd_utils_open_mixer(card);
+    if (!plug_data->mixer_node) {
+        /* Do not print error here.
+         * It is valid for card to not have virtual mixer node
+         */
+        goto err_get_mixer_node;
+    }
+
+    ret = snd_utils_get_str(plug_data->mixer_node, "so-name",
+                               &so_name);
+    if(ret) {
+        fprintf(stderr, "%s: mixer so-name not found for card %u\n",
+                __func__, card);
+        goto err_get_lib_name;
+
+    }
+
+    dl_hdl = dlopen(so_name, RTLD_NOW);
+    if (!dl_hdl) {
+        fprintf(stderr, "%s: unable to open %s\n",
+                __func__, so_name);
+        goto err_dlopen;
+    }
+
+    dlerror();
+    plug_data->ops = dlsym(dl_hdl, "mixer_plugin_ops");
+    if (!plug_data->ops) {
+        fprintf(stderr, "%s: dlsym open fn failed: %s\n",
+                __func__, dlerror());
+        goto err_ops;
+    }
+
+    ret = plug_data->ops->open(&plugin, card);
+    if (ret) {
+        fprintf(stderr, "%s: failed to open plugin, err: %d\n",
+                __func__, ret);
+        goto err_ops;
+    }
+
+    plug_data->plugin = plugin;
+    plug_data->card = card;
+    plug_data->dl_hdl = dl_hdl;
+    plugin->eventfd = eventfd(0, 0);
+
+    *data = plug_data;
+    *ops = &mixer_plug_ops;
+
+    return 0;
+
+err_ops:
+    dlclose(dl_hdl);
+err_dlopen:
+err_get_lib_name:
+    snd_utils_close_dev_node(plug_data->mixer_node);
+err_get_mixer_node:
+    free(plug_data);
+    return -1;
+}
diff --git a/src/pcm.c b/src/pcm.c
new file mode 100644
index 0000000..98ca9eb
--- /dev/null
+++ b/src/pcm.c
@@ -0,0 +1,1770 @@
+/* pcm.c
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <poll.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <time.h>
+#include <limits.h>
+
+#include <linux/ioctl.h>
+
+#ifndef __force
+#define __force
+#endif
+
+#ifndef __bitwise
+#define __bitwise
+#endif
+
+#ifndef __user
+#define __user
+#endif
+
+#include <sound/asound.h>
+
+#include <tinyalsa/pcm.h>
+#include <tinyalsa/limits.h>
+#include "pcm_io.h"
+#include "snd_card_plugin.h"
+
+#ifndef PARAM_MAX
+#define PARAM_MAX SNDRV_PCM_HW_PARAM_LAST_INTERVAL
+#endif /* PARAM_MAX */
+
+#ifndef SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP
+#define SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP (1<<2)
+#endif /* SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP */
+
+/* Logs information into a string; follows snprintf() in that
+ * offset may be greater than size, and though no characters are copied
+ * into string, characters are still counted into offset. */
+#define STRLOG(string, offset, size, ...) \
+    do { int temp, clipoffset = offset > size ? size : offset; \
+         temp = snprintf(string + clipoffset, size - clipoffset, __VA_ARGS__); \
+         if (temp > 0) offset += temp; } while (0)
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+#endif
+
+/* refer to SNDRV_PCM_ACCESS_##index in sound/asound.h. */
+static const char * const access_lookup[] = {
+        "MMAP_INTERLEAVED",
+        "MMAP_NONINTERLEAVED",
+        "MMAP_COMPLEX",
+        "RW_INTERLEAVED",
+        "RW_NONINTERLEAVED",
+};
+
+/* refer to SNDRV_PCM_FORMAT_##index in sound/asound.h. */
+static const char * const format_lookup[] = {
+        /*[0] =*/ "S8",
+        "U8",
+        "S16_LE",
+        "S16_BE",
+        "U16_LE",
+        "U16_BE",
+        "S24_LE",
+        "S24_BE",
+        "U24_LE",
+        "U24_BE",
+        "S32_LE",
+        "S32_BE",
+        "U32_LE",
+        "U32_BE",
+        "FLOAT_LE",
+        "FLOAT_BE",
+        "FLOAT64_LE",
+        "FLOAT64_BE",
+        "IEC958_SUBFRAME_LE",
+        "IEC958_SUBFRAME_BE",
+        "MU_LAW",
+        "A_LAW",
+        "IMA_ADPCM",
+        "MPEG",
+        /*[24] =*/ "GSM",
+        /* gap */
+        [31] = "SPECIAL",
+        "S24_3LE",
+        "S24_3BE",
+        "U24_3LE",
+        "U24_3BE",
+        "S20_3LE",
+        "S20_3BE",
+        "U20_3LE",
+        "U20_3BE",
+        "S18_3LE",
+        "S18_3BE",
+        "U18_3LE",
+        /*[43] =*/ "U18_3BE",
+#if 0
+        /* recent additions, may not be present on local asound.h */
+        "G723_24",
+        "G723_24_1B",
+        "G723_40",
+        "G723_40_1B",
+        "DSD_U8",
+        "DSD_U16_LE",
+#endif
+};
+
+/* refer to SNDRV_PCM_SUBFORMAT_##index in sound/asound.h. */
+static const char * const subformat_lookup[] = {
+        "STD",
+};
+
+static inline int param_is_mask(int p)
+{
+    return (p >= SNDRV_PCM_HW_PARAM_FIRST_MASK) &&
+        (p <= SNDRV_PCM_HW_PARAM_LAST_MASK);
+}
+
+static inline int param_is_interval(int p)
+{
+    return (p >= SNDRV_PCM_HW_PARAM_FIRST_INTERVAL) &&
+        (p <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL);
+}
+
+static inline const struct snd_interval *param_get_interval(const struct snd_pcm_hw_params *p, int n)
+{
+    return &(p->intervals[n - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]);
+}
+
+static inline struct snd_interval *param_to_interval(struct snd_pcm_hw_params *p, int n)
+{
+    return &(p->intervals[n - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]);
+}
+
+static inline struct snd_mask *param_to_mask(struct snd_pcm_hw_params *p, int n)
+{
+    return &(p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK]);
+}
+
+static void param_set_mask(struct snd_pcm_hw_params *p, int n, unsigned int bit)
+{
+    if (bit >= SNDRV_MASK_MAX)
+        return;
+    if (param_is_mask(n)) {
+        struct snd_mask *m = param_to_mask(p, n);
+        m->bits[0] = 0;
+        m->bits[1] = 0;
+        m->bits[bit >> 5] |= (1 << (bit & 31));
+    }
+}
+
+static void param_set_min(struct snd_pcm_hw_params *p, int n, unsigned int val)
+{
+    if (param_is_interval(n)) {
+        struct snd_interval *i = param_to_interval(p, n);
+        i->min = val;
+    }
+}
+
+static unsigned int param_get_min(const struct snd_pcm_hw_params *p, int n)
+{
+    if (param_is_interval(n)) {
+        const struct snd_interval *i = param_get_interval(p, n);
+        return i->min;
+    }
+    return 0;
+}
+
+static unsigned int param_get_max(const struct snd_pcm_hw_params *p, int n)
+{
+    if (param_is_interval(n)) {
+        const struct snd_interval *i = param_get_interval(p, n);
+        return i->max;
+    }
+    return 0;
+}
+
+static void param_set_int(struct snd_pcm_hw_params *p, int n, unsigned int val)
+{
+    if (param_is_interval(n)) {
+        struct snd_interval *i = param_to_interval(p, n);
+        i->min = val;
+        i->max = val;
+        i->integer = 1;
+    }
+}
+
+static unsigned int param_get_int(struct snd_pcm_hw_params *p, int n)
+{
+    if (param_is_interval(n)) {
+        struct snd_interval *i = param_to_interval(p, n);
+        if (i->integer)
+            return i->max;
+    }
+    return 0;
+}
+
+static void param_init(struct snd_pcm_hw_params *p)
+{
+    int n;
+
+    memset(p, 0, sizeof(*p));
+    for (n = SNDRV_PCM_HW_PARAM_FIRST_MASK;
+         n <= SNDRV_PCM_HW_PARAM_LAST_MASK; n++) {
+            struct snd_mask *m = param_to_mask(p, n);
+            m->bits[0] = ~0;
+            m->bits[1] = ~0;
+    }
+    for (n = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL;
+         n <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; n++) {
+            struct snd_interval *i = param_to_interval(p, n);
+            i->min = 0;
+            i->max = ~0;
+    }
+    p->rmask = ~0U;
+    p->cmask = 0;
+    p->info = ~0U;
+}
+
+static unsigned int pcm_format_to_alsa(enum pcm_format format)
+{
+    switch (format) {
+
+    case PCM_FORMAT_S8:
+        return SNDRV_PCM_FORMAT_S8;
+
+    default:
+    case PCM_FORMAT_S16_LE:
+        return SNDRV_PCM_FORMAT_S16_LE;
+    case PCM_FORMAT_S16_BE:
+        return SNDRV_PCM_FORMAT_S16_BE;
+
+    case PCM_FORMAT_S24_LE:
+        return SNDRV_PCM_FORMAT_S24_LE;
+    case PCM_FORMAT_S24_BE:
+        return SNDRV_PCM_FORMAT_S24_BE;
+
+    case PCM_FORMAT_S24_3LE:
+        return SNDRV_PCM_FORMAT_S24_3LE;
+    case PCM_FORMAT_S24_3BE:
+        return SNDRV_PCM_FORMAT_S24_3BE;
+
+    case PCM_FORMAT_S32_LE:
+        return SNDRV_PCM_FORMAT_S32_LE;
+    case PCM_FORMAT_S32_BE:
+        return SNDRV_PCM_FORMAT_S32_BE;
+    };
+}
+
+#define PCM_ERROR_MAX 128
+
+/** A PCM handle.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm {
+    /** The PCM's file descriptor */
+    int fd;
+    /** Flags that were passed to @ref pcm_open */
+    unsigned int flags;
+    /** The number of (under/over)runs that have occured */
+    int xruns;
+    /** Size of the buffer */
+    unsigned int buffer_size;
+    /** The boundary for ring buffer pointers */
+    unsigned int boundary;
+    /** Description of the last error that occured */
+    char error[PCM_ERROR_MAX];
+    /** Configuration that was passed to @ref pcm_open */
+    struct pcm_config config;
+    struct snd_pcm_mmap_status *mmap_status;
+    struct snd_pcm_mmap_control *mmap_control;
+    struct snd_pcm_sync_ptr *sync_ptr;
+    void *mmap_buffer;
+    unsigned int noirq_frames_per_msec;
+    /** The delay of the PCM, in terms of frames */
+    long pcm_delay;
+    /** The subdevice corresponding to the PCM */
+    unsigned int subdevice;
+    /** Pointer to the pcm ops, either hw or plugin */
+    const struct pcm_ops *ops;
+    /** Private data for pcm_hw or pcm_plugin */
+    void *data;
+    /** Pointer to the pcm node from snd card definition */
+    struct snd_node *snd_node;
+};
+
+static int oops(struct pcm *pcm, int e, const char *fmt, ...)
+{
+    va_list ap;
+    int sz;
+
+    va_start(ap, fmt);
+    vsnprintf(pcm->error, PCM_ERROR_MAX, fmt, ap);
+    va_end(ap);
+    sz = strlen(pcm->error);
+
+    if (e)
+        snprintf(pcm->error + sz, PCM_ERROR_MAX - sz,
+                 ": %s", strerror(e));
+    return -1;
+}
+
+/** Gets the buffer size of the PCM.
+ * @param pcm A PCM handle.
+ * @return The buffer size of the PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_get_buffer_size(const struct pcm *pcm)
+{
+    return pcm->buffer_size;
+}
+
+/** Gets the channel count of the PCM.
+ * @param pcm A PCM handle.
+ * @return The channel count of the PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_get_channels(const struct pcm *pcm)
+{
+    return pcm->config.channels;
+}
+
+/** Gets the PCM configuration.
+ * @param pcm A PCM handle.
+ * @return The PCM configuration.
+ *  This function only returns NULL if
+ *  @p pcm is NULL.
+ * @ingroup libtinyalsa-pcm
+ * */
+const struct pcm_config * pcm_get_config(const struct pcm *pcm)
+{
+    if (pcm == NULL)
+        return NULL;
+    return &pcm->config;
+}
+
+/** Gets the rate of the PCM.
+ * The rate is given in frames per second.
+ * @param pcm A PCM handle.
+ * @return The rate of the PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_get_rate(const struct pcm *pcm)
+{
+    return pcm->config.rate;
+}
+
+/** Gets the format of the PCM.
+ * @param pcm A PCM handle.
+ * @return The format of the PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+enum pcm_format pcm_get_format(const struct pcm *pcm)
+{
+    return pcm->config.format;
+}
+
+/** Gets the file descriptor of the PCM.
+ * Useful for extending functionality of the PCM when needed.
+ * @param pcm A PCM handle.
+ * @return The file descriptor of the PCM.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_get_file_descriptor(const struct pcm *pcm)
+{
+    return pcm->fd;
+}
+
+/** Gets the error message for the last error that occured.
+ * If no error occured and this function is called, the results are undefined.
+ * @param pcm A PCM handle.
+ * @return The error message of the last error that occured.
+ * @ingroup libtinyalsa-pcm
+ */
+const char* pcm_get_error(const struct pcm *pcm)
+{
+    return pcm->error;
+}
+
+/** Sets the PCM configuration.
+ * @param pcm A PCM handle.
+ * @param config The configuration to use for the
+ *  PCM. This parameter may be NULL, in which case
+ *  the default configuration is used.
+ * @returns Zero on success, a negative errno value
+ *  on failure.
+ * @ingroup libtinyalsa-pcm
+ * */
+int pcm_set_config(struct pcm *pcm, const struct pcm_config *config)
+{
+    if (pcm == NULL)
+        return -EFAULT;
+    else if (config == NULL) {
+        config = &pcm->config;
+        pcm->config.channels = 2;
+        pcm->config.rate = 48000;
+        pcm->config.period_size = 1024;
+        pcm->config.period_count = 4;
+        pcm->config.format = PCM_FORMAT_S16_LE;
+        pcm->config.start_threshold = config->period_count * config->period_size;
+        pcm->config.stop_threshold = config->period_count * config->period_size;
+        pcm->config.silence_threshold = 0;
+        pcm->config.silence_size = 0;
+    } else
+        pcm->config = *config;
+
+    struct snd_pcm_hw_params params;
+    param_init(&params);
+    param_set_mask(&params, SNDRV_PCM_HW_PARAM_FORMAT,
+                   pcm_format_to_alsa(config->format));
+    param_set_min(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
+    param_set_int(&params, SNDRV_PCM_HW_PARAM_CHANNELS,
+                  config->channels);
+    param_set_int(&params, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
+    param_set_int(&params, SNDRV_PCM_HW_PARAM_RATE, config->rate);
+
+    if (pcm->flags & PCM_NOIRQ) {
+
+        if (!(pcm->flags & PCM_MMAP)) {
+            oops(pcm, EINVAL, "noirq only currently supported with mmap().");
+            return -EINVAL;
+        }
+
+        params.flags |= SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP;
+        pcm->noirq_frames_per_msec = config->rate / 1000;
+    }
+
+    if (pcm->flags & PCM_MMAP)
+        param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,
+                   SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
+    else
+        param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,
+                   SNDRV_PCM_ACCESS_RW_INTERLEAVED);
+
+    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) {
+        int errno_copy = errno;
+        oops(pcm, errno, "cannot set hw params");
+        return -errno_copy;
+    }
+
+    /* get our refined hw_params */
+    pcm->config.period_size = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
+    pcm->config.period_count = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIODS);
+    pcm->buffer_size = config->period_count * config->period_size;
+
+    if (pcm->flags & PCM_MMAP) {
+        pcm->mmap_buffer = pcm->ops->mmap(pcm->data, NULL, pcm_frames_to_bytes(pcm, pcm->buffer_size),
+                                PROT_READ | PROT_WRITE, MAP_SHARED, 0);
+        if (pcm->mmap_buffer == MAP_FAILED) {
+            int errno_copy = errno;
+            oops(pcm, errno, "failed to mmap buffer %d bytes\n",
+                 pcm_frames_to_bytes(pcm, pcm->buffer_size));
+            return -errno_copy;
+        }
+    }
+
+    struct snd_pcm_sw_params sparams;
+    memset(&sparams, 0, sizeof(sparams));
+    sparams.tstamp_mode = SNDRV_PCM_TSTAMP_ENABLE;
+    sparams.period_step = 1;
+    sparams.avail_min = config->period_size;
+
+    if (!config->start_threshold) {
+        if (pcm->flags & PCM_IN)
+            pcm->config.start_threshold = sparams.start_threshold = 1;
+        else
+            pcm->config.start_threshold = sparams.start_threshold =
+                config->period_count * config->period_size / 2;
+    } else
+        sparams.start_threshold = config->start_threshold;
+
+    /* pick a high stop threshold - todo: does this need further tuning */
+    if (!config->stop_threshold) {
+        if (pcm->flags & PCM_IN)
+            pcm->config.stop_threshold = sparams.stop_threshold =
+                config->period_count * config->period_size * 10;
+        else
+            pcm->config.stop_threshold = sparams.stop_threshold =
+                config->period_count * config->period_size;
+    }
+    else
+        sparams.stop_threshold = config->stop_threshold;
+
+    sparams.xfer_align = config->period_size / 2; /* needed for old kernels */
+    sparams.silence_size = config->silence_size;
+    sparams.silence_threshold = config->silence_threshold;
+    pcm->boundary = sparams.boundary = pcm->buffer_size;
+
+    while (pcm->boundary * 2 <= INT_MAX - pcm->buffer_size)
+        pcm->boundary *= 2;
+
+    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)) {
+        int errno_copy = errno;
+        oops(pcm, errno, "cannot set sw params");
+        return -errno_copy;
+    }
+
+    return 0;
+}
+
+/** Gets the subdevice on which the pcm has been opened.
+ * @param pcm A PCM handle.
+ * @return The subdevice on which the pcm has been opened */
+unsigned int pcm_get_subdevice(const struct pcm *pcm)
+{
+    return pcm->subdevice;
+}
+
+/** Determines the number of bits occupied by a @ref pcm_format.
+ * @param format A PCM format.
+ * @return The number of bits associated with @p format
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_format_to_bits(enum pcm_format format)
+{
+    switch (format) {
+    case PCM_FORMAT_S32_LE:
+    case PCM_FORMAT_S32_BE:
+    case PCM_FORMAT_S24_LE:
+    case PCM_FORMAT_S24_BE:
+        return 32;
+    case PCM_FORMAT_S24_3LE:
+    case PCM_FORMAT_S24_3BE:
+        return 24;
+    default:
+    case PCM_FORMAT_S16_LE:
+    case PCM_FORMAT_S16_BE:
+        return 16;
+    case PCM_FORMAT_S8:
+        return 8;
+    };
+}
+
+/** Determines how many frames of a PCM can fit into a number of bytes.
+ * @param pcm A PCM handle.
+ * @param bytes The number of bytes.
+ * @return The number of frames that may fit into @p bytes
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_bytes_to_frames(const struct pcm *pcm, unsigned int bytes)
+{
+    return bytes / (pcm->config.channels *
+        (pcm_format_to_bits(pcm->config.format) >> 3));
+}
+
+/** Determines how many bytes are occupied by a number of frames of a PCM.
+ * @param pcm A PCM handle.
+ * @param frames The number of frames of a PCM.
+ * @return The bytes occupied by @p frames.
+ * @ingroup libtinyalsa-pcm
+ */
+unsigned int pcm_frames_to_bytes(const struct pcm *pcm, unsigned int frames)
+{
+    return frames * pcm->config.channels *
+        (pcm_format_to_bits(pcm->config.format) >> 3);
+}
+
+static int pcm_sync_ptr(struct pcm *pcm, int flags)
+{
+    if (pcm->sync_ptr == NULL) {
+        /* status and control are mmaped */
+
+        if (flags & SNDRV_PCM_SYNC_PTR_HWSYNC) {
+            if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_HWSYNC) == -1) {
+                oops(pcm, errno, "failed to sync hardware pointer");
+                return -1;
+            }
+        }
+    } else {
+        pcm->sync_ptr->flags = flags;
+        if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_SYNC_PTR,
+                            pcm->sync_ptr) < 0) {
+            oops(pcm, errno, "failed to sync mmap ptr");
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+static int pcm_hw_mmap_status(struct pcm *pcm)
+{
+    if (pcm->sync_ptr)
+        return 0;
+
+    int page_size = sysconf(_SC_PAGE_SIZE);
+    pcm->mmap_status = pcm->ops->mmap(pcm->data, NULL, page_size, PROT_READ, MAP_SHARED,
+                            SNDRV_PCM_MMAP_OFFSET_STATUS);
+    if (pcm->mmap_status == MAP_FAILED)
+        pcm->mmap_status = NULL;
+    if (!pcm->mmap_status)
+        goto mmap_error;
+
+    pcm->mmap_control = pcm->ops->mmap(pcm->data, NULL, page_size, PROT_READ | PROT_WRITE,
+                             MAP_SHARED, SNDRV_PCM_MMAP_OFFSET_CONTROL);
+    if (pcm->mmap_control == MAP_FAILED)
+        pcm->mmap_control = NULL;
+    if (!pcm->mmap_control) {
+        pcm->ops->munmap(pcm->data, pcm->mmap_status, page_size);
+        pcm->mmap_status = NULL;
+        goto mmap_error;
+    }
+
+    return 0;
+
+mmap_error:
+
+    pcm->sync_ptr = calloc(1, sizeof(*pcm->sync_ptr));
+    if (!pcm->sync_ptr)
+        return -ENOMEM;
+    pcm->mmap_status = &pcm->sync_ptr->s.status;
+    pcm->mmap_control = &pcm->sync_ptr->c.control;
+
+    return 0;
+}
+
+static void pcm_hw_munmap_status(struct pcm *pcm) {
+    if (pcm->sync_ptr) {
+        free(pcm->sync_ptr);
+        pcm->sync_ptr = NULL;
+    } else {
+        int page_size = sysconf(_SC_PAGE_SIZE);
+        if (pcm->mmap_status)
+            pcm->ops->munmap(pcm->data, pcm->mmap_status, page_size);
+        if (pcm->mmap_control)
+            pcm->ops->munmap(pcm->data, pcm->mmap_control, page_size);
+    }
+    pcm->mmap_status = NULL;
+    pcm->mmap_control = NULL;
+}
+
+static struct pcm bad_pcm = {
+    .fd = -1,
+};
+
+/** Gets the hardware parameters of a PCM, without created a PCM handle.
+ * @param card The card of the PCM.
+ *  The default card is zero.
+ * @param device The device of the PCM.
+ *  The default device is zero.
+ * @param flags Specifies whether the PCM is an input or output.
+ *  May be one of the following:
+ *   - @ref PCM_IN
+ *   - @ref PCM_OUT
+ * @return On success, the hardware parameters of the PCM; on failure, NULL.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm_params *pcm_params_get(unsigned int card, unsigned int device,
+                                  unsigned int flags)
+{
+    struct snd_pcm_hw_params *params;
+    void *snd_node = NULL, *data;
+    const struct pcm_ops *ops;
+    int fd;
+
+    ops = &hw_ops;
+    fd = ops->open(card, device, flags, &data, snd_node);
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (fd < 0) {
+        int pcm_type;
+        snd_node = snd_utils_open_pcm(card, device);
+        pcm_type = snd_utils_get_node_type(snd_node);
+        if (!snd_node || pcm_type != SND_NODE_TYPE_PLUGIN) {
+            fprintf(stderr, "no device (hw/plugin) for card(%u), device(%u)",
+                 card, device);
+            goto err_open;
+        }
+        ops = &plug_ops;
+        fd = ops->open(card, device, flags, &data, snd_node);
+    }
+#endif
+    if (fd < 0) {
+        fprintf(stderr, "cannot open card(%d) device (%d): %s\n",
+                card, device, strerror(errno));
+        goto err_open;
+    }
+
+    params = calloc(1, sizeof(struct snd_pcm_hw_params));
+    if (!params)
+        goto err_calloc;
+
+    param_init(params);
+    if (ops->ioctl(data, SNDRV_PCM_IOCTL_HW_REFINE, params)) {
+        fprintf(stderr, "SNDRV_PCM_IOCTL_HW_REFINE error (%d)\n", errno);
+        goto err_hw_refine;
+    }
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (snd_node)
+        snd_utils_close_dev_node(snd_node);
+#endif
+    ops->close(data);
+
+    return (struct pcm_params *)params;
+
+err_hw_refine:
+    free(params);
+err_calloc:
+#ifdef TINYALSA_USES_PLUGINS
+    if (snd_node)
+        snd_utils_close_dev_node(snd_node);
+#endif
+    ops->close(data);
+err_open:
+    return NULL;
+}
+
+/** Frees the hardware parameters returned by @ref pcm_params_get.
+ * @param pcm_params Hardware parameters of a PCM.
+ *  May be NULL.
+ * @ingroup libtinyalsa-pcm
+ */
+void pcm_params_free(struct pcm_params *pcm_params)
+{
+    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;
+
+    if (params)
+        free(params);
+}
+
+static int pcm_param_to_alsa(enum pcm_param param)
+{
+    switch (param) {
+    case PCM_PARAM_ACCESS:
+        return SNDRV_PCM_HW_PARAM_ACCESS;
+    case PCM_PARAM_FORMAT:
+        return SNDRV_PCM_HW_PARAM_FORMAT;
+    case PCM_PARAM_SUBFORMAT:
+        return SNDRV_PCM_HW_PARAM_SUBFORMAT;
+    case PCM_PARAM_SAMPLE_BITS:
+        return SNDRV_PCM_HW_PARAM_SAMPLE_BITS;
+        break;
+    case PCM_PARAM_FRAME_BITS:
+        return SNDRV_PCM_HW_PARAM_FRAME_BITS;
+        break;
+    case PCM_PARAM_CHANNELS:
+        return SNDRV_PCM_HW_PARAM_CHANNELS;
+        break;
+    case PCM_PARAM_RATE:
+        return SNDRV_PCM_HW_PARAM_RATE;
+        break;
+    case PCM_PARAM_PERIOD_TIME:
+        return SNDRV_PCM_HW_PARAM_PERIOD_TIME;
+        break;
+    case PCM_PARAM_PERIOD_SIZE:
+        return SNDRV_PCM_HW_PARAM_PERIOD_SIZE;
+        break;
+    case PCM_PARAM_PERIOD_BYTES:
+        return SNDRV_PCM_HW_PARAM_PERIOD_BYTES;
+        break;
+    case PCM_PARAM_PERIODS:
+        return SNDRV_PCM_HW_PARAM_PERIODS;
+        break;
+    case PCM_PARAM_BUFFER_TIME:
+        return SNDRV_PCM_HW_PARAM_BUFFER_TIME;
+        break;
+    case PCM_PARAM_BUFFER_SIZE:
+        return SNDRV_PCM_HW_PARAM_BUFFER_SIZE;
+        break;
+    case PCM_PARAM_BUFFER_BYTES:
+        return SNDRV_PCM_HW_PARAM_BUFFER_BYTES;
+        break;
+    case PCM_PARAM_TICK_TIME:
+        return SNDRV_PCM_HW_PARAM_TICK_TIME;
+        break;
+
+    default:
+        return -1;
+    }
+}
+
+/** Gets a mask from a PCM's hardware parameters.
+ * @param pcm_params A PCM's hardware parameters.
+ * @param param The parameter to get.
+ * @return If @p pcm_params is NULL or @p param is not a mask, NULL is returned.
+ *  Otherwise, the mask associated with @p param is returned.
+ * @ingroup libtinyalsa-pcm
+ */
+const struct pcm_mask *pcm_params_get_mask(const struct pcm_params *pcm_params,
+                                     enum pcm_param param)
+{
+    int p;
+    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;
+    if (params == NULL) {
+        return NULL;
+    }
+
+    p = pcm_param_to_alsa(param);
+    if (p < 0 || !param_is_mask(p)) {
+        return NULL;
+    }
+
+    return (const struct pcm_mask *)param_to_mask(params, p);
+}
+
+/** Get the minimum of a specified PCM parameter.
+ * @param pcm_params A PCM parameters structure.
+ * @param param The specified parameter to get the minimum of.
+ * @returns On success, the parameter minimum.
+ *  On failure, zero.
+ */
+unsigned int pcm_params_get_min(const struct pcm_params *pcm_params,
+                                enum pcm_param param)
+{
+    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;
+    int p;
+
+    if (!params)
+        return 0;
+
+    p = pcm_param_to_alsa(param);
+    if (p < 0)
+        return 0;
+
+    return param_get_min(params, p);
+}
+
+/** Get the maximum of a specified PCM parameter.
+ * @param pcm_params A PCM parameters structure.
+ * @param param The specified parameter to get the maximum of.
+ * @returns On success, the parameter maximum.
+ *  On failure, zero.
+ */
+unsigned int pcm_params_get_max(const struct pcm_params *pcm_params,
+                                enum pcm_param param)
+{
+    const struct snd_pcm_hw_params *params = (const struct snd_pcm_hw_params *)pcm_params;
+    int p;
+
+    if (!params)
+        return 0;
+
+    p = pcm_param_to_alsa(param);
+    if (p < 0)
+        return 0;
+
+    return param_get_max(params, p);
+}
+
+static int pcm_mask_test(const struct pcm_mask *m, unsigned int index)
+{
+    const unsigned int bitshift = 5; /* for 32 bit integer */
+    const unsigned int bitmask = (1 << bitshift) - 1;
+    unsigned int element;
+
+    element = index >> bitshift;
+    if (element >= ARRAY_SIZE(m->bits))
+        return 0; /* for safety, but should never occur */
+    return (m->bits[element] >> (index & bitmask)) & 1;
+}
+
+static int pcm_mask_to_string(const struct pcm_mask *m, char *string, unsigned int size,
+                              char *mask_name,
+                              const char * const *bit_array_name, size_t bit_array_size)
+{
+    unsigned int i;
+    unsigned int offset = 0;
+
+    if (m == NULL)
+        return 0;
+    if (bit_array_size < 32) {
+        STRLOG(string, offset, size, "%12s:\t%#08x\n", mask_name, m->bits[0]);
+    } else { /* spans two or more bitfields, print with an array index */
+        for (i = 0; i < (bit_array_size + 31) >> 5; ++i) {
+            STRLOG(string, offset, size, "%9s[%d]:\t%#08x\n",
+                   mask_name, i, m->bits[i]);
+        }
+    }
+    for (i = 0; i < bit_array_size; ++i) {
+        if (pcm_mask_test(m, i)) {
+            STRLOG(string, offset, size, "%12s \t%s\n", "", bit_array_name[i]);
+        }
+    }
+    return offset;
+}
+
+int pcm_params_to_string(struct pcm_params *params, char *string, unsigned int size)
+{
+    const struct pcm_mask *m;
+    unsigned int min, max;
+    unsigned int clipoffset, offset;
+
+    m = pcm_params_get_mask(params, PCM_PARAM_ACCESS);
+    offset = pcm_mask_to_string(m, string, size,
+                                 "Access", access_lookup, ARRAY_SIZE(access_lookup));
+    m = pcm_params_get_mask(params, PCM_PARAM_FORMAT);
+    clipoffset = offset > size ? size : offset;
+    offset += pcm_mask_to_string(m, string + clipoffset, size - clipoffset,
+                                 "Format", format_lookup, ARRAY_SIZE(format_lookup));
+    m = pcm_params_get_mask(params, PCM_PARAM_SUBFORMAT);
+    clipoffset = offset > size ? size : offset;
+    offset += pcm_mask_to_string(m, string + clipoffset, size - clipoffset,
+                                 "Subformat", subformat_lookup, ARRAY_SIZE(subformat_lookup));
+    min = pcm_params_get_min(params, PCM_PARAM_RATE);
+    max = pcm_params_get_max(params, PCM_PARAM_RATE);
+    STRLOG(string, offset, size, "        Rate:\tmin=%uHz\tmax=%uHz\n", min, max);
+    min = pcm_params_get_min(params, PCM_PARAM_CHANNELS);
+    max = pcm_params_get_max(params, PCM_PARAM_CHANNELS);
+    STRLOG(string, offset, size, "    Channels:\tmin=%u\t\tmax=%u\n", min, max);
+    min = pcm_params_get_min(params, PCM_PARAM_SAMPLE_BITS);
+    max = pcm_params_get_max(params, PCM_PARAM_SAMPLE_BITS);
+    STRLOG(string, offset, size, " Sample bits:\tmin=%u\t\tmax=%u\n", min, max);
+    min = pcm_params_get_min(params, PCM_PARAM_PERIOD_SIZE);
+    max = pcm_params_get_max(params, PCM_PARAM_PERIOD_SIZE);
+    STRLOG(string, offset, size, " Period size:\tmin=%u\t\tmax=%u\n", min, max);
+    min = pcm_params_get_min(params, PCM_PARAM_PERIODS);
+    max = pcm_params_get_max(params, PCM_PARAM_PERIODS);
+    STRLOG(string, offset, size, "Period count:\tmin=%u\t\tmax=%u\n", min, max);
+    return offset;
+}
+
+int pcm_params_format_test(struct pcm_params *params, enum pcm_format format)
+{
+    unsigned int alsa_format = pcm_format_to_alsa(format);
+
+    if (alsa_format == SNDRV_PCM_FORMAT_S16_LE && format != PCM_FORMAT_S16_LE)
+        return 0; /* caution: format not recognized is equivalent to S16_LE */
+    return pcm_mask_test(pcm_params_get_mask(params, PCM_PARAM_FORMAT), alsa_format);
+}
+
+/** Closes a PCM returned by @ref pcm_open.
+ * @param pcm A PCM returned by @ref pcm_open.
+ *  May not be NULL.
+ * @return Always returns zero.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_close(struct pcm *pcm)
+{
+    if (pcm == &bad_pcm)
+        return 0;
+
+    pcm_hw_munmap_status(pcm);
+
+    if (pcm->flags & PCM_MMAP) {
+        pcm_stop(pcm);
+        pcm->ops->munmap(pcm->data, pcm->mmap_buffer, pcm_frames_to_bytes(pcm, pcm->buffer_size));
+    }
+
+    snd_utils_close_dev_node(pcm->snd_node);
+    pcm->ops->close(pcm->data);
+    pcm->buffer_size = 0;
+    pcm->fd = -1;
+    free(pcm);
+    return 0;
+}
+
+/** Opens a PCM by it's name.
+ * @param name The name of the PCM.
+ *  The name is given in the format: <i>hw</i>:<b>card</b>,<b>device</b>
+ * @param flags Specify characteristics and functionality about the pcm.
+ *  May be a bitwise AND of the following:
+ *   - @ref PCM_IN
+ *   - @ref PCM_OUT
+ *   - @ref PCM_MMAP
+ *   - @ref PCM_NOIRQ
+ *   - @ref PCM_MONOTONIC
+ * @param config The hardware and software parameters to open the PCM with.
+ * @returns A PCM structure.
+ *  If an error occurs allocating memory for the PCM, NULL is returned.
+ *  Otherwise, client code should check that the PCM opened properly by calling @ref pcm_is_ready.
+ *  If @ref pcm_is_ready, check @ref pcm_get_error for more information.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm *pcm_open_by_name(const char *name,
+                             unsigned int flags,
+                             const struct pcm_config *config)
+{
+  unsigned int card, device;
+  if ((name[0] != 'h')
+   || (name[1] != 'w')
+   || (name[2] != ':')) {
+    return NULL;
+  } else if (sscanf(&name[3], "%u,%u", &card, &device) != 2) {
+    return NULL;
+  }
+  return pcm_open(card, device, flags, config);
+}
+
+/** Opens a PCM.
+ * @param card The card that the pcm belongs to.
+ *  The default card is zero.
+ * @param device The device that the pcm belongs to.
+ *  The default device is zero.
+ * @param flags Specify characteristics and functionality about the pcm.
+ *  May be a bitwise AND of the following:
+ *   - @ref PCM_IN
+ *   - @ref PCM_OUT
+ *   - @ref PCM_MMAP
+ *   - @ref PCM_NOIRQ
+ *   - @ref PCM_MONOTONIC
+ * @param config The hardware and software parameters to open the PCM with.
+ * @returns A PCM structure.
+ *  If an error occurs allocating memory for the PCM, NULL is returned.
+ *  Otherwise, client code should check that the PCM opened properly by calling @ref pcm_is_ready.
+ *  If @ref pcm_is_ready, check @ref pcm_get_error for more information.
+ * @ingroup libtinyalsa-pcm
+ */
+struct pcm *pcm_open(unsigned int card, unsigned int device,
+                     unsigned int flags, const struct pcm_config *config)
+{
+    struct pcm *pcm;
+    struct snd_pcm_info info;
+    int rc;
+
+    pcm = calloc(1, sizeof(struct pcm));
+    if (!pcm)
+        return &bad_pcm;
+
+    /* Default to hw_ops, attemp plugin open only if hw (/dev/snd/pcm*) open fails */
+    pcm->ops = &hw_ops;
+    pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, NULL);
+
+#ifdef TINYALSA_USES_PLUGINS
+    if (pcm->fd < 0) {
+        int pcm_type;
+        pcm->snd_node = snd_utils_open_pcm(card, device);
+        pcm_type = snd_utils_get_node_type(pcm->snd_node);
+        if (!pcm->snd_node || pcm_type != SND_NODE_TYPE_PLUGIN) {
+            oops(pcm, -ENODEV, "no device (hw/plugin) for card(%u), device(%u)",
+                 card, device);
+            goto fail_close_dev_node;
+        }
+        pcm->ops = &plug_ops;
+        pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, pcm->snd_node);
+    }
+#endif
+    if (pcm->fd < 0) {
+        oops(pcm, errno, "cannot open device (%u) for card (%u)",
+             device, card);
+        goto fail_close_dev_node;
+    }
+
+    pcm->flags = flags;
+
+    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_INFO, &info)) {
+        oops(pcm, errno, "cannot get info");
+        goto fail_close;
+    }
+    pcm->subdevice = info.subdevice;
+
+    if (pcm_set_config(pcm, config) != 0)
+        goto fail_close;
+
+    rc = pcm_hw_mmap_status(pcm);
+    if (rc < 0) {
+        oops(pcm, errno, "mmap status failed");
+        goto fail;
+    }
+
+#ifdef SNDRV_PCM_IOCTL_TTSTAMP
+    if (pcm->flags & PCM_MONOTONIC) {
+        int arg = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC;
+        rc = pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_TTSTAMP, &arg);
+        if (rc < 0) {
+            oops(pcm, errno, "cannot set timestamp type");
+            goto fail;
+        }
+    }
+#endif
+
+    /* prepare here so the user does not need to do this later */
+    if (pcm_prepare(pcm))
+        goto fail;
+
+    pcm->xruns = 0;
+    return pcm;
+
+fail:
+    pcm_hw_munmap_status(pcm);
+    if (flags & PCM_MMAP)
+        pcm->ops->munmap(pcm->data, pcm->mmap_buffer, pcm_frames_to_bytes(pcm, pcm->buffer_size));
+fail_close:
+    pcm->ops->close(pcm->data);
+fail_close_dev_node:
+#ifdef TINYALSA_USES_PLUGINS
+    if (pcm->snd_node)
+        snd_utils_close_dev_node(pcm->snd_node);
+#endif
+    free(pcm);
+    return &bad_pcm;
+}
+
+/** Checks if a PCM file has been opened without error.
+ * @param pcm A PCM handle.
+ *  May be NULL.
+ * @return If a PCM's file descriptor is not valid or the pointer is NULL, it returns zero.
+ *  Otherwise, the function returns one.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_is_ready(const struct pcm *pcm)
+{
+    if (pcm != NULL) {
+        return pcm->fd >= 0;
+    }
+    return 0;
+}
+
+/** Links two PCMs.
+ * After this function is called, the two PCMs will prepare, start and stop in sync (at the same time).
+ * If an error occurs, the error message will be written to @p pcm1.
+ * @param pcm1 A PCM handle.
+ * @param pcm2 Another PCM handle.
+ * @return On success, zero; on failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_link(struct pcm *pcm1, struct pcm *pcm2)
+{
+    int err = ioctl(pcm1->fd, SNDRV_PCM_IOCTL_LINK, pcm2->fd);
+    if (err == -1) {
+        return oops(pcm1, errno, "cannot link PCM");
+    }
+    return 0;
+}
+
+/** Unlinks a PCM.
+ * @see @ref pcm_link
+ * @param pcm A PCM handle.
+ * @return On success, zero; on failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_unlink(struct pcm *pcm)
+{
+    int err = ioctl(pcm->fd, SNDRV_PCM_IOCTL_UNLINK);
+    if (err == -1) {
+        return oops(pcm, errno, "cannot unlink PCM");
+    }
+    return 0;
+}
+
+/** Prepares a PCM, if it has not been prepared already.
+ * @param pcm A PCM handle.
+ * @return On success, zero; on failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_prepare(struct pcm *pcm)
+{
+    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_PREPARE) < 0)
+        return oops(pcm, errno, "cannot prepare channel");
+
+    /* get appl_ptr and avail_min from kernel */
+    pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_APPL|SNDRV_PCM_SYNC_PTR_AVAIL_MIN);
+
+    return 0;
+}
+
+/** Starts a PCM.
+ * @param pcm A PCM handle.
+ * @return On success, zero; on failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_start(struct pcm *pcm)
+{
+    /* set appl_ptr and avail_min in kernel */
+    if (pcm_sync_ptr(pcm, 0) < 0)
+        return -1;
+
+    if (pcm->mmap_status->state != PCM_STATE_RUNNING) {
+        if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_START) < 0)
+            return oops(pcm, errno, "cannot start channel");
+    }
+
+    return 0;
+}
+
+/** Stops a PCM.
+ * @param pcm A PCM handle.
+ * @return On success, zero; on failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_stop(struct pcm *pcm)
+{
+    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_DROP) < 0)
+        return oops(pcm, errno, "cannot stop channel");
+
+    return 0;
+}
+
+static inline int pcm_mmap_playback_avail(struct pcm *pcm)
+{
+    int avail;
+
+    avail = pcm->mmap_status->hw_ptr + pcm->buffer_size - pcm->mmap_control->appl_ptr;
+
+    if (avail < 0)
+        avail += pcm->boundary;
+    else if (avail >= (int)pcm->boundary)
+        avail -= pcm->boundary;
+
+    return avail;
+}
+
+static inline int pcm_mmap_capture_avail(struct pcm *pcm)
+{
+    int avail = pcm->mmap_status->hw_ptr - pcm->mmap_control->appl_ptr;
+    if (avail < 0)
+        avail += pcm->boundary;
+    return avail;
+}
+
+int pcm_mmap_avail(struct pcm *pcm)
+{
+    if (pcm->flags & PCM_IN)
+        return pcm_mmap_capture_avail(pcm);
+    else
+        return pcm_mmap_playback_avail(pcm);
+}
+
+static void pcm_mmap_appl_forward(struct pcm *pcm, int frames)
+{
+    unsigned int appl_ptr = pcm->mmap_control->appl_ptr;
+    appl_ptr += frames;
+
+    /* check for boundary wrap */
+    if (appl_ptr > pcm->boundary)
+         appl_ptr -= pcm->boundary;
+    pcm->mmap_control->appl_ptr = appl_ptr;
+}
+
+int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset,
+                   unsigned int *frames)
+{
+    unsigned int continuous, copy_frames, avail;
+
+    /* return the mmap buffer */
+    *areas = pcm->mmap_buffer;
+
+    /* and the application offset in frames */
+    *offset = pcm->mmap_control->appl_ptr % pcm->buffer_size;
+
+    avail = pcm_mmap_avail(pcm);
+    if (avail > pcm->buffer_size)
+        avail = pcm->buffer_size;
+    continuous = pcm->buffer_size - *offset;
+
+    /* we can only copy frames if the are availabale and continuos */
+    copy_frames = *frames;
+    if (copy_frames > avail)
+        copy_frames = avail;
+    if (copy_frames > continuous)
+        copy_frames = continuous;
+    *frames = copy_frames;
+
+    return 0;
+}
+
+static int pcm_areas_copy(struct pcm *pcm, unsigned int pcm_offset,
+                          char *buf, unsigned int src_offset,
+                          unsigned int frames)
+{
+    int size_bytes = pcm_frames_to_bytes(pcm, frames);
+    int pcm_offset_bytes = pcm_frames_to_bytes(pcm, pcm_offset);
+    int src_offset_bytes = pcm_frames_to_bytes(pcm, src_offset);
+
+    /* interleaved only atm */
+    if (pcm->flags & PCM_IN)
+        memcpy(buf + src_offset_bytes,
+               (char*)pcm->mmap_buffer + pcm_offset_bytes,
+               size_bytes);
+    else
+        memcpy((char*)pcm->mmap_buffer + pcm_offset_bytes,
+               buf + src_offset_bytes,
+               size_bytes);
+    return 0;
+}
+
+int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames)
+{
+    int ret;
+
+    /* not used */
+    (void) offset;
+
+    /* update the application pointer in userspace and kernel */
+    pcm_mmap_appl_forward(pcm, frames);
+    ret = pcm_sync_ptr(pcm, 0);
+    if (ret != 0){
+        printf("%d\n", ret);
+        return ret;
+    }
+
+    return frames;
+}
+
+static int pcm_mmap_transfer_areas(struct pcm *pcm, char *buf,
+                                unsigned int offset, unsigned int size)
+{
+    void *pcm_areas;
+    int commit;
+    unsigned int pcm_offset, frames, count = 0;
+
+    while (pcm_mmap_avail(pcm) && size) {
+        frames = size;
+        pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
+        pcm_areas_copy(pcm, pcm_offset, buf, offset, frames);
+        commit = pcm_mmap_commit(pcm, pcm_offset, frames);
+        if (commit < 0) {
+            oops(pcm, commit, "failed to commit %d frames\n", frames);
+            return commit;
+        }
+
+        offset += commit;
+        count += commit;
+        size -= commit;
+    }
+    return count;
+}
+
+int pcm_get_poll_fd(struct pcm *pcm)
+{
+    return pcm->fd;
+}
+
+int pcm_avail_update(struct pcm *pcm)
+{
+    pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_APPL|SNDRV_PCM_SYNC_PTR_AVAIL_MIN);
+    return pcm_mmap_avail(pcm);
+}
+
+/** Returns available frames in pcm buffer and corresponding time stamp.
+ * The clock is CLOCK_MONOTONIC if flag @ref PCM_MONOTONIC was specified in @ref pcm_open,
+ * otherwise the clock is CLOCK_REALTIME.
+ * For an input stream, frames available are frames ready for the application to read.
+ * For an output stream, frames available are the number of empty frames available for the application to write.
+ * @param pcm A PCM handle.
+ * @param avail The number of available frames
+ * @param tstamp The timestamp
+ * @return On success, zero is returned; on failure, negative one.
+ */
+int pcm_get_htimestamp(struct pcm *pcm, unsigned int *avail,
+                       struct timespec *tstamp)
+{
+    int checking;
+    int tmp;
+
+    if (!pcm_is_ready(pcm))
+        return -1;
+
+    checking = 0;
+
+again:
+
+    tmp = pcm_avail_update(pcm);
+    if (tmp < 0)
+        return tmp; /* error */
+
+    if (checking && (unsigned int) tmp == *avail)
+        return 0;
+
+    *avail = (unsigned int) tmp;
+    *tstamp = pcm->mmap_status->tstamp;
+
+    /*
+     * When status is mmaped, get avail again to ensure
+     * valid timestamp.
+     */
+    if (!pcm->sync_ptr) {
+        checking = 1;
+        goto again;
+    }
+
+    /* SYNC_PTR ioctl was used, no need to check avail */
+    return 0;
+}
+
+int pcm_state(struct pcm *pcm)
+{
+    int err = pcm_sync_ptr(pcm, 0);
+    if (err < 0)
+        return err;
+
+    return pcm->mmap_status->state;
+}
+
+/** Waits for frames to be available for read or write operations.
+ * @param pcm A PCM handle.
+ * @param timeout The maximum amount of time to wait for, in terms of milliseconds.
+ * @returns If frames became available, one is returned.
+ *  If a timeout occured, zero is returned.
+ *  If an error occured, a negative number is returned.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_wait(struct pcm *pcm, int timeout)
+{
+    struct pollfd pfd;
+    int err;
+
+    pfd.fd = pcm->fd;
+    pfd.events = POLLIN | POLLOUT | POLLERR | POLLNVAL;
+
+    do {
+        /* let's wait for avail or timeout */
+        err = pcm->ops->poll(pcm->data, &pfd, 1, timeout);
+        if (err < 0)
+            return -errno;
+
+        /* timeout ? */
+        if (err == 0)
+            return 0;
+
+        /* have we been interrupted ? */
+        if (errno == -EINTR)
+            continue;
+
+        /* check for any errors */
+        if (pfd.revents & (POLLERR | POLLNVAL)) {
+            switch (pcm_state(pcm)) {
+            case PCM_STATE_XRUN:
+                return -EPIPE;
+            case PCM_STATE_SUSPENDED:
+                return -ESTRPIPE;
+            case PCM_STATE_DISCONNECTED:
+                return -ENODEV;
+            default:
+                return -EIO;
+            }
+        }
+    /* poll again if fd not ready for IO */
+    } while (!(pfd.revents & (POLLIN | POLLOUT)));
+
+    return 1;
+}
+
+/*
+ * Transfer data to/from mmaped buffer. This imitates the
+ * behavior of read/write system calls.
+ *
+ * However, this doesn't seems to offer any advantage over
+ * the read/write syscalls. Should it be removed?
+ */
+int pcm_mmap_transfer(struct pcm *pcm, void *buffer, unsigned int frames)
+{
+    int is_playback;
+
+    int state;
+    unsigned int avail;
+    unsigned int user_offset;
+
+    int err;
+    int tmp;
+
+    is_playback = !(pcm->flags & PCM_IN);
+
+    if (frames == 0)
+        return 0;
+
+    /* update hardware pointer and get state */
+    err = pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_HWSYNC |
+                            SNDRV_PCM_SYNC_PTR_APPL |
+                            SNDRV_PCM_SYNC_PTR_AVAIL_MIN);
+    if (err == -1)
+        return -1;
+    state = pcm->mmap_status->state;
+
+    /*
+     * If frames < start_threshold, wait indefinitely.
+     * Another thread may start capture
+     */
+    if (!is_playback && state == PCM_STATE_PREPARED &&
+        frames >= pcm->config.start_threshold) {
+            err = pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_START);
+        if (err == -1)
+            return -1;
+        /* state = PCM_STATE_RUNNING */
+    }
+
+    avail = pcm_mmap_avail(pcm);
+    user_offset = 0;
+
+    while (frames) {
+        if (!avail) {
+            if (pcm->flags & PCM_NONBLOCK) {
+                errno = EAGAIN;
+                break;
+            }
+
+            /* wait for interrupt */
+            err = pcm_wait(pcm, -1);
+            if (err < 0) {
+                errno = -err;
+                break;
+            }
+
+            /* get hardware pointer */
+            avail = pcm_avail_update(pcm);
+        }
+
+        tmp = pcm_mmap_transfer_areas(pcm, buffer, user_offset, frames);
+        if (tmp < 0)
+            break;
+
+        user_offset += tmp;
+        frames -= tmp;
+        avail -= tmp;
+
+        /* start playback if written >= start_threshold */
+        if (is_playback && state == PCM_STATE_PREPARED &&
+            pcm->buffer_size - avail >= pcm->config.start_threshold) {
+            err = pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_START);
+            if (err == -1)
+                break;
+        }
+    }
+
+    return user_offset ? (int) user_offset : -1;
+}
+
+int pcm_mmap_write(struct pcm *pcm, const void *data, unsigned int count)
+{
+    if ((~pcm->flags) & (PCM_OUT | PCM_MMAP))
+        return -ENOSYS;
+
+    return pcm_mmap_transfer(pcm, (void *)data,
+                             pcm_bytes_to_frames(pcm, count));
+}
+
+int pcm_mmap_read(struct pcm *pcm, void *data, unsigned int count)
+{
+    if ((~pcm->flags) & (PCM_IN | PCM_MMAP))
+        return -ENOSYS;
+
+    return pcm_mmap_transfer(pcm, data, pcm_bytes_to_frames(pcm, count));
+}
+
+/* Returns current read/write position in the mmap buffer with associated time stamp. */
+int pcm_mmap_get_hw_ptr(struct pcm* pcm, unsigned int *hw_ptr, struct timespec *tstamp)
+{
+    int rc;
+
+    if (pcm == NULL || hw_ptr == NULL || tstamp == NULL)
+        return oops(pcm, EINVAL, "pcm %p, hw_ptr %p, tstamp %p", pcm, hw_ptr, tstamp);
+
+    if (!pcm_is_ready(pcm))
+        return oops(pcm, errno, "pcm_is_ready failed");
+
+    rc = pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_HWSYNC);
+    if (rc < 0)
+        return oops(pcm, errno, "pcm_sync_ptr failed");
+
+    if (pcm->mmap_status == NULL)
+        return oops(pcm, EINVAL, "pcm %p, mmap_status is NULL", pcm);
+
+    if ((pcm->mmap_status->state != PCM_STATE_RUNNING) &&
+            (pcm->mmap_status->state != PCM_STATE_DRAINING))
+        return oops(pcm, ENOSYS, "invalid stream state %d", pcm->mmap_status->state);
+
+    *tstamp = pcm->mmap_status->tstamp;
+    if (tstamp->tv_sec == 0 && tstamp->tv_nsec == 0)
+        return oops(pcm, errno, "invalid time stamp");
+
+    *hw_ptr = pcm->mmap_status->hw_ptr;
+
+    return 0;
+}
+
+static int pcm_rw_transfer(struct pcm *pcm, void *data, unsigned int frames)
+{
+    int is_playback;
+
+    struct snd_xferi transfer;
+    int res;
+
+    is_playback = !(pcm->flags & PCM_IN);
+
+    transfer.buf = data;
+    transfer.frames = frames;
+    transfer.result = 0;
+
+    res = pcm->ops->ioctl(pcm->data, is_playback
+                          ? SNDRV_PCM_IOCTL_WRITEI_FRAMES
+                          : SNDRV_PCM_IOCTL_READI_FRAMES, &transfer);
+
+    return res == 0 ? (int) transfer.result : -1;
+}
+
+static int pcm_generic_transfer(struct pcm *pcm, void *data,
+                                unsigned int frames)
+{
+    int res;
+
+#if UINT_MAX > TINYALSA_FRAMES_MAX
+    if (frames > TINYALSA_FRAMES_MAX)
+        return -EINVAL;
+#endif
+    if (frames > INT_MAX)
+        return -EINVAL;
+
+again:
+
+    if (pcm->flags & PCM_MMAP)
+        res = pcm_mmap_transfer(pcm, data, frames);
+    else
+        res = pcm_rw_transfer(pcm, data, frames);
+
+    if (res < 0) {
+        switch (errno) {
+        case EPIPE:
+            pcm->xruns++;
+            /* fallthrough */
+        case ESTRPIPE:
+            /*
+             * Try to restart if we are allowed to do so.
+             * Otherwise, return error.
+             */
+            if (pcm->flags & PCM_NORESTART || pcm_prepare(pcm))
+                return -1;
+            goto again;
+        case EAGAIN:
+            if (pcm->flags & PCM_NONBLOCK)
+                return -1;
+            /* fallthrough */
+        default:
+            return oops(pcm, errno, "cannot read/write stream data");
+        }
+    }
+
+    return res;
+}
+
+/** Writes audio samples to PCM.
+ * If the PCM has not been started, it is started in this function.
+ * This function is only valid for PCMs opened with the @ref PCM_OUT flag.
+ * @param pcm A PCM handle.
+ * @param data The audio sample array
+ * @param frame_count The number of frames occupied by the sample array.
+ *  This value should not be greater than @ref TINYALSA_FRAMES_MAX
+ *  or INT_MAX.
+ * @return On success, this function returns the number of frames written; otherwise, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_writei(struct pcm *pcm, const void *data, unsigned int frame_count)
+{
+    if (pcm->flags & PCM_IN)
+        return -EINVAL;
+
+    return pcm_generic_transfer(pcm, (void*) data, frame_count);
+}
+
+/** Reads audio samples from PCM.
+ * If the PCM has not been started, it is started in this function.
+ * This function is only valid for PCMs opened with the @ref PCM_IN flag.
+ * @param pcm A PCM handle.
+ * @param data The audio sample array
+ * @param frame_count The number of frames occupied by the sample array.
+ *  This value should not be greater than @ref TINYALSA_FRAMES_MAX
+ *  or INT_MAX.
+ * @return On success, this function returns the number of frames written; otherwise, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_readi(struct pcm *pcm, void *data, unsigned int frame_count)
+{
+    if (!(pcm->flags & PCM_IN))
+        return -EINVAL;
+
+    return pcm_generic_transfer(pcm, data, frame_count);
+}
+
+/** Writes audio samples to PCM.
+ * If the PCM has not been started, it is started in this function.
+ * This function is only valid for PCMs opened with the @ref PCM_OUT flag.
+ * This function is not valid for PCMs opened with the @ref PCM_MMAP flag.
+ * @param pcm A PCM handle.
+ * @param data The audio sample array
+ * @param count The number of bytes occupied by the sample array.
+ * @return On success, this function returns zero; otherwise, a negative number.
+ * @deprecated
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
+{
+    unsigned int requested_frames = pcm_bytes_to_frames(pcm, count);
+    int ret = pcm_writei(pcm, data, requested_frames);
+
+    if (ret < 0)
+        return ret;
+
+    return ((unsigned int )ret == requested_frames) ? 0 : -EIO;
+}
+
+/** Reads audio samples from PCM.
+ * If the PCM has not been started, it is started in this function.
+ * This function is only valid for PCMs opened with the @ref PCM_IN flag.
+ * This function is not valid for PCMs opened with the @ref PCM_MMAP flag.
+ * @param pcm A PCM handle.
+ * @param data The audio sample array
+ * @param count The number of bytes occupied by the sample array.
+ * @return On success, this function returns zero; otherwise, a negative number.
+ * @deprecated
+ * @ingroup libtinyalsa-pcm
+ */
+int pcm_read(struct pcm *pcm, void *data, unsigned int count)
+{
+    unsigned int requested_frames = pcm_bytes_to_frames(pcm, count);
+    int ret = pcm_readi(pcm, data, requested_frames);
+
+    if (ret < 0)
+        return ret;
+
+    return ((unsigned int )ret == requested_frames) ? 0 : -EIO;
+}
+
+/** Gets the delay of the PCM, in terms of frames.
+ * @param pcm A PCM handle.
+ * @returns On success, the delay of the PCM.
+ *  On failure, a negative number.
+ * @ingroup libtinyalsa-pcm
+ */
+long pcm_get_delay(struct pcm *pcm)
+{
+    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_DELAY, &pcm->pcm_delay) < 0)
+        return -1;
+
+    return pcm->pcm_delay;
+}
+
+// TODO: Currently in Android, there are some libraries using this function to control the driver.
+//   We should remove this function as soon as possible.
+int pcm_ioctl(struct pcm *pcm, int request, ...)
+{
+    va_list ap;
+    void * arg;
+
+    if (!pcm_is_ready(pcm))
+        return -1;
+
+    va_start(ap, request);
+    arg = va_arg(ap, void *);
+    va_end(ap);
+
+    // FIXME Does not handle plugins
+    return ioctl(pcm->fd, request, arg);
+}
diff --git a/src/pcm_hw.c b/src/pcm_hw.c
new file mode 100644
index 0000000..38b2e83
--- /dev/null
+++ b/src/pcm_hw.c
@@ -0,0 +1,142 @@
+/* pcm_hw.c
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <poll.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <linux/ioctl.h>
+#include <sound/asound.h>
+#include <tinyalsa/asoundlib.h>
+
+#include "pcm_io.h"
+
+struct pcm_hw_data {
+    /** Card number of the pcm device */
+    unsigned int card;
+    /** Device number for the pcm device */
+    unsigned int device;
+    /** File descriptor to the pcm device file node */
+    unsigned int fd;
+    /** Pointer to the pcm node from snd card definiton */
+    struct snd_node *node;
+};
+
+static void pcm_hw_close(void *data)
+{
+    struct pcm_hw_data *hw_data = data;
+
+    if (hw_data->fd > 0)
+        close(hw_data->fd);
+
+    free(hw_data);
+}
+
+static int pcm_hw_ioctl(void *data, unsigned int cmd, ...)
+{
+    struct pcm_hw_data *hw_data = data;
+    va_list ap;
+    void *arg;
+
+    va_start(ap, cmd);
+    arg = va_arg(ap, void *);
+    va_end(ap);
+
+    return ioctl(hw_data->fd, cmd, arg);
+}
+
+static int pcm_hw_poll(void *data __attribute__((unused)),
+                        struct pollfd *pfd, nfds_t nfds, int timeout)
+{
+    return poll(pfd, nfds, timeout);
+}
+
+static void *pcm_hw_mmap(void *data, void *addr, size_t length, int prot,
+                       int flags, off_t offset)
+{
+    struct pcm_hw_data *hw_data = data;
+
+    return mmap(addr, length, prot, flags, hw_data->fd, offset);
+}
+
+static int pcm_hw_munmap(void *data __attribute__((unused)), void *addr, size_t length)
+{
+    return munmap(addr, length);
+}
+
+static int pcm_hw_open(unsigned int card, unsigned int device,
+                unsigned int flags, void **data, struct snd_node *node)
+{
+    struct pcm_hw_data *hw_data;
+    char fn[256];
+    int fd;
+
+    hw_data = calloc(1, sizeof(*hw_data));
+    if (!hw_data) {
+        return -ENOMEM;
+    }
+
+    snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
+             flags & PCM_IN ? 'c' : 'p');
+    if (flags & PCM_NONBLOCK)
+        fd = open(fn, O_RDWR|O_NONBLOCK);
+    else
+        fd = open(fn, O_RDWR);
+
+    if (fd < 0) {
+        free(hw_data);
+        return fd;
+    }
+
+    hw_data->card = card;
+    hw_data->device = device;
+    hw_data->fd = fd;
+    hw_data->node = node;
+
+    *data = hw_data;
+
+    return fd;
+}
+
+const struct pcm_ops hw_ops = {
+    .open = pcm_hw_open,
+    .close = pcm_hw_close,
+    .ioctl = pcm_hw_ioctl,
+    .mmap = pcm_hw_mmap,
+    .munmap = pcm_hw_munmap,
+    .poll = pcm_hw_poll,
+};
+
diff --git a/src/pcm_io.h b/src/pcm_io.h
new file mode 100644
index 0000000..3c622fd
--- /dev/null
+++ b/src/pcm_io.h
@@ -0,0 +1,52 @@
+/* pcm_io.h
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#ifndef TINYALSA_SRC_PCM_IO_H
+#define TINYALSA_SRC_PCM_IO_H
+
+#include <poll.h>
+#include <sound/asound.h>
+
+struct snd_node;
+
+struct pcm_ops {
+    int (*open) (unsigned int card, unsigned int device,
+                 unsigned int flags, void **data, struct snd_node *node);
+    void (*close) (void *data);
+    int (*ioctl) (void *data, unsigned int cmd, ...);
+    void *(*mmap) (void *data, void *addr, size_t length, int prot, int flags,
+                   off_t offset);
+    int (*munmap) (void *data, void *addr, size_t length);
+    int (*poll) (void *data, struct pollfd *pfd, nfds_t nfds, int timeout);
+};
+
+extern const struct pcm_ops hw_ops;
+extern const struct pcm_ops plug_ops;
+
+#endif /* TINYALSA_SRC_PCM_IO_H */
diff --git a/src/pcm_plugin.c b/src/pcm_plugin.c
new file mode 100644
index 0000000..15bfc80
--- /dev/null
+++ b/src/pcm_plugin.c
@@ -0,0 +1,778 @@
+/* pcm_plugin.c
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <poll.h>
+#include <dlfcn.h>
+
+#include <sys/ioctl.h>
+#include <linux/ioctl.h>
+#include <sound/asound.h>
+#include <tinyalsa/asoundlib.h>
+#include <tinyalsa/plugin.h>
+
+#include "pcm_io.h"
+#include "snd_card_plugin.h"
+
+/* 2 words of uint32_t = 64 bits of mask */
+#define PCM_MASK_SIZE (2)
+#define ARRAY_SIZE(a)         \
+    (sizeof(a) / sizeof(a[0]))
+
+#define PCM_PARAM_GET_MASK(p, n)    \
+    &p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK];
+
+enum {
+    PCM_PLUG_HW_PARAM_SELECT_MIN,
+    PCM_PLUG_HW_PARAM_SELECT_MAX,
+    PCM_PLUG_HW_PARAM_SELECT_VAL,
+};
+
+enum {
+    PCM_PLUG_STATE_OPEN,
+    PCM_PLUG_STATE_SETUP,
+    PCM_PLUG_STATE_PREPARED,
+    PCM_PLUG_STATE_RUNNING,
+};
+
+struct pcm_plug_data {
+    unsigned int card;
+    unsigned int device;
+    unsigned int fd;
+    unsigned int flags;
+
+    void *dl_hdl;
+    /** pointer to plugin operation */
+    const struct pcm_plugin_ops *ops;
+    struct pcm_plugin *plugin;
+    void *dev_node;
+};
+
+static unsigned int param_list[] = {
+    SNDRV_PCM_HW_PARAM_SAMPLE_BITS,
+    SNDRV_PCM_HW_PARAM_CHANNELS,
+    SNDRV_PCM_HW_PARAM_RATE,
+    SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+    SNDRV_PCM_HW_PARAM_PERIODS,
+};
+
+static int convert_plugin_to_pcm_state(int plugin_state)
+{
+    switch (plugin_state) {
+    case PCM_PLUG_STATE_SETUP:
+        return PCM_STATE_SETUP;
+    case PCM_PLUG_STATE_RUNNING:
+        return PCM_STATE_RUNNING;
+    case PCM_PLUG_STATE_PREPARED:
+        return PCM_STATE_PREPARED;
+    case PCM_PLUG_STATE_OPEN:
+        return PCM_STATE_OPEN;
+    default:
+        break;
+    }
+
+    return PCM_STATE_OPEN;
+}
+
+static void pcm_plug_close(void *data)
+{
+    struct pcm_plug_data *plug_data = data;
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    plug_data->ops->close(plugin);
+    dlclose(plug_data->dl_hdl);
+
+    free(plug_data);
+}
+
+static int pcm_plug_info(struct pcm_plug_data *plug_data,
+                struct snd_pcm_info *info)
+{
+    int stream = SNDRV_PCM_STREAM_PLAYBACK;
+    int ret = 0, val = -1;
+    char *name;
+
+    memset(info, 0, sizeof(*info));
+
+    if (plug_data->flags & PCM_IN) {
+        stream = SNDRV_PCM_STREAM_CAPTURE;
+        ret = snd_utils_get_int(plug_data->dev_node, "capture", &val);
+        if (ret || !val) {
+            fprintf(stderr, "%s: not a capture device\n", __func__);
+            return -EINVAL;
+        }
+    } else {
+        stream = SNDRV_PCM_STREAM_PLAYBACK;
+        ret = snd_utils_get_int(plug_data->dev_node, "playback", &val);
+        if (ret || !val) {
+            fprintf(stderr, "%s: not a playback device\n", __func__);
+            return -EINVAL;
+        }
+    }
+
+    info->stream = stream;
+    info->card = plug_data->card;
+    info->device = plug_data->device;
+
+    ret = snd_utils_get_str(plug_data->dev_node, "name", &name);
+    if (ret) {
+        fprintf(stderr, "%s: failed to get pcm device name\n", __func__);
+        return ret;
+    }
+
+    strncpy((char *)info->id, name, sizeof(info->id));
+    strncpy((char *)info->name, name, sizeof(info->name));
+    strncpy((char *)info->subname, name, sizeof(info->subname));
+
+    info->subdevices_count = 1;
+
+    return ret;
+}
+
+static void pcm_plug_set_mask(struct snd_pcm_hw_params *p, int n, uint64_t v)
+{
+    struct snd_mask *mask;
+
+    mask = PCM_PARAM_GET_MASK(p, n);
+
+    mask->bits[0] |= (v & 0xFFFFFFFF);
+    mask->bits[1] |= ((v >> 32) & 0xFFFFFFFF);
+    /*
+     * currently only supporting 64 bits, may need to update to support
+     * more than 64 bits
+     */
+}
+
+static void pcm_plug_set_interval(struct snd_pcm_hw_params *params,
+                    int p, struct pcm_plugin_min_max *v, int is_integer)
+{
+    struct snd_interval *i;
+
+    i = &params->intervals[p - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL];
+
+    i->min = v->min;
+    i->max = v->max;
+
+    if (is_integer)
+        i->integer = 1;
+}
+
+static int pcm_plug_frames_to_bytes(unsigned int frames,
+                                    unsigned int frame_bits)
+{
+    return (frames * (frame_bits / 8));
+}
+
+static int pcm_plug_bytes_to_frames(unsigned int size,
+                                    unsigned int frame_bits)
+{
+    return (size * 8)  / frame_bits;
+}
+
+static int pcm_plug_get_params(struct pcm_plugin *plugin,
+                struct snd_pcm_hw_params *params)
+{
+    struct pcm_plugin_min_max bw, ch, pb, periods;
+    struct pcm_plugin_min_max val;
+    struct pcm_plugin_min_max frame_bits, buffer_bytes;
+
+    /*
+     * populate the struct snd_pcm_hw_params structure
+     * using the hw_param constraints provided by plugin
+     * via the plugin->constraints
+     */
+
+    /* Set the mask params */
+    pcm_plug_set_mask(params, SNDRV_PCM_HW_PARAM_ACCESS,
+                      plugin->constraints->access);
+    pcm_plug_set_mask(params, SNDRV_PCM_HW_PARAM_FORMAT,
+                      plugin->constraints->format);
+    pcm_plug_set_mask(params, SNDRV_PCM_HW_PARAM_SUBFORMAT,
+                      SNDRV_PCM_SUBFORMAT_STD);
+
+    /* Set the standard interval params */
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_SAMPLE_BITS,
+                          &plugin->constraints->bit_width, 1);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS,
+                          &plugin->constraints->channels, 1);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_RATE,
+                          &plugin->constraints->rate, 1);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
+                          &plugin->constraints->period_bytes, 0);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_PERIODS,
+                          &plugin->constraints->periods, 1);
+
+    /* set the calculated interval params */
+
+    bw.min = plugin->constraints->bit_width.min;
+    bw.max = plugin->constraints->bit_width.max;
+
+    ch.min = plugin->constraints->channels.min;
+    ch.max = plugin->constraints->channels.max;
+
+    pb.min = plugin->constraints->period_bytes.min;
+    pb.max = plugin->constraints->period_bytes.max;
+
+    periods.min = plugin->constraints->periods.min;
+    periods.max = plugin->constraints->periods.max;
+
+    /* Calculate and set frame bits */
+    frame_bits.min = bw.min * ch.min;
+    frame_bits.max = bw.max * ch.max;
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_FRAME_BITS,
+                          &frame_bits, 1);
+
+
+    /* Calculate and set period_size in frames */
+    val.min = pcm_plug_bytes_to_frames(pb.min, frame_bits.min);
+    val.max = pcm_plug_bytes_to_frames(pb.max, frame_bits.min);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
+                          &val, 1);
+
+    /* Calculate and set buffer_bytes */
+    buffer_bytes.min = pb.min * periods.min;
+    buffer_bytes.max = pb.max * periods.max;
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_BUFFER_BYTES,
+                          &buffer_bytes, 1);
+
+    /* Calculate and set buffer_size in frames */
+    val.min = pcm_plug_bytes_to_frames(buffer_bytes.min, frame_bits.min);
+    val.max = pcm_plug_bytes_to_frames(buffer_bytes.max, frame_bits.min);
+    pcm_plug_set_interval(params, SNDRV_PCM_HW_PARAM_BUFFER_SIZE,
+                          &val, 1);
+    return 0;
+}
+
+static int pcm_plug_masks_refine(struct snd_pcm_hw_params *p,
+                struct snd_pcm_hw_params *c)
+{
+    struct snd_mask *req_mask;
+    struct snd_mask *con_mask;
+    unsigned int idx, i, masks;
+
+    masks = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK;
+
+    for (idx = 0; idx <= masks; idx++) {
+
+        if (!(p->rmask & (1 << (idx + SNDRV_PCM_HW_PARAM_FIRST_MASK))))
+            continue;
+
+        req_mask = PCM_PARAM_GET_MASK(p, idx);
+        con_mask = PCM_PARAM_GET_MASK(c, idx);
+
+        /*
+         * set the changed mask if requested mask value is not the same as
+         * constrained mask value
+         */
+        if (memcmp(req_mask, con_mask, PCM_MASK_SIZE * sizeof(uint32_t)))
+            p->cmask |= 1 << (idx + SNDRV_PCM_HW_PARAM_FIRST_MASK);
+
+        /* Actually change the requested mask to constrained mask */
+        for (i = 0; i < PCM_MASK_SIZE; i++)
+            req_mask->bits[i] &= con_mask->bits[i];
+    }
+
+    return 0;
+}
+
+static int pcm_plug_interval_refine(struct snd_pcm_hw_params *p,
+                struct snd_pcm_hw_params *c)
+{
+    struct snd_interval *ri;
+    struct snd_interval *ci;
+    unsigned int idx;
+    unsigned int intervals;
+    int changed = 0;
+
+    intervals = SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
+                SNDRV_PCM_HW_PARAM_FIRST_INTERVAL;
+
+    for (idx = 0; idx <= intervals; idx++) {
+        ri = &p->intervals[idx];
+        ci = &c->intervals[idx];
+
+        if (!(p->rmask & (1 << (idx + SNDRV_PCM_HW_PARAM_FIRST_INTERVAL)) ))
+            continue;
+
+        if (ri->min < ci->min) {
+            ri->min = ci->min;
+            ri->openmin = ci->openmin;
+            changed = 1;
+        } else if (ri->min == ci->min && !ri->openmin && ci->openmin) {
+            ri->openmin = 1;
+            changed = 1;
+        }
+
+        if (ri->max > ci->max) {
+            ri->max = ci->max;
+            ri->openmax = ci->openmax;
+            changed = 1;
+        } else if (ri->max == ci->max && !ri->openmax && ci->openmax) {
+            ri->openmax = 1;
+            changed = 1;
+        };
+
+        if (!ri->integer && ci->integer) {
+            ri->integer = 1;
+            changed = 1;
+        }
+
+        if (ri->integer) {
+            if (ri->openmin) {
+                ri->min++;
+                ri->openmin = 0;
+            }
+            if (ri->openmax) {
+                ri->max--;
+                ri->openmax = 0;
+            }
+        } else if (!ri->openmin && !ri->openmax && ri->min == ri->max) {
+            ri->integer = 1;
+        }
+
+        /* Set the changed mask */
+        if (changed)
+            p->cmask |= (1 << (idx + SNDRV_PCM_HW_PARAM_FIRST_INTERVAL));
+    }
+
+    return 0;
+}
+
+
+static int pcm_plug_hw_params_refine(struct snd_pcm_hw_params *p,
+                struct snd_pcm_hw_params *c)
+{
+    int rc;
+
+    rc = pcm_plug_masks_refine(p, c);
+    if (rc) {
+        fprintf(stderr, "%s: masks refine failed %d\n", __func__, rc);
+        return rc;
+    }
+
+    rc = pcm_plug_interval_refine(p, c);
+    if (rc) {
+        fprintf(stderr, "%s: interval refine failed %d\n", __func__, rc);
+        return rc;
+    }
+
+    /* clear the requested params */
+    p->rmask = 0;
+
+    return rc;
+}
+
+static int __pcm_plug_hrefine(struct pcm_plug_data *plug_data,
+                struct snd_pcm_hw_params *params)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    struct snd_pcm_hw_params plug_params;
+    int rc;
+
+    memset(&plug_params, 0, sizeof(plug_params));
+    rc = pcm_plug_get_params(plugin, &plug_params);
+    if (rc) {
+        fprintf(stderr, "%s: pcm_plug_get_params failed %d\n",
+               __func__, rc);
+        return -EINVAL;
+    }
+
+    return pcm_plug_hw_params_refine(params, &plug_params);
+
+}
+
+static int pcm_plug_hrefine(struct pcm_plug_data *plug_data,
+                struct snd_pcm_hw_params *params)
+{
+    return __pcm_plug_hrefine(plug_data, params);
+}
+
+static int pcm_plug_interval_select(struct snd_pcm_hw_params *p,
+        unsigned int param, unsigned int select, unsigned int val)
+{
+    struct snd_interval *i;
+
+    if (param < SNDRV_PCM_HW_PARAM_FIRST_INTERVAL ||
+        param > SNDRV_PCM_HW_PARAM_LAST_INTERVAL)
+        return -EINVAL;
+
+    i = &p->intervals[param - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL];
+
+    if (!i->min)
+        return -EINVAL;
+
+    switch (select) {
+
+    case PCM_PLUG_HW_PARAM_SELECT_MIN:
+        i->max = i->min;
+        break;
+
+    case PCM_PLUG_HW_PARAM_SELECT_MAX:
+        i->min = i->max;
+        break;
+
+    case PCM_PLUG_HW_PARAM_SELECT_VAL:
+        i->min = i->max = val;
+        break;
+
+    default:
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+static void pcm_plug_hw_params_set(struct snd_pcm_hw_params *p)
+{
+    unsigned int i, select;
+    unsigned int bw, ch, period_sz, periods;
+    unsigned int val1, val2, offset;
+
+    offset = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL;
+
+    /* Select the min values first */
+    select = PCM_PLUG_HW_PARAM_SELECT_MIN;
+    for (i = 0; i < ARRAY_SIZE(param_list); i++)
+        pcm_plug_interval_select(p, param_list[i], select, 0);
+
+    /* Select calculated values */
+    select = PCM_PLUG_HW_PARAM_SELECT_VAL;
+    bw = (p->intervals[SNDRV_PCM_HW_PARAM_SAMPLE_BITS - offset]).min;
+    ch = (p->intervals[SNDRV_PCM_HW_PARAM_CHANNELS - offset]).min;
+    period_sz = (p->intervals[SNDRV_PCM_HW_PARAM_PERIOD_SIZE - offset]).min;
+    periods = (p->intervals[SNDRV_PCM_HW_PARAM_PERIODS - offset]).min;
+
+    val1 = bw * ch;        // frame_bits;
+    pcm_plug_interval_select(p, SNDRV_PCM_HW_PARAM_FRAME_BITS, select, val1);
+
+    val2 = pcm_plug_frames_to_bytes(period_sz, val1); // period_bytes;
+    pcm_plug_interval_select(p, SNDRV_PCM_HW_PARAM_PERIOD_BYTES, select,
+                             val2);
+
+    val2 = period_sz * periods; //buffer_size;
+    pcm_plug_interval_select(p, SNDRV_PCM_HW_PARAM_BUFFER_SIZE, select, val2);
+
+    val2 = pcm_plug_frames_to_bytes(period_sz * periods, val1); //buffer_bytes;
+    pcm_plug_interval_select(p, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, select, val2);
+}
+
+static int pcm_plug_hparams(struct pcm_plug_data *plug_data,
+                struct snd_pcm_hw_params *params)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int rc;
+
+    if (plugin->state != PCM_PLUG_STATE_OPEN)
+            return -EBADFD;
+
+    params->rmask = ~0U;
+
+    rc = __pcm_plug_hrefine(plug_data, params);
+    if (rc) {
+        fprintf(stderr, "%s: __pcm_plug_hrefine failed %d\n",
+               __func__, rc);
+        return rc;
+    }
+
+    pcm_plug_hw_params_set(params);
+
+    rc = plug_data->ops->hw_params(plugin, params);
+    if (!rc)
+        plugin->state = PCM_PLUG_STATE_SETUP;
+
+    return rc;
+}
+
+static int pcm_plug_sparams(struct pcm_plug_data *plug_data,
+                struct snd_pcm_sw_params *params)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state != PCM_PLUG_STATE_SETUP)
+        return -EBADFD;
+
+    return plug_data->ops->sw_params(plugin, params);
+}
+
+static int pcm_plug_sync_ptr(struct pcm_plug_data *plug_data,
+                struct snd_pcm_sync_ptr *sync_ptr)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int ret = -EBADFD;
+
+    if (plugin->state >= PCM_PLUG_STATE_SETUP) {
+        ret = plug_data->ops->sync_ptr(plugin, sync_ptr);
+        if (ret == 0)
+            sync_ptr->s.status.state = convert_plugin_to_pcm_state(plugin->state);
+    }
+
+    return ret;
+}
+
+static int pcm_plug_writei_frames(struct pcm_plug_data *plug_data,
+                struct snd_xferi *x)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state != PCM_PLUG_STATE_PREPARED &&
+        plugin->state != PCM_PLUG_STATE_RUNNING)
+        return -EBADFD;
+
+    return plug_data->ops->writei_frames(plugin, x);
+}
+
+static int pcm_plug_readi_frames(struct pcm_plug_data *plug_data,
+                struct snd_xferi *x)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state != PCM_PLUG_STATE_RUNNING)
+        return -EBADFD;
+
+    return plug_data->ops->readi_frames(plugin, x);
+}
+
+static int pcm_plug_ttstamp(struct pcm_plug_data *plug_data,
+                int *tstamp)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state < PCM_PLUG_STATE_SETUP)
+        return -EBADFD;
+
+    return plug_data->ops->ttstamp(plugin, tstamp);
+}
+
+static int pcm_plug_prepare(struct pcm_plug_data *plug_data)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int rc;
+
+    if (plugin->state != PCM_PLUG_STATE_SETUP)
+        return -EBADFD;
+
+    rc = plug_data->ops->prepare(plugin);
+    if (!rc)
+        plugin->state = PCM_PLUG_STATE_PREPARED;
+
+    return rc;
+}
+
+static int pcm_plug_start(struct pcm_plug_data *plug_data)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int rc;
+
+    if (plugin->state != PCM_PLUG_STATE_PREPARED)
+        return -EBADFD;
+
+    rc = plug_data->ops->start(plugin);
+    if (!rc)
+        plugin->state = PCM_PLUG_STATE_RUNNING;
+
+    return rc;
+}
+
+static int pcm_plug_drop(struct pcm_plug_data *plug_data)
+{
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int rc;
+
+    rc = plug_data->ops->drop(plugin);
+    if (!rc)
+        plugin->state = PCM_PLUG_STATE_SETUP;
+
+    return rc;
+}
+
+static int pcm_plug_ioctl(void *data, unsigned int cmd, ...)
+{
+    struct pcm_plug_data *plug_data = data;
+    struct pcm_plugin *plugin = plug_data->plugin;
+    int ret;
+    va_list ap;
+    void *arg;
+
+    va_start(ap, cmd);
+    arg = va_arg(ap, void *);
+    va_end(ap);
+
+    switch (cmd) {
+    case SNDRV_PCM_IOCTL_INFO:
+        ret = pcm_plug_info(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_TTSTAMP:
+        ret = pcm_plug_ttstamp(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_HW_REFINE:
+        ret = pcm_plug_hrefine(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_HW_PARAMS:
+        ret = pcm_plug_hparams(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_SW_PARAMS:
+        ret = pcm_plug_sparams(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_SYNC_PTR:
+        ret = pcm_plug_sync_ptr(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_PREPARE:
+        ret = pcm_plug_prepare(plug_data);
+        break;
+    case SNDRV_PCM_IOCTL_START:
+        ret = pcm_plug_start(plug_data);
+        break;
+    case SNDRV_PCM_IOCTL_DROP:
+        ret = pcm_plug_drop(plug_data);
+        break;
+    case SNDRV_PCM_IOCTL_WRITEI_FRAMES:
+        ret = pcm_plug_writei_frames(plug_data, arg);
+        break;
+    case SNDRV_PCM_IOCTL_READI_FRAMES:
+        ret = pcm_plug_readi_frames(plug_data, arg);
+        break;
+    default:
+        ret = plug_data->ops->ioctl(plugin, cmd, arg);
+        break;
+    }
+
+    return ret;
+}
+
+static int pcm_plug_poll(void *data, struct pollfd *pfd, nfds_t nfds,
+        int timeout)
+{
+    struct pcm_plug_data *plug_data = data;
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    return plug_data->ops->poll(plugin, pfd, nfds, timeout);
+}
+
+static void *pcm_plug_mmap(void *data, void *addr, size_t length, int prot,
+                       int flags, off_t offset)
+{
+    struct pcm_plug_data *plug_data = data;
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state != PCM_PLUG_STATE_SETUP)
+        return NULL;
+
+    return plug_data->ops->mmap(plugin, addr, length, prot, flags, offset);
+}
+
+static int pcm_plug_munmap(void *data, void *addr, size_t length)
+{
+    struct pcm_plug_data *plug_data = data;
+    struct pcm_plugin *plugin = plug_data->plugin;
+
+    if (plugin->state != PCM_PLUG_STATE_SETUP)
+        return -EBADFD;
+
+    return plug_data->ops->munmap(plugin, addr, length);
+}
+
+static int pcm_plug_open(unsigned int card, unsigned int device,
+                         unsigned int flags, void **data, struct snd_node *pcm_node)
+{
+    struct pcm_plug_data *plug_data;
+    void *dl_hdl;
+    int rc = 0;
+    char *so_name;
+
+    plug_data = calloc(1, sizeof(*plug_data));
+    if (!plug_data) {
+        return -ENOMEM;
+    }
+
+    rc = snd_utils_get_str(pcm_node, "so-name", &so_name);
+    if (rc) {
+        fprintf(stderr, "%s: failed to get plugin lib name\n", __func__);
+        goto err_get_lib;
+    }
+
+    dl_hdl = dlopen(so_name, RTLD_NOW);
+    if (!dl_hdl) {
+        fprintf(stderr, "%s: unable to open %s\n", __func__, so_name);
+        goto err_dl_open;
+    } else {
+        fprintf(stderr, "%s: dlopen successful for %s\n", __func__, so_name);
+    }
+
+    dlerror();
+
+    plug_data->ops = dlsym(dl_hdl, "pcm_plugin_ops");
+    if (!plug_data->ops) {
+        fprintf(stderr, "%s: dlsym to open fn failed, err = '%s'\n",
+                __func__, dlerror());
+        goto err_dlsym;
+    }
+
+    rc = plug_data->ops->open(&plug_data->plugin, card, device, flags);
+    if (rc) {
+        fprintf(stderr, "%s: failed to open plugin\n", __func__);
+        goto err_open;
+    }
+
+    plug_data->dl_hdl = dl_hdl;
+    plug_data->card = card;
+    plug_data->device = device;
+    plug_data->dev_node = pcm_node;
+    plug_data->flags = flags;
+
+    *data = plug_data;
+
+    plug_data->plugin->state = PCM_PLUG_STATE_OPEN;
+
+    return 0;
+
+err_open:
+err_dlsym:
+    dlclose(dl_hdl);
+err_get_lib:
+err_dl_open:
+    free(plug_data);
+
+    return rc;
+}
+
+const struct pcm_ops plug_ops = {
+    .open = pcm_plug_open,
+    .close = pcm_plug_close,
+    .ioctl = pcm_plug_ioctl,
+    .mmap = pcm_plug_mmap,
+    .munmap = pcm_plug_munmap,
+    .poll = pcm_plug_poll,
+};
diff --git a/src/snd_card_plugin.c b/src/snd_card_plugin.c
new file mode 100644
index 0000000..c6d4baf
--- /dev/null
+++ b/src/snd_card_plugin.c
@@ -0,0 +1,149 @@
+/* snd_card_plugin.c
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#include "snd_card_plugin.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#define SND_DLSYM(h, p, s, err) \
+do {                            \
+    err = 0;                    \
+    p = dlsym(h, s);            \
+    if (!p)                        \
+        err = -ENODEV;            \
+} while(0)
+
+int snd_utils_get_int(struct snd_node *node, const char *prop, int *val)
+{
+    if (!node || !node->card_node || !node->dev_node)
+        return SND_NODE_TYPE_HW;
+
+    return node->ops->get_int(node->dev_node, prop, val);
+}
+
+int snd_utils_get_str(struct snd_node *node, const char *prop, char **val)
+{
+    if (!node || !node->card_node || !node->dev_node)
+        return SND_NODE_TYPE_HW;
+
+    return node->ops->get_str(node->dev_node, prop, val);
+}
+
+void snd_utils_close_dev_node(struct snd_node *node)
+{
+    if (!node)
+        return;
+
+    if (node->card_node)
+        node->ops->close_card(node->card_node);
+
+    if (node->dl_hdl)
+        dlclose(node->dl_hdl);
+
+    free(node);
+}
+
+enum snd_node_type snd_utils_get_node_type(struct snd_node *node)
+{
+    int val = SND_NODE_TYPE_HW;
+
+    if (!node || !node->card_node || !node->dev_node)
+        return SND_NODE_TYPE_HW;
+
+    node->ops->get_int(node->dev_node, "type", &val);
+
+    return val;
+}
+
+static int snd_utils_resolve_symbols(struct snd_node *node)
+{
+    void *dl = node->dl_hdl;
+    int err;
+    SND_DLSYM(dl, node->ops, "snd_card_ops", err);
+    return err;
+}
+
+static struct snd_node *snd_utils_open_dev_node(unsigned int card,
+                                                unsigned int device,
+                                                int dev_type)
+{
+    struct snd_node *node;
+    int rc = 0;
+
+    node = calloc(1, sizeof(*node));
+    if (!node)
+        return NULL;
+
+    node->dl_hdl = dlopen("libsndcardparser.so", RTLD_NOW);
+    if (!node->dl_hdl) {
+        goto err_dl_open;
+    }
+
+    rc = snd_utils_resolve_symbols(node);
+    if (rc < 0)
+        goto err_resolve_symbols;
+
+    node->card_node = node->ops->open_card(card);
+    if (!node->card_node)
+        goto err_resolve_symbols;
+
+    if (dev_type == NODE_PCM) {
+      node->dev_node = node->ops->get_pcm(node->card_node, device);
+    } else {
+      node->dev_node = node->ops->get_mixer(node->card_node);
+    }
+
+    if (!node->dev_node)
+        goto err_get_node;
+
+    return node;
+
+err_get_node:
+    node->ops->close_card(node->card_node);
+
+err_resolve_symbols:
+    dlclose(node->dl_hdl);
+
+err_dl_open:
+    free(node);
+    return NULL;
+}
+
+struct snd_node* snd_utils_open_pcm(unsigned int card,
+                                    unsigned int device)
+{
+  return snd_utils_open_dev_node(card, device, NODE_PCM);
+}
+
+struct snd_node* snd_utils_open_mixer(unsigned int card)
+{
+  return snd_utils_open_dev_node(card, 0, NODE_MIXER);
+}
diff --git a/src/snd_card_plugin.h b/src/snd_card_plugin.h
new file mode 100644
index 0000000..b80695e
--- /dev/null
+++ b/src/snd_card_plugin.h
@@ -0,0 +1,74 @@
+/* snd_utils.h
+** Copyright (c) 2019, The Linux Foundation.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above
+**     copyright notice, this list of conditions and the following
+**     disclaimer in the documentation and/or other materials provided
+**     with the distribution.
+**   * Neither the name of The Linux Foundation nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+** ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+** BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+** BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+** OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+** IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+#ifndef TINYALSA_SRC_SND_CARD_UTILS_H
+#define TINYALSA_SRC_SND_CARD_UTILS_H
+
+#include <tinyalsa/plugin.h>
+
+#include <dlfcn.h>
+
+/** Encapsulates the pcm device definition from
+ * the sound card definition configuration file.
+ */
+struct snd_node {
+    /** Pointer the card definition */
+    void *card_node;
+    /** Pointer to device definition, either PCM or MIXER device */
+    void *dev_node;
+    /** Pointer to the sound card parser library */
+    void *dl_hdl;
+    /** A pointer to the operations structure. */
+    const struct snd_node_ops* ops;
+};
+
+enum snd_node_type {
+    SND_NODE_TYPE_HW = 0,
+    SND_NODE_TYPE_PLUGIN,
+    SND_NODE_TYPE_INVALID,
+};
+
+enum {
+  NODE_PCM,
+  NODE_MIXER
+};
+
+struct snd_node *snd_utils_open_pcm(unsigned int card, unsigned int device);
+
+struct snd_node *snd_utils_open_mixer(unsigned int card);
+
+void snd_utils_close_dev_node(struct snd_node *node);
+
+enum snd_node_type snd_utils_get_node_type(struct snd_node *node);
+
+int snd_utils_get_int(struct snd_node *node, const char *prop, int *val);
+
+int snd_utils_get_str(struct snd_node *node, const char *prop, char **val);
+
+#endif /* end of TINYALSA_SRC_SND_CARD_UTILS_H */
diff --git a/tests/include/pcm_test_device.h b/tests/include/pcm_test_device.h
new file mode 100644
index 0000000..7ced192
--- /dev/null
+++ b/tests/include/pcm_test_device.h
@@ -0,0 +1,54 @@
+/* pcm_test.h
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#ifndef TINYALSA_TESTS_PCM_TEST_H_
+#define TINYALSA_TESTS_PCM_TEST_H_
+
+namespace tinyalsa {
+namespace testing {
+
+#ifndef TEST_LOOPBACK_CARD
+#define TEST_LOOPBACK_CARD 2
+#endif
+
+#ifndef TEST_LOOPBACK_PLAYBACK_DEVICE
+#define TEST_LOOPBACK_PLAYBACK_DEVICE 0
+#endif
+
+#ifndef TEST_LOOPBACK_CAPTURE_DEVICE
+#define TEST_LOOPBACK_CAPTURE_DEVICE 1
+#endif
+
+constexpr unsigned int kLoopbackCard = TEST_LOOPBACK_CARD;
+constexpr unsigned int kLoopbackPlaybackDevice = TEST_LOOPBACK_PLAYBACK_DEVICE;
+constexpr unsigned int kLoopbackCaptureDevice = TEST_LOOPBACK_CAPTURE_DEVICE;
+
+} // namespace testing
+} // namespace tinyalsa
+
+#endif
diff --git a/tests/src/mixer_test.cc b/tests/src/mixer_test.cc
new file mode 100644
index 0000000..717269c
--- /dev/null
+++ b/tests/src/mixer_test.cc
@@ -0,0 +1,316 @@
+/* mixer_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+#include "pcm_test_device.h"
+
+#include <string_view>
+#include <string>
+#include <thread>
+#include <type_traits>
+#include <unordered_map>
+#include <unordered_set>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/mixer.h"
+
+namespace tinyalsa {
+namespace testing {
+
+#ifndef MAX_CARD_INDEX
+#define MAX_CARD_INDEX 2
+#endif
+
+static constexpr unsigned int kMaxCardIndex = MAX_CARD_INDEX;
+
+static constexpr int k100Percent = 100;
+static constexpr int k0Percent = 0;
+
+TEST(MixerTest, OpenAndClose) {
+    ASSERT_EQ(mixer_open(1000), nullptr);
+    mixer_close(nullptr);
+}
+
+class MixerTest : public ::testing::TestWithParam<unsigned int> {
+  protected:
+    MixerTest() : mixer_object(nullptr) {}
+    virtual ~MixerTest() = default;
+
+    virtual void SetUp() override {
+        unsigned int card = GetParam();
+        mixer_object = mixer_open(card);
+        ASSERT_NE(mixer_object, nullptr);
+    }
+
+    virtual void TearDown() override {
+        mixer_close(mixer_object);
+    }
+
+    mixer *mixer_object;
+};
+
+TEST_P(MixerTest, AddNewControls) {
+    ASSERT_EQ(mixer_add_new_ctls(mixer_object), 0);
+}
+
+TEST_P(MixerTest, GetName) {
+    const char *name = mixer_get_name(mixer_object);
+    std::cout << name << std::endl;
+    ASSERT_STRNE(name, "");
+}
+
+TEST_P(MixerTest, GetNumberOfControls) {
+    unsigned int nums = mixer_get_num_ctls(mixer_object);
+    std::cout << nums << std::endl;
+    ASSERT_GT(nums, 0);
+}
+
+class MixerControlsTest : public MixerTest {
+  protected:
+    MixerControlsTest() : number_of_controls(0), controls(nullptr) {}
+    virtual ~MixerControlsTest() = default;
+
+    virtual void SetUp() override {
+        MixerTest::SetUp();
+
+        number_of_controls = mixer_get_num_ctls(mixer_object);
+        ASSERT_GT(number_of_controls, 0);
+
+        controls = std::make_unique<const mixer_ctl *[]>(number_of_controls);
+        ASSERT_NE(controls, nullptr);
+
+        for (unsigned int i = 0; i < number_of_controls; i++) {
+            controls[i] = mixer_get_ctl_const(mixer_object, i);
+            ASSERT_EQ(mixer_ctl_get_id(controls[i]), i);
+            ASSERT_STRNE(mixer_ctl_get_name(controls[i]), "");
+            ASSERT_NE(controls[i], nullptr);
+        }
+    }
+
+    virtual void TearDown() override {
+        controls = nullptr;
+        MixerTest::TearDown();
+    }
+
+    unsigned int number_of_controls;
+    std::unique_ptr<const mixer_ctl *[]> controls;
+};
+
+TEST_P(MixerControlsTest, GetNumberOfControlsByName) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        const char *name = mixer_ctl_get_name(controls[i]);
+        ASSERT_GE(mixer_get_num_ctls_by_name(mixer_object, name), 1);
+    }
+
+    std::string name{mixer_ctl_get_name(controls[0])};
+    name += "1";
+    ASSERT_EQ(mixer_get_num_ctls_by_name(mixer_object, name.c_str()), 0);
+}
+
+TEST_P(MixerControlsTest, GetControlById) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        ASSERT_EQ(mixer_get_ctl(mixer_object, i), controls[i]);
+    }
+
+    ASSERT_EQ(mixer_get_ctl(mixer_object, number_of_controls), nullptr);
+}
+
+TEST_P(MixerControlsTest, GetControlByName) {
+    std::unordered_set<std::string> visited_names_set;
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        std::string name{mixer_ctl_get_name(controls[i])};
+        if (visited_names_set.find(name) == visited_names_set.end()) {
+            ASSERT_EQ(mixer_get_ctl_by_name(mixer_object, name.c_str()), controls[i]);
+            visited_names_set.insert(name);
+        }
+    }
+}
+
+TEST_P(MixerControlsTest, GetControlByNameAndIndex) {
+    std::unordered_map<std::string, int32_t> visited_names_and_count_map;
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        std::string name{mixer_ctl_get_name(controls[i])};
+        if (visited_names_and_count_map.find(name) == visited_names_and_count_map.end()) {
+            visited_names_and_count_map[name] = 0;
+        }
+        ASSERT_EQ(
+                mixer_get_ctl_by_name_and_index(mixer_object,
+                                                name.c_str(),
+                                                visited_names_and_count_map[name]),
+                controls[i]);
+        visited_names_and_count_map[name] = visited_names_and_count_map[name] + 1;
+    }
+}
+
+static inline bool IsValidTypeString(std::string& type) {
+    return type == "BOOL" || type == "INT" || type == "ENUM" || type == "BYTE" ||
+            type == "IEC958" || type == "INT64";
+}
+
+TEST_P(MixerControlsTest, GetControlTypeString) {
+    ASSERT_STREQ(mixer_ctl_get_type_string(nullptr), "");
+
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        std::string type{mixer_ctl_get_type_string(controls[i])};
+        ASSERT_TRUE(IsValidTypeString(type));
+    }
+}
+
+TEST_P(MixerControlsTest, GetNumberOfValues) {
+    ASSERT_EQ(mixer_ctl_get_num_values(nullptr), 0);
+}
+
+TEST_P(MixerControlsTest, GetNumberOfEnumsAndEnumString) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        const mixer_ctl *control = controls[i];
+        if (mixer_ctl_get_type(control) == MIXER_CTL_TYPE_ENUM) {
+            unsigned int number_of_enums = mixer_ctl_get_num_enums(control);
+            ASSERT_GT(number_of_enums, 0);
+            for (unsigned int enum_id = 0; enum_id < number_of_enums; ++enum_id) {
+                const char *enum_name = mixer_ctl_get_enum_string(
+                        const_cast<mixer_ctl *>(control),
+                        enum_id);
+                ASSERT_STRNE(enum_name, "");
+            }
+        }
+    }
+}
+
+TEST_P(MixerControlsTest, UpdateControl) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        mixer_ctl_update(const_cast<mixer_ctl *>(controls[i]));
+    }
+}
+
+TEST_P(MixerControlsTest, GetPercent) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        const mixer_ctl *control = controls[i];
+        if (mixer_ctl_get_type(control) == MIXER_CTL_TYPE_INT) {
+            unsigned int number_of_values = mixer_ctl_get_num_values(controls[i]);
+            std::unique_ptr<long []> values = std::make_unique<long []>(number_of_values);
+            mixer_ctl_get_array(control, values.get(), number_of_values);
+            for (unsigned int value_id = 0; value_id < number_of_values; ++value_id) {
+                int max = mixer_ctl_get_range_max(control);
+                int min = mixer_ctl_get_range_min(control);
+                int percent = mixer_ctl_get_percent(control, value_id);
+                ASSERT_GE(percent, k0Percent);
+                ASSERT_LE(percent, k100Percent);
+                int range = max - min;
+                ASSERT_EQ(percent, (values[value_id] - min) * k100Percent / range);
+            }
+        } else {
+            ASSERT_EQ(mixer_ctl_get_percent(control, 0), -EINVAL);
+        }
+    }
+}
+
+TEST_P(MixerControlsTest, SetPercent) {
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        const mixer_ctl *control = controls[i];
+        if (mixer_ctl_get_type(control) == MIXER_CTL_TYPE_INT) {
+            unsigned int number_of_values = mixer_ctl_get_num_values(controls[i]);
+            std::unique_ptr<long []> values = std::make_unique<long []>(number_of_values);
+            mixer_ctl_get_array(control, values.get(), number_of_values);
+            for (unsigned int value_id = 0; value_id < number_of_values; ++value_id) {
+                int max = mixer_ctl_get_range_max(control);
+                int min = mixer_ctl_get_range_min(control);
+                int value = values[value_id];
+                int percent = mixer_ctl_get_percent(control, value_id);
+                if (mixer_ctl_set_percent(
+                        const_cast<mixer_ctl *>(control), value_id, k100Percent) == 0) {
+                    // note: some controls are able to be written, but their values might not be
+                    //   changed.
+                    mixer_ctl_get_array(control, values.get(), number_of_values);
+                    int new_value = values[value_id];
+                    ASSERT_TRUE(new_value == value || new_value == max);
+                }
+                if (mixer_ctl_set_percent(
+                        const_cast<mixer_ctl *>(control), value_id, k0Percent) == 0) {
+                    mixer_ctl_get_array(control, values.get(), number_of_values);
+                    int new_value = values[value_id];
+                    ASSERT_TRUE(new_value == value || new_value == min);
+                }
+                mixer_ctl_set_percent(const_cast<mixer_ctl *>(control), value_id, percent);
+            }
+        } else {
+            ASSERT_EQ(mixer_ctl_get_percent(control, 0), -EINVAL);
+        }
+    }
+}
+
+TEST_P(MixerControlsTest, Event) {
+    ASSERT_EQ(mixer_subscribe_events(mixer_object, 1), 0);
+    const mixer_ctl *control = nullptr;
+    for (unsigned int i = 0; i < number_of_controls; ++i) {
+        std::string_view name{mixer_ctl_get_name(controls[i])};
+
+        if (name.find("Volume") != std::string_view::npos) {
+            control = controls[i];
+        }
+    }
+
+    if (control == nullptr) {
+        GTEST_SKIP() << "No volume control was found in the controls list.";
+    }
+
+    auto *local_mixer_object = mixer_object;
+    int percent = mixer_ctl_get_percent(control, 0);
+    std::thread thread([local_mixer_object, control, percent] () {
+        std::this_thread::sleep_for(std::chrono::milliseconds(50));
+        mixer_ctl_set_percent(
+                const_cast<mixer_ctl *>(control), 0,
+                percent == k100Percent ? k0Percent : k100Percent);
+    });
+
+    EXPECT_EQ(mixer_wait_event(mixer_object, 1000), 1);
+
+    EXPECT_EQ(mixer_consume_event(mixer_object), 0);
+
+    thread.join();
+    ASSERT_EQ(mixer_subscribe_events(mixer_object, 0), 0);
+
+    mixer_ctl_set_percent(const_cast<mixer_ctl *>(control), 0, percent);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    MixerTest,
+    MixerTest,
+    ::testing::Range<unsigned int>(
+        0,
+        kMaxCardIndex + 1
+    ));
+
+INSTANTIATE_TEST_SUITE_P(
+    MixerControlsTest,
+    MixerControlsTest,
+    ::testing::Range<unsigned int>(
+        0,
+        kMaxCardIndex + 1
+    ));
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/tests/src/pcm_in_test.cc b/tests/src/pcm_in_test.cc
new file mode 100644
index 0000000..e912abb
--- /dev/null
+++ b/tests/src/pcm_in_test.cc
@@ -0,0 +1,114 @@
+/* pcm_in_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+#include "pcm_test_device.h"
+
+#include <chrono>
+#include <cstring>
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+class PcmInTest : public ::testing::Test {
+  protected:
+    PcmInTest() : pcm_object(nullptr) {}
+    virtual ~PcmInTest() = default;
+
+    virtual void SetUp() override {
+        pcm_object = pcm_open(kLoopbackCard, kLoopbackCaptureDevice, PCM_IN, &kDefaultConfig);
+        ASSERT_NE(pcm_object, nullptr);
+        ASSERT_TRUE(pcm_is_ready(pcm_object));
+    }
+
+    virtual void TearDown() override {
+        ASSERT_EQ(pcm_close(pcm_object), 0);
+    }
+
+    static constexpr unsigned int kDefaultChannels = 2;
+    static constexpr unsigned int kDefaultSamplingRate = 48000;
+    static constexpr unsigned int kDefaultPeriodSize = 1024;
+    static constexpr unsigned int kDefaultPeriodCount = 3;
+    static constexpr pcm_config kDefaultConfig = {
+        .channels = kDefaultChannels,
+        .rate = kDefaultSamplingRate,
+        .period_size = kDefaultPeriodSize,
+        .period_count = kDefaultPeriodCount,
+        .format = PCM_FORMAT_S16_LE,
+        .start_threshold = 0,
+        .stop_threshold = 0,
+        .silence_threshold = 0,
+        .silence_size = 0,
+    };
+
+    pcm* pcm_object;
+};
+
+TEST_F(PcmInTest, GetDelay) {
+    long delay = pcm_get_delay(pcm_object);
+    std::cout << delay << std::endl;
+    ASSERT_GE(delay, 0);
+}
+
+TEST_F(PcmInTest, Readi) {
+    constexpr uint32_t read_count = 20;
+
+    size_t buffer_size = pcm_frames_to_bytes(pcm_object, kDefaultConfig.period_size);
+    auto buffer = std::make_unique<char[]>(buffer_size);
+
+    int read_frames = 0;
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, buffer_size);
+    auto start = std::chrono::steady_clock::now();
+    for (uint32_t i = 0; i < read_count; ++i) {
+        read_frames = pcm_readi(pcm_object, buffer.get(), frames);
+        ASSERT_EQ(read_frames, frames);
+    }
+
+    std::chrono::duration<double> difference = std::chrono::steady_clock::now() - start;
+    std::chrono::milliseconds expected_elapsed_time_ms(frames * read_count /
+            (kDefaultConfig.rate / 1000));
+
+    std::cout << difference.count() << std::endl;
+    std::cout << expected_elapsed_time_ms.count() << std::endl;
+
+    ASSERT_NEAR(difference.count() * 1000, expected_elapsed_time_ms.count(), 100);
+}
+
+TEST_F(PcmInTest, Writei) {
+    size_t buffer_size = pcm_frames_to_bytes(pcm_object, kDefaultConfig.period_size);
+    auto buffer = std::make_unique<char[]>(buffer_size);
+
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, buffer_size);
+    ASSERT_EQ(pcm_writei(pcm_object, buffer.get(), frames), -EINVAL);
+}
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/tests/src/pcm_loopback_test.cc b/tests/src/pcm_loopback_test.cc
new file mode 100644
index 0000000..6a3ffb8
--- /dev/null
+++ b/tests/src/pcm_loopback_test.cc
@@ -0,0 +1,230 @@
+/* pcm_loopback_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+#include "pcm_test_device.h"
+
+#include <chrono>
+#include <cmath>
+#include <cstring>
+#include <iostream>
+#include <thread>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+template<int32_t CH, int32_t SR, pcm_format F>
+class SilenceGenerator {
+public:
+    pcm_format GetFormat() {
+        return F;
+    }
+
+    int32_t GetChannels() {
+        return CH;
+    };
+
+    int32_t GetSamplingRate() {
+        return SR;
+    };
+
+    virtual int32_t Read(void *buffer, int32_t size) {
+        std::memset(buffer, 0, size);
+        return size;
+    }
+};
+
+template<pcm_format F>
+struct PcmFormat {
+    using Type = void;
+    static constexpr int32_t kMax = 0;
+    static constexpr int32_t kMin = 0;
+};
+
+template<>
+struct PcmFormat<PCM_FORMAT_S16_LE> {
+    using Type = int16_t;
+    static constexpr Type kMax = std::numeric_limits<Type>::max();
+    static constexpr Type kMin = std::numeric_limits<Type>::min();
+};
+
+// CH: channels
+// SR: sampling rate
+// FQ: sine wave frequency
+// L: max level
+template<int32_t CH, int32_t SR, int32_t FQ, int32_t L, pcm_format F>
+class SineToneGenerator : public SilenceGenerator<CH, SR, F> {
+private:
+    using Type = typename PcmFormat<F>::Type;
+    static constexpr double kPi = M_PI;
+    static constexpr double kStep = FQ * CH * kPi / SR;
+
+    double channels[CH];
+    double gain;
+
+    Type GetSample(double radian) {
+        double sine = std::sin(radian) * gain;
+        if (sine >= 1.0) {
+            return PcmFormat<F>::kMax;
+        } else if (sine <= -1.0) {
+            return PcmFormat<F>::kMin;
+        }
+        return static_cast<Type>(sine * PcmFormat<F>::kMax);
+    }
+
+public:
+    SineToneGenerator() {
+        constexpr double phase = (CH == 1) ? 0 : kPi / 2 / (CH - 1);
+
+        channels[0] = 0.0;
+        for (int32_t i = 1; i < CH; ++i) {
+            channels[i] = channels[i - 1] + phase;
+        }
+
+        gain = std::pow(M_E, std::log(10) * static_cast<double>(L) / 20.0);
+    }
+
+    ~SineToneGenerator() = default;
+
+    int32_t Read(void *buffer, int32_t size) override {
+        Type *pcm_buffer = reinterpret_cast<Type *>(buffer);
+
+        size = (size / (CH * sizeof(Type))) * (CH * sizeof(Type));
+        int32_t samples = size / sizeof(Type);
+        int32_t s = 0;
+
+        while (s < samples) {
+            for (int32_t i = 0; i < CH; ++i) {
+                pcm_buffer[s++] = GetSample(channels[i]);
+                channels[i] += kStep;
+            }
+        }
+        return size;
+    }
+};
+
+template<typename T>
+static double Energy(T *buffer, size_t samples) {
+    double sum = 0.0;
+    for (size_t i = 0; i < samples; i++) {
+        sum += static_cast<double>(buffer[i]) * static_cast<double>(buffer[i]);
+    }
+    return sum;
+}
+
+TEST(PcmLoopbackTest, LoopbackS16le) {
+    static constexpr unsigned int kDefaultChannels = 2;
+    static constexpr unsigned int kDefaultSamplingRate = 48000;
+    static constexpr unsigned int kDefaultPeriodSize = 1024;
+    static constexpr unsigned int kDefaultPeriodCount = 3;
+    static constexpr unsigned int kDefaultPeriodTimeInMs =
+            kDefaultPeriodSize * 1000 / kDefaultSamplingRate;
+
+    static constexpr pcm_config kInConfig = {
+        .channels = kDefaultChannels,
+        .rate = kDefaultSamplingRate,
+        .period_size = kDefaultPeriodSize,
+        .period_count = kDefaultPeriodCount,
+        .format = PCM_FORMAT_S16_LE,
+        .start_threshold = 0,
+        .stop_threshold = 0,
+        .silence_threshold = 0,
+        .silence_size = 0,
+    };
+    pcm *pcm_in = pcm_open(kLoopbackCard, kLoopbackCaptureDevice, PCM_IN, &kInConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_in));
+
+    static constexpr pcm_config kOutConfig = {
+        .channels = kDefaultChannels,
+        .rate = kDefaultSamplingRate,
+        .period_size = kDefaultPeriodSize,
+        .period_count = kDefaultPeriodCount,
+        .format = PCM_FORMAT_S16_LE,
+        .start_threshold = kDefaultPeriodSize,
+        .stop_threshold = kDefaultPeriodSize * kDefaultPeriodCount,
+        .silence_threshold = 0,
+        .silence_size = 0,
+    };
+    pcm *pcm_out = pcm_open(kLoopbackCard, kLoopbackPlaybackDevice, PCM_OUT, &kOutConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_out));
+
+    ASSERT_EQ(pcm_link(pcm_in, pcm_out), 0);
+
+    bool stopping = false;
+    ASSERT_EQ(pcm_get_subdevice(pcm_in), pcm_get_subdevice(pcm_out));
+
+    std::thread capture([pcm_in, &stopping] {
+        size_t buffer_size = pcm_frames_to_bytes(pcm_in, kDefaultPeriodSize);
+        unsigned int frames = pcm_bytes_to_frames(pcm_in, buffer_size);
+        auto buffer = std::make_unique<unsigned char[]>(buffer_size);
+        int32_t counter = 0;
+        while (!stopping) {
+            int res = pcm_readi(pcm_in, buffer.get(), frames);
+            if (res == -1) {
+                std::cout << pcm_get_error(pcm_in) << std::endl;
+                std::this_thread::sleep_for(std::chrono::milliseconds(kDefaultPeriodTimeInMs));
+                continue;
+            }
+            EXPECT_EQ(pcm_readi(pcm_in, buffer.get(), frames), frames) << counter;
+            // Test the energy of the buffer after the sine tone samples fill in the buffer.
+            // Therefore, check the buffer 5 times later.
+            if (counter >= 5) {
+                double e = Energy(buffer.get(), frames * kInConfig.channels);
+                EXPECT_GT(e, 0.0) << counter;
+            }
+            counter++;
+        }
+    });
+
+    std::thread playback([pcm_out, &stopping] {
+        SineToneGenerator<2, 48000, 1000, 0, PCM_FORMAT_S16_LE> generator;
+        size_t buffer_size = pcm_frames_to_bytes(pcm_out, kDefaultPeriodSize);
+        unsigned int frames = pcm_bytes_to_frames(pcm_out, buffer_size);
+        auto buffer = std::make_unique<unsigned char[]>(buffer_size);
+        int32_t counter = 0;
+        while (!stopping) {
+            generator.Read(buffer.get(), buffer_size);
+            EXPECT_EQ(pcm_writei(pcm_out, buffer.get(), frames), frames) << counter;
+            counter++;
+        }
+    });
+
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+    stopping = true;
+    capture.join();
+    playback.join();
+
+    ASSERT_EQ(pcm_unlink(pcm_in), 0);
+    pcm_close(pcm_in);
+    pcm_close(pcm_out);
+}
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/tests/src/pcm_out_test.cc b/tests/src/pcm_out_test.cc
new file mode 100644
index 0000000..cbc6983
--- /dev/null
+++ b/tests/src/pcm_out_test.cc
@@ -0,0 +1,216 @@
+/* pcm_out_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+#include "pcm_test_device.h"
+
+#include <chrono>
+#include <cstring>
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+class PcmOutTest : public ::testing::Test {
+  protected:
+    PcmOutTest() : pcm_object(nullptr) {}
+    virtual ~PcmOutTest() = default;
+
+    virtual void SetUp() override {
+        pcm_object = pcm_open(kLoopbackCard, kLoopbackPlaybackDevice, PCM_OUT, &kDefaultConfig);
+        ASSERT_NE(pcm_object, nullptr);
+        ASSERT_TRUE(pcm_is_ready(pcm_object));
+    }
+
+    virtual void TearDown() override {
+        ASSERT_EQ(pcm_close(pcm_object), 0);
+    }
+
+    static constexpr unsigned int kDefaultChannels = 2;
+    static constexpr unsigned int kDefaultSamplingRate = 48000;
+    static constexpr unsigned int kDefaultPeriodSize = 1024;
+    static constexpr unsigned int kDefaultPeriodCount = 3;
+    static constexpr pcm_config kDefaultConfig = {
+        .channels = kDefaultChannels,
+        .rate = kDefaultSamplingRate,
+        .period_size = kDefaultPeriodSize,
+        .period_count = kDefaultPeriodCount,
+        .format = PCM_FORMAT_S16_LE,
+        .start_threshold = kDefaultPeriodSize,
+        .stop_threshold = kDefaultPeriodSize * kDefaultPeriodCount,
+        .silence_threshold = 0,
+        .silence_size = 0,
+    };
+
+    pcm* pcm_object;
+};
+
+TEST_F(PcmOutTest, GetFileDescriptor) {
+    ASSERT_GT(pcm_get_file_descriptor(pcm_object), 0);
+}
+
+TEST_F(PcmOutTest, GetChannels) {
+    ASSERT_EQ(pcm_get_channels(pcm_object), kDefaultConfig.channels);
+}
+
+TEST_F(PcmOutTest, GetSamplingRate) {
+    ASSERT_EQ(pcm_get_rate(pcm_object), kDefaultConfig.rate);
+}
+
+TEST_F(PcmOutTest, GetFormat) {
+    ASSERT_EQ(pcm_get_format(pcm_object), kDefaultConfig.format);
+
+}
+
+TEST_F(PcmOutTest, GetErrorMessage) {
+    ASSERT_STREQ(pcm_get_error(pcm_object), "");
+}
+
+TEST_F(PcmOutTest, GetConfig) {
+    ASSERT_EQ(pcm_get_config(nullptr), nullptr);
+    ASSERT_EQ(std::memcmp(pcm_get_config(pcm_object), &kDefaultConfig, sizeof(pcm_config)), 0);
+}
+
+TEST_F(PcmOutTest, SetConfig) {
+    ASSERT_EQ(pcm_set_config(nullptr, nullptr), -EFAULT);
+    ASSERT_EQ(pcm_set_config(pcm_object, nullptr), 0);
+}
+
+TEST_F(PcmOutTest, GetBufferSize) {
+    unsigned int buffer_size = pcm_get_buffer_size(pcm_object);
+    ASSERT_EQ(buffer_size, kDefaultConfig.period_count * kDefaultConfig.period_size);
+}
+
+TEST_F(PcmOutTest, FramesBytesConvert) {
+    unsigned int bytes = pcm_frames_to_bytes(pcm_object, 1);
+    ASSERT_EQ(bytes, pcm_format_to_bits(kDefaultConfig.format) / 8 * kDefaultConfig.channels);
+
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, bytes + 1);
+    ASSERT_EQ(frames, 1);
+}
+
+TEST_F(PcmOutTest, GetAvailableAndTimestamp) {
+    unsigned int available = 0;
+    timespec time = { 0 };
+
+    ASSERT_LT(pcm_get_htimestamp(nullptr, nullptr, nullptr), 0);
+
+    ASSERT_EQ(pcm_get_htimestamp(pcm_object, &available, &time), 0);
+    ASSERT_NE(available, 0);
+    // ASSERT_NE(time.tv_nsec | time.tv_sec, 0);
+}
+
+TEST_F(PcmOutTest, GetSubdevice) {
+    ASSERT_EQ(pcm_get_subdevice(pcm_object), 0);
+}
+
+TEST_F(PcmOutTest, Readi) {
+    size_t buffer_size = pcm_frames_to_bytes(pcm_object, kDefaultConfig.period_size);
+    auto buffer = std::make_unique<char[]>(buffer_size);
+
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, buffer_size);
+    ASSERT_EQ(pcm_readi(pcm_object, buffer.get(), frames), -EINVAL);
+}
+
+TEST_F(PcmOutTest, Writei) {
+    constexpr uint32_t write_count = 20;
+
+    size_t buffer_size = pcm_frames_to_bytes(pcm_object, kDefaultConfig.period_size);
+    auto buffer = std::make_unique<char[]>(buffer_size);
+    for (uint32_t i = 0; i < buffer_size; ++i) {
+        buffer[i] = static_cast<char>(i);
+    }
+
+    int written_frames = 0;
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, buffer_size);
+    auto start = std::chrono::steady_clock::now();
+    for (uint32_t i = 0; i < write_count; ++i) {
+        written_frames = pcm_writei(pcm_object, buffer.get(), frames);
+        ASSERT_EQ(written_frames, frames);
+    }
+
+    std::chrono::duration<double> difference = std::chrono::steady_clock::now() - start;
+    std::chrono::milliseconds expected_elapsed_time_ms(frames *
+            (write_count - kDefaultConfig.period_count) / (kDefaultConfig.rate / 1000));
+
+    std::cout << difference.count() << std::endl;
+    std::cout << expected_elapsed_time_ms.count() << std::endl;
+
+    ASSERT_NEAR(difference.count() * 1000, expected_elapsed_time_ms.count(), 100);
+}
+
+class PcmOutMmapTest : public PcmOutTest {
+  protected:
+    PcmOutMmapTest() = default;
+    ~PcmOutMmapTest() = default;
+
+    virtual void SetUp() override {
+        pcm_object = pcm_open(kLoopbackCard, kLoopbackPlaybackDevice, PCM_OUT | PCM_MMAP,
+                &kDefaultConfig);
+        ASSERT_NE(pcm_object, nullptr);
+        ASSERT_TRUE(pcm_is_ready(pcm_object));
+    }
+
+    virtual void TearDown() override {
+        ASSERT_EQ(pcm_close(pcm_object), 0);
+    }
+};
+
+TEST_F(PcmOutMmapTest, Write) {
+    constexpr uint32_t write_count = 20;
+
+    size_t buffer_size = pcm_frames_to_bytes(pcm_object, kDefaultConfig.period_size);
+    auto buffer = std::make_unique<char[]>(buffer_size);
+    for (uint32_t i = 0; i < buffer_size; ++i) {
+        buffer[i] = static_cast<char>(i);
+    }
+
+    int written_frames = 0;
+    unsigned int frames = pcm_bytes_to_frames(pcm_object, buffer_size);
+    pcm_start(pcm_object);
+    auto start = std::chrono::steady_clock::now();
+    for (uint32_t i = 0; i < write_count; ++i) {
+        written_frames = pcm_mmap_write(pcm_object, buffer.get(), buffer_size);
+        ASSERT_EQ(written_frames, frames);
+    }
+    pcm_stop(pcm_object);
+
+    std::chrono::duration<double> difference = std::chrono::steady_clock::now() - start;
+    std::chrono::milliseconds expected_elapsed_time_ms(frames *
+            (write_count - kDefaultConfig.period_count) / (kDefaultConfig.rate / 1000));
+
+    std::cout << difference.count() << std::endl;
+    std::cout << expected_elapsed_time_ms.count() << std::endl;
+
+    ASSERT_NEAR(difference.count() * 1000, expected_elapsed_time_ms.count(), 100);
+}
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/tests/src/pcm_params_test.cc b/tests/src/pcm_params_test.cc
new file mode 100644
index 0000000..c8151e1
--- /dev/null
+++ b/tests/src/pcm_params_test.cc
@@ -0,0 +1,222 @@
+/* pcm_params_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include "pcm_test_device.h"
+
+#include <cstring>
+#include <iostream>
+#include <memory>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+static inline unsigned int OrAllBits(const pcm_mask *mask) {
+    static constexpr size_t kTotalMaskBytes = 32;
+    unsigned int res = 0;
+    for (uint32_t i = 0; i < kTotalMaskBytes / sizeof(pcm_mask::bits[0]); ++i) {
+        res |= mask->bits[i];
+    }
+    return res;
+}
+
+TEST(PcmParamsTest, GetAndFreeParams) {
+    pcm_params *params = nullptr;
+
+    // test to get nonexistent card and device.
+    params = pcm_params_get(1000, 1000, PCM_IN);
+    ASSERT_EQ(params, nullptr);
+
+    // test free null params.
+    pcm_params_free(params);
+
+    // assume that card 0, device 0 is always available.
+    params = pcm_params_get(0, 0, PCM_OUT);
+    ASSERT_NE(params, nullptr);
+    pcm_params_free(params);
+}
+
+TEST(PcmParamsTest, GetParamsBitMask) {
+    // test to get mask with null params
+    ASSERT_EQ(pcm_params_get_mask(nullptr, PCM_PARAM_ACCESS), nullptr);
+
+    // assume that card 0, device 0 is always available.
+    pcm_params *params = pcm_params_get(0, 0, PCM_OUT);
+    ASSERT_NE(params, nullptr);
+
+    // test to get param which is not described in bit mask format
+    ASSERT_EQ(pcm_params_get_mask(params, PCM_PARAM_SAMPLE_BITS), nullptr);
+
+    // test to get mask out of pcm_param enum
+    ASSERT_EQ(pcm_params_get_mask(params, static_cast<pcm_param>(100)), nullptr);
+
+    const pcm_mask *mask = pcm_params_get_mask(params, PCM_PARAM_ACCESS);
+    ASSERT_NE(mask, nullptr);
+
+    pcm_params_free(params);
+}
+
+TEST(PcmParamsTest, GetParamsInterval) {
+    // test to get interval with null params
+    ASSERT_EQ(pcm_params_get_min(nullptr, PCM_PARAM_SAMPLE_BITS), 0);
+    ASSERT_EQ(pcm_params_get_max(nullptr, PCM_PARAM_SAMPLE_BITS), 0);
+
+    // assume that card 0, device 0 is always available.
+    pcm_params *params = pcm_params_get(0, 0, PCM_OUT);
+    ASSERT_NE(params, nullptr);
+
+    // test to get param which is not described in interval format
+    ASSERT_EQ(pcm_params_get_min(params, PCM_PARAM_ACCESS), 0);
+    ASSERT_EQ(pcm_params_get_max(params, PCM_PARAM_ACCESS), 0);
+
+    // test to get interval out of pcm_param enum
+    ASSERT_EQ(pcm_params_get_min(params, static_cast<pcm_param>(100)), 0);
+    ASSERT_EQ(pcm_params_get_max(params, static_cast<pcm_param>(100)), 0);
+
+    pcm_params_free(params);
+}
+
+TEST(PcmParamsTest, ParamsToString) {
+    // assume that card 0, device 0 is always available.
+    pcm_params *params = pcm_params_get(0, 0, PCM_OUT);
+    ASSERT_NE(params, nullptr);
+
+    char long_string[1024] = { 0 };
+    int count = pcm_params_to_string(params, long_string, sizeof(long_string));
+    ASSERT_LE(static_cast<size_t>(count), sizeof(long_string));
+    ASSERT_GT(static_cast<size_t>(count), 0);
+
+    char short_string[1] = { 0 };
+    count = pcm_params_to_string(params, short_string, sizeof(short_string));
+    ASSERT_GT(static_cast<size_t>(count), sizeof(short_string));
+
+    int proper_string_len = count;
+    int proper_string_size = proper_string_len + 1;
+    auto proper_string = std::make_unique<char[]>(proper_string_size);
+    count = pcm_params_to_string(params, proper_string.get(), proper_string_size);
+    ASSERT_GT(static_cast<size_t>(count), 0);
+    ASSERT_EQ(static_cast<size_t>(count), proper_string_len);
+    ASSERT_EQ(std::strlen(proper_string.get()), proper_string_len);
+    pcm_params_free(params);
+}
+
+TEST(PcmParamsTest, GetPlaybackDeviceParams) {
+    pcm_params *params = pcm_params_get(kLoopbackCard, kLoopbackPlaybackDevice, PCM_OUT);
+    ASSERT_NE(params, nullptr);
+
+    const pcm_mask *access_mask = pcm_params_get_mask(params, PCM_PARAM_ACCESS);
+    ASSERT_NE(access_mask, nullptr);
+    ASSERT_NE(OrAllBits(access_mask), 0);
+
+    const pcm_mask *format_mask = pcm_params_get_mask(params, PCM_PARAM_FORMAT);
+    ASSERT_NE(format_mask, nullptr);
+    ASSERT_NE(OrAllBits(format_mask), 0);
+
+    const pcm_mask *subformat_mask = pcm_params_get_mask(params, PCM_PARAM_SUBFORMAT);
+    ASSERT_NE(subformat_mask, nullptr);
+    ASSERT_NE(OrAllBits(subformat_mask), 0);
+
+    unsigned int sample_bits_min = pcm_params_get_min(params, PCM_PARAM_SAMPLE_BITS);
+    unsigned int sample_bits_max = pcm_params_get_max(params, PCM_PARAM_SAMPLE_BITS);
+    std::cout << "sample_bits: " << sample_bits_min << " - " << sample_bits_max << std::endl;
+    ASSERT_GT(sample_bits_min, 0);
+    ASSERT_GT(sample_bits_max, 0);
+
+    unsigned int frame_bits_min = pcm_params_get_min(params, PCM_PARAM_FRAME_BITS);
+    unsigned int frame_bits_max = pcm_params_get_max(params, PCM_PARAM_FRAME_BITS);
+    std::cout << "frame_bits: " << frame_bits_min << " - " << frame_bits_max << std::endl;
+    ASSERT_GT(frame_bits_min, 0);
+    ASSERT_GT(frame_bits_max, 0);
+
+    unsigned int channels_min = pcm_params_get_min(params, PCM_PARAM_CHANNELS);
+    unsigned int channels_max = pcm_params_get_max(params, PCM_PARAM_CHANNELS);
+    std::cout << "channels: " << channels_min << " - " << channels_max << std::endl;
+    ASSERT_GT(channels_min, 0);
+    ASSERT_GT(channels_max, 0);
+
+    unsigned int sampling_rate_min = pcm_params_get_min(params, PCM_PARAM_RATE);
+    unsigned int sampling_rate_max = pcm_params_get_max(params, PCM_PARAM_RATE);
+    std::cout << "sampling_rate: " << sampling_rate_min << " - " << sampling_rate_max << std::endl;
+    ASSERT_GT(sampling_rate_min, 0);
+    ASSERT_GT(sampling_rate_max, 0);
+
+    unsigned int period_time_min = pcm_params_get_min(params, PCM_PARAM_PERIOD_TIME);
+    unsigned int period_time_max = pcm_params_get_max(params, PCM_PARAM_PERIOD_TIME);
+    std::cout << "period_time: " << period_time_min << " - " << period_time_max << std::endl;
+    ASSERT_GT(period_time_min, 0);
+    ASSERT_GT(period_time_max, 0);
+
+    unsigned int period_size_min = pcm_params_get_min(params, PCM_PARAM_PERIOD_SIZE);
+    unsigned int period_size_max = pcm_params_get_max(params, PCM_PARAM_PERIOD_SIZE);
+    std::cout << "period_size: " << period_size_min << " - " << period_size_max << std::endl;
+    ASSERT_GT(period_size_min, 0);
+    ASSERT_GT(period_size_max, 0);
+
+    unsigned int period_bytes_min = pcm_params_get_min(params, PCM_PARAM_PERIOD_BYTES);
+    unsigned int period_bytes_max = pcm_params_get_max(params, PCM_PARAM_PERIOD_BYTES);
+    std::cout << "period_bytes: " << period_bytes_min << " - " << period_bytes_max << std::endl;
+    ASSERT_GT(period_bytes_min, 0);
+    ASSERT_GT(period_bytes_max, 0);
+
+    unsigned int period_count_min = pcm_params_get_min(params, PCM_PARAM_PERIODS);
+    unsigned int period_count_max = pcm_params_get_max(params, PCM_PARAM_PERIODS);
+    std::cout << "period_count: " << period_count_min << " - " << period_count_max << std::endl;
+    ASSERT_GT(period_count_min, 0);
+    ASSERT_GT(period_count_max, 0);
+
+    unsigned int buffer_time_min = pcm_params_get_min(params, PCM_PARAM_BUFFER_TIME);
+    unsigned int buffer_time_max = pcm_params_get_max(params, PCM_PARAM_BUFFER_TIME);
+    std::cout << "buffer_time: " << buffer_time_min << " - " << buffer_time_max << std::endl;
+    ASSERT_GT(buffer_time_min, 0);
+    ASSERT_GT(buffer_time_max, 0);
+
+    unsigned int buffer_size_min = pcm_params_get_min(params, PCM_PARAM_BUFFER_SIZE);
+    unsigned int buffer_size_max = pcm_params_get_max(params, PCM_PARAM_BUFFER_SIZE);
+    std::cout << "buffer_size: " << buffer_size_min << " - " << buffer_size_max << std::endl;
+    ASSERT_GT(buffer_size_min, 0);
+    ASSERT_GT(buffer_size_max, 0);
+
+    unsigned int buffer_bytes_min = pcm_params_get_min(params, PCM_PARAM_BUFFER_BYTES);
+    unsigned int buffer_bytes_max = pcm_params_get_max(params, PCM_PARAM_BUFFER_BYTES);
+    std::cout << "buffer_bytes: " << buffer_bytes_min << " - " << buffer_bytes_max << std::endl;
+    ASSERT_GT(buffer_bytes_min, 0);
+    ASSERT_GT(buffer_bytes_max, 0);
+
+    unsigned int tick_in_us_min = pcm_params_get_min(params, PCM_PARAM_TICK_TIME);
+    unsigned int tick_in_us_max = pcm_params_get_max(params, PCM_PARAM_TICK_TIME);
+    ASSERT_GT(tick_in_us_max, 0);
+    std::cout << "tick_in_us: " << tick_in_us_min << " - " << tick_in_us_max << std::endl;
+
+    pcm_params_free(params);
+}
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/tests/src/pcm_test.cc b/tests/src/pcm_test.cc
new file mode 100644
index 0000000..2668350
--- /dev/null
+++ b/tests/src/pcm_test.cc
@@ -0,0 +1,103 @@
+/* pcm_out_test.c
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <string>
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+#include "tinyalsa/pcm.h"
+
+namespace tinyalsa {
+namespace testing {
+
+TEST(PcmTest, FormatToBits) {
+    // FIXME: Should we return 16 bits for INVALID?
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_INVALID), 16);
+
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S16_LE), 16);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S32_LE), 32);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S8), 8);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S24_LE), 32);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S24_3LE), 24);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S16_BE), 16);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S24_BE), 32);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S24_3BE), 24);
+    ASSERT_EQ(pcm_format_to_bits(PCM_FORMAT_S32_BE), 32);
+}
+
+TEST(PcmTest, OpenAndCloseOutPcm) {
+    static constexpr unsigned int kDefaultChannels = 2;
+    static constexpr unsigned int kDefaultSamplingRate = 48000;
+    static constexpr unsigned int kDefaultPeriodSize = 1024;
+    static constexpr unsigned int kDefaultPeriodCount = 3;
+    static constexpr pcm_config kDefaultConfig = {
+        .channels = kDefaultChannels,
+        .rate = kDefaultSamplingRate,
+        .period_size = kDefaultPeriodSize,
+        .period_count = kDefaultPeriodCount,
+        .format = PCM_FORMAT_S16_LE,
+        .start_threshold = kDefaultPeriodSize,
+        .stop_threshold = kDefaultPeriodSize * kDefaultPeriodCount,
+        .silence_threshold = 0,
+        .silence_size = 0,
+    };
+
+    pcm *pcm_object = pcm_open(1000, 1000, PCM_OUT, &kDefaultConfig);
+    ASSERT_FALSE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    // assume card 0, device 0 is always available
+    pcm_object = pcm_open(0, 0, PCM_OUT, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    pcm_object = pcm_open(0, 0, PCM_OUT | PCM_MMAP, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    pcm_object = pcm_open(0, 0, PCM_OUT | PCM_MMAP | PCM_NOIRQ, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    pcm_object = pcm_open(0, 0, PCM_OUT | PCM_NONBLOCK, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    pcm_object = pcm_open(0, 0, PCM_OUT | PCM_MONOTONIC, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+
+    std::string name = "hw:0,0";
+    pcm_object = pcm_open_by_name(name.c_str(), PCM_OUT, &kDefaultConfig);
+    ASSERT_TRUE(pcm_is_ready(pcm_object));
+    ASSERT_EQ(pcm_close(pcm_object), 0);
+}
+
+} // namespace testing
+} // namespace tinyalsa
diff --git a/utils/Makefile b/utils/Makefile
new file mode 100644
index 0000000..f733c39
--- /dev/null
+++ b/utils/Makefile
@@ -0,0 +1,59 @@
+DESTDIR ?=
+PREFIX ?= /usr/local
+BINDIR ?= $(PREFIX)/bin
+MANDIR ?= $(PREFIX)/man
+
+CROSS_COMPILE ?=
+CC = $(CROSS_COMPILE)gcc
+
+CFLAGS += -Wall -Wextra -Werror -Wfatal-errors
+CFLAGS += -I ../include
+CFLAGS += -fPIC
+CFLAGS += -O2
+
+LDFLAGS += -L ../src
+LDFLAGS += -pie
+
+VPATH = ../src:../include/tinyalsa
+
+.PHONY: all
+all: -ltinyalsa tinyplay tinycap tinymix tinypcminfo
+
+tinyplay tinycap tinypcminfo tinymix: LDLIBS+=-ldl
+
+tinyplay: tinyplay.o libtinyalsa.a
+
+tinyplay.o: tinyplay.c pcm.h mixer.h asoundlib.h optparse.h
+
+tinycap: tinycap.o libtinyalsa.a
+
+tinycap.o: tinycap.c pcm.h mixer.h asoundlib.h optparse.h
+
+tinymix: tinymix.o libtinyalsa.a
+
+tinymix.o: tinymix.c pcm.h mixer.h asoundlib.h optparse.h
+
+tinypcminfo: tinypcminfo.o libtinyalsa.a
+
+tinypcminfo.o: tinypcminfo.c pcm.h mixer.h asoundlib.h optparse.h
+
+.PHONY: clean
+clean:
+	$(RM) tinyplay tinyplay.o
+	$(RM) tinycap tinycap.o
+	$(RM) tinymix tinymix.o
+	$(RM) tinypcminfo tinypcminfo.o
+
+.PHONY: install
+install: tinyplay tinycap tinymix tinypcminfo
+	install -d $(DESTDIR)$(BINDIR)
+	install tinyplay $(DESTDIR)$(BINDIR)/
+	install tinycap $(DESTDIR)$(BINDIR)/
+	install tinymix $(DESTDIR)$(BINDIR)/
+	install tinypcminfo $(DESTDIR)$(BINDIR)/
+	install -d $(DESTDIR)$(MANDIR)/man1
+	install tinyplay.1 $(DESTDIR)$(MANDIR)/man1/
+	install tinycap.1 $(DESTDIR)$(MANDIR)/man1/
+	install tinymix.1 $(DESTDIR)$(MANDIR)/man1/
+	install tinypcminfo.1 $(DESTDIR)$(MANDIR)/man1/
+
diff --git a/utils/meson.build b/utils/meson.build
new file mode 100644
index 0000000..934402b
--- /dev/null
+++ b/utils/meson.build
@@ -0,0 +1,9 @@
+utils = ['tinyplay', 'tinycap', 'tinymix', 'tinypcminfo']
+
+foreach util : utils
+  executable(util, '@0@.c'.format(util),
+    include_directories: tinyalsa_includes,
+    link_with: tinyalsa,
+    install: true)
+  install_man('@0@.1'.format(util))
+endforeach
diff --git a/utils/optparse.h b/utils/optparse.h
new file mode 100644
index 0000000..3a577a7
--- /dev/null
+++ b/utils/optparse.h
@@ -0,0 +1,403 @@
+/* Optparse --- portable, reentrant, embeddable, getopt-like option parser
+ *
+ * This is free and unencumbered software released into the public domain.
+ *
+ * To get the implementation, define OPTPARSE_IMPLEMENTATION.
+ * Optionally define OPTPARSE_API to control the API's visibility
+ * and/or linkage (static, __attribute__, __declspec).
+ *
+ * The POSIX getopt() option parser has three fatal flaws. These flaws
+ * are solved by Optparse.
+ *
+ * 1) Parser state is stored entirely in global variables, some of
+ * which are static and inaccessible. This means only one thread can
+ * use getopt(). It also means it's not possible to recursively parse
+ * nested sub-arguments while in the middle of argument parsing.
+ * Optparse fixes this by storing all state on a local struct.
+ *
+ * 2) The POSIX standard provides no way to properly reset the parser.
+ * This means for portable code that getopt() is only good for one
+ * run, over one argv with one option string. It also means subcommand
+ * options cannot be processed with getopt(). Most implementations
+ * provide a method to reset the parser, but it's not portable.
+ * Optparse provides an optparse_arg() function for stepping over
+ * subcommands and continuing parsing of options with another option
+ * string. The Optparse struct itself can be passed around to
+ * subcommand handlers for additional subcommand option parsing. A
+ * full reset can be achieved by with an additional optparse_init().
+ *
+ * 3) Error messages are printed to stderr. This can be disabled with
+ * opterr, but the messages themselves are still inaccessible.
+ * Optparse solves this by writing an error message in its errmsg
+ * field. The downside to Optparse is that this error message will
+ * always be in English rather than the current locale.
+ *
+ * Optparse should be familiar with anyone accustomed to getopt(), and
+ * it could be a nearly drop-in replacement. The option string is the
+ * same and the fields have the same names as the getopt() global
+ * variables (optarg, optind, optopt).
+ *
+ * Optparse also supports GNU-style long options with optparse_long().
+ * The interface is slightly different and simpler than getopt_long().
+ *
+ * By default, argv is permuted as it is parsed, moving non-option
+ * arguments to the end. This can be disabled by setting the `permute`
+ * field to 0 after initialization.
+ */
+#ifndef OPTPARSE_H
+#define OPTPARSE_H
+
+#ifndef OPTPARSE_API
+#  define OPTPARSE_API
+#endif
+
+struct optparse {
+    char **argv;
+    int permute;
+    int optind;
+    int optopt;
+    char *optarg;
+    char errmsg[64];
+    int subopt;
+};
+
+enum optparse_argtype {
+    OPTPARSE_NONE,
+    OPTPARSE_REQUIRED,
+    OPTPARSE_OPTIONAL
+};
+
+struct optparse_long {
+    const char *longname;
+    int shortname;
+    enum optparse_argtype argtype;
+};
+
+/**
+ * Initializes the parser state.
+ */
+OPTPARSE_API
+void optparse_init(struct optparse *options, char **argv);
+
+/**
+ * Read the next option in the argv array.
+ * @param optstring a getopt()-formatted option string.
+ * @return the next option character, -1 for done, or '?' for error
+ *
+ * Just like getopt(), a character followed by no colons means no
+ * argument. One colon means the option has a required argument. Two
+ * colons means the option takes an optional argument.
+ */
+OPTPARSE_API
+int optparse(struct optparse *options, const char *optstring);
+
+/**
+ * Handles GNU-style long options in addition to getopt() options.
+ * This works a lot like GNU's getopt_long(). The last option in
+ * longopts must be all zeros, marking the end of the array. The
+ * longindex argument may be NULL.
+ */
+OPTPARSE_API
+int optparse_long(struct optparse *options,
+                  const struct optparse_long *longopts,
+                  int *longindex);
+
+/**
+ * Used for stepping over non-option arguments.
+ * @return the next non-option argument, or NULL for no more arguments
+ *
+ * Argument parsing can continue with optparse() after using this
+ * function. That would be used to parse the options for the
+ * subcommand returned by optparse_arg(). This function allows you to
+ * ignore the value of optind.
+ */
+OPTPARSE_API
+char *optparse_arg(struct optparse *options);
+
+/* Implementation */
+#ifdef OPTPARSE_IMPLEMENTATION
+
+#define OPTPARSE_MSG_INVALID "invalid option"
+#define OPTPARSE_MSG_MISSING "option requires an argument"
+#define OPTPARSE_MSG_TOOMANY "option takes no arguments"
+
+static int
+optparse_error(struct optparse *options, const char *msg, const char *data)
+{
+    unsigned p = 0;
+    const char *sep = " -- '";
+    while (*msg)
+        options->errmsg[p++] = *msg++;
+    while (*sep)
+        options->errmsg[p++] = *sep++;
+    while (p < sizeof(options->errmsg) - 2 && *data)
+        options->errmsg[p++] = *data++;
+    options->errmsg[p++] = '\'';
+    options->errmsg[p++] = '\0';
+    return '?';
+}
+
+OPTPARSE_API
+void
+optparse_init(struct optparse *options, char **argv)
+{
+    options->argv = argv;
+    options->permute = 1;
+    options->optind = 1;
+    options->subopt = 0;
+    options->optarg = 0;
+    options->errmsg[0] = '\0';
+}
+
+static int
+optparse_is_dashdash(const char *arg)
+{
+    return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] == '\0';
+}
+
+static int
+optparse_is_shortopt(const char *arg)
+{
+    return arg != 0 && arg[0] == '-' && arg[1] != '-' && arg[1] != '\0';
+}
+
+static int
+optparse_is_longopt(const char *arg)
+{
+    return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] != '\0';
+}
+
+static void
+optparse_permute(struct optparse *options, int index)
+{
+    char *nonoption = options->argv[index];
+    int i;
+    for (i = index; i < options->optind - 1; i++)
+        options->argv[i] = options->argv[i + 1];
+    options->argv[options->optind - 1] = nonoption;
+}
+
+static int
+optparse_argtype(const char *optstring, char c)
+{
+    int count = OPTPARSE_NONE;
+    if (c == ':')
+        return -1;
+    for (; *optstring && c != *optstring; optstring++);
+    if (!*optstring)
+        return -1;
+    if (optstring[1] == ':')
+        count += optstring[2] == ':' ? 2 : 1;
+    return count;
+}
+
+OPTPARSE_API
+int
+optparse(struct optparse *options, const char *optstring)
+{
+    int type;
+    char *next;
+    char *option = options->argv[options->optind];
+    options->errmsg[0] = '\0';
+    options->optopt = 0;
+    options->optarg = 0;
+    if (option == 0) {
+        return -1;
+    } else if (optparse_is_dashdash(option)) {
+        options->optind++; /* consume "--" */
+        return -1;
+    } else if (!optparse_is_shortopt(option)) {
+        if (options->permute) {
+            int index = options->optind++;
+            int r = optparse(options, optstring);
+            optparse_permute(options, index);
+            options->optind--;
+            return r;
+        } else {
+            return -1;
+        }
+    }
+    option += options->subopt + 1;
+    options->optopt = option[0];
+    type = optparse_argtype(optstring, option[0]);
+    next = options->argv[options->optind + 1];
+    switch (type) {
+    case -1: {
+        char str[2] = {0, 0};
+        str[0] = option[0];
+        options->optind++;
+        return optparse_error(options, OPTPARSE_MSG_INVALID, str);
+    }
+    case OPTPARSE_NONE:
+        if (option[1]) {
+            options->subopt++;
+        } else {
+            options->subopt = 0;
+            options->optind++;
+        }
+        return option[0];
+    case OPTPARSE_REQUIRED:
+        options->subopt = 0;
+        options->optind++;
+        if (option[1]) {
+            options->optarg = option + 1;
+        } else if (next != 0) {
+            options->optarg = next;
+            options->optind++;
+        } else {
+            char str[2] = {0, 0};
+            str[0] = option[0];
+            options->optarg = 0;
+            return optparse_error(options, OPTPARSE_MSG_MISSING, str);
+        }
+        return option[0];
+    case OPTPARSE_OPTIONAL:
+        options->subopt = 0;
+        options->optind++;
+        if (option[1])
+            options->optarg = option + 1;
+        else
+            options->optarg = 0;
+        return option[0];
+    }
+    return 0;
+}
+
+OPTPARSE_API
+char *
+optparse_arg(struct optparse *options)
+{
+    char *option = options->argv[options->optind];
+    options->subopt = 0;
+    if (option != 0)
+        options->optind++;
+    return option;
+}
+
+static int
+optparse_longopts_end(const struct optparse_long *longopts, int i)
+{
+    return !longopts[i].longname && !longopts[i].shortname;
+}
+
+static void
+optparse_from_long(const struct optparse_long *longopts, char *optstring)
+{
+    char *p = optstring;
+    int i;
+    for (i = 0; !optparse_longopts_end(longopts, i); i++) {
+        if (longopts[i].shortname) {
+            int a;
+            *p++ = longopts[i].shortname;
+            for (a = 0; a < (int)longopts[i].argtype; a++)
+                *p++ = ':';
+        }
+    }
+    *p = '\0';
+}
+
+/* Unlike strcmp(), handles options containing "=". */
+static int
+optparse_longopts_match(const char *longname, const char *option)
+{
+    const char *a = option, *n = longname;
+    if (longname == 0)
+        return 0;
+    for (; *a && *n && *a != '='; a++, n++)
+        if (*a != *n)
+            return 0;
+    return *n == '\0' && (*a == '\0' || *a == '=');
+}
+
+/* Return the part after "=", or NULL. */
+static char *
+optparse_longopts_arg(char *option)
+{
+    for (; *option && *option != '='; option++);
+    if (*option == '=')
+        return option + 1;
+    else
+        return 0;
+}
+
+static int
+optparse_long_fallback(struct optparse *options,
+                       const struct optparse_long *longopts,
+                       int *longindex)
+{
+    int result;
+    char optstring[96 * 3 + 1]; /* 96 ASCII printable characters */
+    optparse_from_long(longopts, optstring);
+    result = optparse(options, optstring);
+    if (longindex != 0) {
+        *longindex = -1;
+        if (result != -1) {
+            int i;
+            for (i = 0; !optparse_longopts_end(longopts, i); i++)
+                if (longopts[i].shortname == options->optopt)
+                    *longindex = i;
+        }
+    }
+    return result;
+}
+
+OPTPARSE_API
+int
+optparse_long(struct optparse *options,
+              const struct optparse_long *longopts,
+              int *longindex)
+{
+    int i;
+    char *option = options->argv[options->optind];
+    if (option == 0) {
+        return -1;
+    } else if (optparse_is_dashdash(option)) {
+        options->optind++; /* consume "--" */
+        return -1;
+    } else if (optparse_is_shortopt(option)) {
+        return optparse_long_fallback(options, longopts, longindex);
+    } else if (!optparse_is_longopt(option)) {
+        if (options->permute) {
+            int index = options->optind++;
+            int r = optparse_long(options, longopts, longindex);
+            optparse_permute(options, index);
+            options->optind--;
+            return r;
+        } else {
+            return -1;
+        }
+    }
+
+    /* Parse as long option. */
+    options->errmsg[0] = '\0';
+    options->optopt = 0;
+    options->optarg = 0;
+    option += 2; /* skip "--" */
+    options->optind++;
+    for (i = 0; !optparse_longopts_end(longopts, i); i++) {
+        const char *name = longopts[i].longname;
+        if (optparse_longopts_match(name, option)) {
+            char *arg;
+            if (longindex)
+                *longindex = i;
+            options->optopt = longopts[i].shortname;
+            arg = optparse_longopts_arg(option);
+            if (longopts[i].argtype == OPTPARSE_NONE && arg != 0) {
+                return optparse_error(options, OPTPARSE_MSG_TOOMANY, name);
+            } if (arg != 0) {
+                options->optarg = arg;
+            } else if (longopts[i].argtype == OPTPARSE_REQUIRED) {
+                options->optarg = options->argv[options->optind];
+                if (options->optarg == 0)
+                    return optparse_error(options, OPTPARSE_MSG_MISSING, name);
+                else
+                    options->optind++;
+            }
+            return options->optopt;
+        }
+    }
+    return optparse_error(options, OPTPARSE_MSG_INVALID, option);
+}
+
+#endif /* OPTPARSE_IMPLEMENTATION */
+#endif /* OPTPARSE_H */
diff --git a/utils/tinycap.1 b/utils/tinycap.1
new file mode 100644
index 0000000..ad60a2e
--- /dev/null
+++ b/utils/tinycap.1
@@ -0,0 +1,87 @@
+.TH TINYCAP 1 "October 2, 2016" "tinycap" "TinyALSA"
+
+.SH NAME
+tinycap \- captures audio from an audio device
+
+.SH SYNOPSIS
+.B tinycap\fR [ \fIfile\fR ] [ \fIoptions\fR ]
+
+.SH Description
+
+\fBtinycap\fR can record audio from an audio device to a wav file or standard output (as raw samples).
+Options can be used to specify various hardware parameters to open the PCM with.
+
+.SH OPTIONS
+
+.TP
+\fB\-D\fR \fIcard\fR
+Card number of the PCM.
+The default is 0.
+
+.TP
+\fB\-d\fR \fIdevice\fR
+Device number of the PCM.
+The default is 0.
+
+.TP
+\fB\-c\fR \fIchannels\fR
+Number of channels the PCM will have.
+The default is 2.
+
+.TP
+\fB\-r\fR \fIrate\fR
+Number of frames per second of the PCM.
+The default is 48000.
+
+.TP
+\fB\-b\fR \fIbits\fR
+Number of bits per sample the PCM will have.
+The default is 32.
+
+.TP
+\fB\-p\fR \fIperiod_size\fR
+Number of frames in a period.
+The default is 1024.
+
+.TP
+\fB\-n\fR \fIperiods\fR
+Number of periods the PCM will have.
+The default is 4.
+
+.TP
+\fB\-t\fR \fIseconds\fR
+Number of seconds to record audio.
+
+.SH SIGNALS
+
+When capturing audio, SIGINT will stop the recording and close the file.
+
+.SH EXAMPLES
+
+.TP
+\fBtinycap output.wav\fR
+Records a file called output.wav until an interrupt signal is caught.
+
+.TP
+\fBtinycap output.wav -D 1 -t 2
+Records a file called output.wav from card 1 for two seconds or until an interrupt signal is caught.
+
+.TP
+\fBtinycap -- -t 3
+Records to standard output for three seconds or until an interrupt signal is caught.
+
+.SH BUGS
+
+Please report bugs to https://github.com/tinyalsa/tinyalsa/issues.
+
+.SH SEE ALSO
+
+.BR tinyplay(1),
+.BR tinymix(1),
+.BR tinypcminfo(1)
+
+.SH AUTHORS
+Simon Wilson
+.P
+For a complete list of authors, visit the project page at https://github.com/tinyalsa/tinyalsa.
+
diff --git a/utils/tinycap.c b/utils/tinycap.c
new file mode 100644
index 0000000..7d4b8a4
--- /dev/null
+++ b/utils/tinycap.c
@@ -0,0 +1,267 @@
+/* tinycap.c
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <tinyalsa/asoundlib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <signal.h>
+#include <string.h>
+#include <limits.h>
+
+#define OPTPARSE_IMPLEMENTATION
+#include "optparse.h"
+
+#define ID_RIFF 0x46464952
+#define ID_WAVE 0x45564157
+#define ID_FMT  0x20746d66
+#define ID_DATA 0x61746164
+
+#define FORMAT_PCM 1
+
+struct wav_header {
+    uint32_t riff_id;
+    uint32_t riff_sz;
+    uint32_t riff_fmt;
+    uint32_t fmt_id;
+    uint32_t fmt_sz;
+    uint16_t audio_format;
+    uint16_t num_channels;
+    uint32_t sample_rate;
+    uint32_t byte_rate;
+    uint16_t block_align;
+    uint16_t bits_per_sample;
+    uint32_t data_id;
+    uint32_t data_sz;
+};
+
+int capturing = 1;
+int prinfo = 1;
+
+unsigned int capture_sample(FILE *file, unsigned int card, unsigned int device,
+                            unsigned int channels, unsigned int rate,
+                            enum pcm_format format, unsigned int period_size,
+                            unsigned int period_count, unsigned int capture_time);
+
+void sigint_handler(int sig)
+{
+    if (sig == SIGINT){
+        capturing = 0;
+    }
+}
+
+int main(int argc, char **argv)
+{
+    FILE *file;
+    struct wav_header header;
+    unsigned int card = 0;
+    unsigned int device = 0;
+    unsigned int channels = 2;
+    unsigned int rate = 48000;
+    unsigned int bits = 16;
+    unsigned int frames;
+    unsigned int period_size = 1024;
+    unsigned int period_count = 4;
+    unsigned int capture_time = UINT_MAX;
+    enum pcm_format format;
+    int no_header = 0, c;
+    struct optparse opts;
+
+    if (argc < 2) {
+        fprintf(stderr, "Usage: %s {file.wav | --} [-D card] [-d device] [-c channels] "
+                "[-r rate] [-b bits] [-p period_size] [-n n_periods] [-t time_in_seconds]\n\n"
+                "Use -- for filename to send raw PCM to stdout\n", argv[0]);
+        return 1;
+    }
+
+    if (strcmp(argv[1],"--") == 0) {
+        file = stdout;
+        prinfo = 0;
+        no_header = 1;
+    } else {
+        file = fopen(argv[1], "wb");
+        if (!file) {
+            fprintf(stderr, "Unable to create file '%s'\n", argv[1]);
+            return 1;
+        }
+    }
+
+    /* parse command line arguments */
+    optparse_init(&opts, argv + 1);
+    while ((c = optparse(&opts, "D:d:c:r:b:p:n:t:")) != -1) {
+        switch (c) {
+        case 'd':
+            device = atoi(opts.optarg);
+            break;
+        case 'c':
+            channels = atoi(opts.optarg);
+            break;
+        case 'r':
+            rate = atoi(opts.optarg);
+            break;
+        case 'b':
+            bits = atoi(opts.optarg);
+            break;
+        case 'D':
+            card = atoi(opts.optarg);
+            break;
+        case 'p':
+            period_size = atoi(opts.optarg);
+            break;
+        case 'n':
+            period_count = atoi(opts.optarg);
+            break;
+        case 't':
+            capture_time = atoi(opts.optarg);
+            break;
+        case '?':
+            fprintf(stderr, "%s\n", opts.errmsg);
+            return EXIT_FAILURE;
+        }
+    }
+
+    header.riff_id = ID_RIFF;
+    header.riff_sz = 0;
+    header.riff_fmt = ID_WAVE;
+    header.fmt_id = ID_FMT;
+    header.fmt_sz = 16;
+    header.audio_format = FORMAT_PCM;
+    header.num_channels = channels;
+    header.sample_rate = rate;
+
+    switch (bits) {
+    case 32:
+        format = PCM_FORMAT_S32_LE;
+        break;
+    case 24:
+        format = PCM_FORMAT_S24_LE;
+        break;
+    case 16:
+        format = PCM_FORMAT_S16_LE;
+        break;
+    default:
+        fprintf(stderr, "%u bits is not supported.\n", bits);
+        fclose(file);
+        return 1;
+    }
+
+    header.bits_per_sample = pcm_format_to_bits(format);
+    header.byte_rate = (header.bits_per_sample / 8) * channels * rate;
+    header.block_align = channels * (header.bits_per_sample / 8);
+    header.data_id = ID_DATA;
+
+    /* leave enough room for header */
+    if (!no_header) {
+        fseek(file, sizeof(struct wav_header), SEEK_SET);
+    }
+
+    /* install signal handler and begin capturing */
+    signal(SIGINT, sigint_handler);
+    frames = capture_sample(file, card, device, header.num_channels,
+                            header.sample_rate, format,
+                            period_size, period_count, capture_time);
+    if (prinfo) {
+        printf("Captured %u frames\n", frames);
+    }
+
+    /* write header now all information is known */
+    if (!no_header) {
+        header.data_sz = frames * header.block_align;
+        header.riff_sz = header.data_sz + sizeof(header) - 8;
+        fseek(file, 0, SEEK_SET);
+        fwrite(&header, sizeof(struct wav_header), 1, file);
+    }
+
+    fclose(file);
+
+    return 0;
+}
+
+unsigned int capture_sample(FILE *file, unsigned int card, unsigned int device,
+                            unsigned int channels, unsigned int rate,
+                            enum pcm_format format, unsigned int period_size,
+                            unsigned int period_count, unsigned int capture_time)
+{
+    struct pcm_config config;
+    struct pcm *pcm;
+    char *buffer;
+    unsigned int size;
+    unsigned int frames_read;
+    unsigned int total_frames_read;
+    unsigned int bytes_per_frame;
+
+    memset(&config, 0, sizeof(config));
+    config.channels = channels;
+    config.rate = rate;
+    config.period_size = period_size;
+    config.period_count = period_count;
+    config.format = format;
+    config.start_threshold = 0;
+    config.stop_threshold = 0;
+    config.silence_threshold = 0;
+
+    pcm = pcm_open(card, device, PCM_IN, &config);
+    if (!pcm || !pcm_is_ready(pcm)) {
+        fprintf(stderr, "Unable to open PCM device (%s)\n",
+                pcm_get_error(pcm));
+        return 0;
+    }
+
+    size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
+    buffer = malloc(size);
+    if (!buffer) {
+        fprintf(stderr, "Unable to allocate %u bytes\n", size);
+        pcm_close(pcm);
+        return 0;
+    }
+
+    if (prinfo) {
+        printf("Capturing sample: %u ch, %u hz, %u bit\n", channels, rate,
+           pcm_format_to_bits(format));
+    }
+
+    bytes_per_frame = pcm_frames_to_bytes(pcm, 1);
+    total_frames_read = 0;
+    frames_read = 0;
+    while (capturing) {
+        frames_read = pcm_readi(pcm, buffer, pcm_get_buffer_size(pcm));
+        total_frames_read += frames_read;
+        if ((total_frames_read / rate) >= capture_time) {
+            capturing = 0;
+        }
+        if (fwrite(buffer, bytes_per_frame, frames_read, file) != frames_read) {
+            fprintf(stderr,"Error capturing sample\n");
+            break;
+        }
+    }
+
+    free(buffer);
+    pcm_close(pcm);
+    return total_frames_read;
+}
+
diff --git a/utils/tinymix.1 b/utils/tinymix.1
new file mode 100644
index 0000000..e56d67c
--- /dev/null
+++ b/utils/tinymix.1
@@ -0,0 +1,86 @@
+.TH TINYMIX 1 "October 2, 2016" "tinymix" "TinyALSA"
+
+.SH NAME
+tinymix \- view and edit mixer controls for a specified mixer.
+
+.SH SYNOPSIS
+.B tinymix\fR [ \fIoptions\fR ] \fIcommand\fR
+
+.SH Description
+
+\fBtinymix\fR can be used to view and/or edit a list of mixer controls for a specified mixer.
+
+.SH OPTIONS
+
+.TP
+\fB\-D, --card\fR \fIcard\fR
+Card number of the mixer.
+The default is 0.
+
+.TP
+\fB\-h, --help\fR
+Print help contents and exit.
+
+.TP
+\fB\-v, --version\fR
+Print the current version of tinymix and exit.
+
+.SH COMMANDS
+
+.TP
+\fBget <control-id|control-name>\fR
+Prints the value of a specified control
+
+.TP
+\fBset <control-id|control-name> <control-value>\fR
+Sets the value of a specified control
+
+.TP
+\fBcontents\fR
+Prints the contents of all mixer controls.
+
+.TP
+\fBcontrols\fR
+Prints the names and IDs of all mixer controls.
+
+.SH EXAMPLES
+
+.TP
+\fBtinymix controls\fR
+Prints a list of control IDs for the mixer of card 0.
+
+.TP
+\fBtinymix -D 1 controls\fR
+Prints a list of control IDs for the mixer of card 1.
+
+.TP
+\fBtinymix get 0\fR
+Prints information about control 0.
+
+.TP
+\fBtinymix get "Headphone Playback Volume"\fR
+Prints information about a control called "Headphone Playback Volume"\fR
+
+.TP
+\fBtinymix set 0 4\fR
+Sets control 0 to the value of 4.
+
+.TP
+\fBtinymix --card 1 set 2 32
+Sets control 2 of card 1 to the value of 32.
+
+.SH BUGS
+
+Please report bugs to https://github.com/tinyalsa/tinyalsa/issues.
+
+.SH SEE ALSO
+
+.BR tinycap(1),
+.BR tinyplay(1),
+.BR tinypcminfo(1)
+
+.SH AUTHORS
+Simon Wilson
+.P
+For a complete list of authors, visit the project page at https://github.com/tinyalsa/tinyalsa.
+
diff --git a/utils/tinymix.c b/utils/tinymix.c
new file mode 100644
index 0000000..5c0378c
--- /dev/null
+++ b/utils/tinymix.c
@@ -0,0 +1,580 @@
+/* tinymix.c
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <tinyalsa/asoundlib.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+
+#define OPTPARSE_IMPLEMENTATION
+#include "optparse.h"
+
+static void list_controls(struct mixer *mixer, int print_all);
+
+static void print_control_values(struct mixer_ctl *control);
+static void print_control_values_by_name_or_id(struct mixer *mixer, const char *name_or_id);
+
+static int set_values(struct mixer *mixer, const char *control,
+                             char **values, unsigned int num_values);
+
+static void print_enum(struct mixer_ctl *ctl);
+
+void usage(void)
+{
+    printf("usage: tinymix [options] <command>\n");
+    printf("options:\n");
+    printf("\t-h, --help               : prints this help message and exits\n");
+    printf("\t-v, --version            : prints this version of tinymix and exits\n");
+    printf("\t-D, --card NUMBER        : specifies the card number of the mixer\n");
+    printf("\n");
+    printf("commands:\n");
+    printf("\tget NAME|ID              : prints the values of a control\n");
+    printf("\tset NAME|ID VALUE(S) ... : sets the value of a control\n");
+    printf("\t\tVALUE(S): integers, percents, and relative values\n");
+    printf("\t\t\tIntegers: 0, 100, -100 ...\n");
+    printf("\t\t\tPercents: 0%%, 100%% ...\n");
+    printf("\t\t\tRelative values: 1+, 1-, 1%%+, 2%%+ ...\n");
+    printf("\tcontrols                 : lists controls of the mixer\n");
+    printf("\tcontents                 : lists controls of the mixer and their contents\n");
+}
+
+void version(void)
+{
+    printf("tinymix version 2.0 (tinyalsa version %s)\n", TINYALSA_VERSION_STRING);
+}
+
+static int is_command(char *arg) {
+    return strcmp(arg, "get") == 0 || strcmp(arg, "set") == 0 ||
+            strcmp(arg, "controls") == 0 || strcmp(arg, "contents") == 0;
+}
+
+static int find_command_position(int argc, char **argv)
+{
+    for (int i = 0; i < argc; ++i) {
+        if (is_command(argv[i])) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+int main(int argc, char **argv)
+{
+    int card = 0;
+    struct optparse opts;
+    static struct optparse_long long_options[] = {
+        { "card",    'D', OPTPARSE_REQUIRED },
+        { "version", 'v', OPTPARSE_NONE     },
+        { "help",    'h', OPTPARSE_NONE     },
+        { 0, 0, 0 }
+    };
+
+    // optparse_long may change the order of the argv list. Duplicate one for parsing the options.
+    char **argv_options_list = (char **) calloc(argc + 1, sizeof(char *));
+    if (!argv_options_list) {
+        fprintf(stderr, "Failed to allocate options list\n");
+        return EXIT_FAILURE;
+    }
+
+    for (int i = 0; i < argc; ++i) {
+        argv_options_list[i] = argv[i];
+    }
+
+    optparse_init(&opts, argv_options_list);
+    /* Detect the end of the options. */
+    int c;
+    while ((c = optparse_long(&opts, long_options, NULL)) != -1) {
+        switch (c) {
+        case 'D':
+            card = atoi(opts.optarg);
+            break;
+        case 'h':
+            usage();
+            free(argv_options_list);
+            return EXIT_SUCCESS;
+        case 'v':
+            version();
+            free(argv_options_list);
+            return EXIT_SUCCESS;
+        case '?':
+        default:
+            break;
+        }
+    }
+    free(argv_options_list);
+
+    struct mixer *mixer = mixer_open(card);
+    if (!mixer) {
+        fprintf(stderr, "Failed to open mixer\n");
+        return EXIT_FAILURE;
+    }
+
+    int command_position = find_command_position(argc, argv);
+    if (command_position < 0) {
+        usage();
+        mixer_close(mixer);
+        return EXIT_FAILURE;
+    }
+
+    char *cmd = argv[command_position];
+    if (strcmp(cmd, "get") == 0) {
+        if (command_position + 1 >= argc) {
+            fprintf(stderr, "no control specified\n");
+            mixer_close(mixer);
+            return EXIT_FAILURE;
+        }
+        print_control_values_by_name_or_id(mixer, argv[command_position + 1]);
+        printf("\n");
+    } else if (strcmp(cmd, "set") == 0) {
+        if (command_position + 1 >= argc) {
+            fprintf(stderr, "no control specified\n");
+            mixer_close(mixer);
+            return EXIT_FAILURE;
+        }
+        if (command_position + 2 >= argc) {
+            fprintf(stderr, "no value(s) specified\n");
+            mixer_close(mixer);
+            return EXIT_FAILURE;
+        }
+        int res = set_values(mixer,
+                             argv[command_position + 1],
+                             &argv[command_position + 2],
+                             argc - command_position - 2);
+        if (res != 0) {
+            mixer_close(mixer);
+            return EXIT_FAILURE;
+        }
+    } else if (strcmp(cmd, "controls") == 0) {
+        list_controls(mixer, 0);
+    } else if (strcmp(cmd, "contents") == 0) {
+        list_controls(mixer, 1);
+    } else {
+        fprintf(stderr, "unknown command '%s'\n", cmd);
+        usage();
+        mixer_close(mixer);
+        return EXIT_FAILURE;
+    }
+
+    mixer_close(mixer);
+    return EXIT_SUCCESS;
+}
+
+static int isnumber(const char *str) {
+    char *end;
+
+    if (str == NULL || strlen(str) == 0)
+        return 0;
+
+    strtol(str, &end, 0);
+    return strlen(end) == 0;
+}
+
+static void list_controls(struct mixer *mixer, int print_all)
+{
+    struct mixer_ctl *ctl;
+    const char *name, *type;
+    unsigned int num_ctls, num_values;
+    unsigned int i;
+
+    num_ctls = mixer_get_num_ctls(mixer);
+
+    printf("Number of controls: %u\n", num_ctls);
+
+    if (print_all)
+        printf("ctl\ttype\tnum\t%-40svalue\n", "name");
+    else
+        printf("ctl\ttype\tnum\t%-40s\n", "name");
+
+    for (i = 0; i < num_ctls; i++) {
+        ctl = mixer_get_ctl(mixer, i);
+
+        name = mixer_ctl_get_name(ctl);
+        type = mixer_ctl_get_type_string(ctl);
+        num_values = mixer_ctl_get_num_values(ctl);
+        printf("%u\t%s\t%u\t%-40s", i, type, num_values, name);
+        if (print_all)
+            print_control_values(ctl);
+        printf("\n");
+    }
+}
+
+static void print_enum(struct mixer_ctl *ctl)
+{
+    unsigned int num_enums;
+    unsigned int i;
+    unsigned int value;
+    const char *string;
+
+    num_enums = mixer_ctl_get_num_enums(ctl);
+    value = mixer_ctl_get_value(ctl, 0);
+
+    for (i = 0; i < num_enums; i++) {
+        string = mixer_ctl_get_enum_string(ctl, i);
+        printf("%s%s, ", value == i ? "> " : "", string);
+    }
+}
+
+static void print_control_values_by_name_or_id(struct mixer *mixer, const char *name_or_id)
+{
+    struct mixer_ctl *ctl;
+
+    if (isnumber(name_or_id))
+        ctl = mixer_get_ctl(mixer, atoi(name_or_id));
+    else
+        ctl = mixer_get_ctl_by_name(mixer, name_or_id);
+
+    if (!ctl) {
+        fprintf(stderr, "Invalid mixer control\n");
+        return;
+    }
+
+    print_control_values(ctl);
+}
+
+static void print_control_values(struct mixer_ctl *control)
+{
+    enum mixer_ctl_type type;
+    unsigned int num_values;
+    unsigned int i;
+    int min, max;
+    int ret;
+    char *buf = NULL;
+
+    type = mixer_ctl_get_type(control);
+    num_values = mixer_ctl_get_num_values(control);
+
+    if ((type == MIXER_CTL_TYPE_BYTE) && (num_values > 0)) {
+        buf = calloc(1, num_values);
+        if (buf == NULL) {
+            fprintf(stderr, "Failed to alloc mem for bytes %u\n", num_values);
+            return;
+        }
+
+        ret = mixer_ctl_get_array(control, buf, num_values);
+        if (ret < 0) {
+            fprintf(stderr, "Failed to mixer_ctl_get_array\n");
+            free(buf);
+            return;
+        }
+    }
+
+    for (i = 0; i < num_values; i++) {
+        switch (type)
+        {
+        case MIXER_CTL_TYPE_INT:
+            printf("%d", mixer_ctl_get_value(control, i));
+            break;
+        case MIXER_CTL_TYPE_BOOL:
+            printf("%s", mixer_ctl_get_value(control, i) ? "On" : "Off");
+            break;
+        case MIXER_CTL_TYPE_ENUM:
+            print_enum(control);
+            break;
+        case MIXER_CTL_TYPE_BYTE:
+            printf("%02hhx", buf[i]);
+            break;
+        default:
+            printf("unknown");
+            break;
+        };
+        if ((i + 1) < num_values) {
+           printf(", ");
+        }
+    }
+
+    if (type == MIXER_CTL_TYPE_INT) {
+        min = mixer_ctl_get_range_min(control);
+        max = mixer_ctl_get_range_max(control);
+        printf(" (range %d->%d)", min, max);
+    }
+
+    free(buf);
+}
+
+static void tinymix_set_byte_ctl(struct mixer_ctl *ctl,
+                                 char **values, unsigned int num_values)
+{
+    int ret;
+    char *buf;
+    char *end;
+    unsigned int i;
+    long n;
+
+    buf = calloc(1, num_values);
+    if (buf == NULL) {
+        fprintf(stderr, "set_byte_ctl: Failed to alloc mem for bytes %u\n", num_values);
+        exit(EXIT_FAILURE);
+    }
+
+    for (i = 0; i < num_values; i++) {
+        errno = 0;
+        n = strtol(values[i], &end, 0);
+        if (*end) {
+            fprintf(stderr, "%s not an integer\n", values[i]);
+            goto fail;
+        }
+        if (errno) {
+            fprintf(stderr, "strtol: %s: %s\n", values[i],
+                strerror(errno));
+            goto fail;
+        }
+        if (n < 0 || n > 0xff) {
+            fprintf(stderr, "%s should be between [0, 0xff]\n",
+                values[i]);
+            goto fail;
+        }
+        buf[i] = n;
+    }
+
+    ret = mixer_ctl_set_array(ctl, buf, num_values);
+    if (ret < 0) {
+        fprintf(stderr, "Failed to set binary control\n");
+        goto fail;
+    }
+
+    free(buf);
+    return;
+
+fail:
+    free(buf);
+    exit(EXIT_FAILURE);
+}
+
+static int is_int(const char *value)
+{
+    return value[0] >= '0' || value[0] <= '9';
+}
+
+struct parsed_int
+{
+    /** Wether or not the integer was valid. */
+    int valid;
+    /** The value of the parsed integer. */
+    int value;
+    /** The number of characters that were parsed. */
+    unsigned int length;
+    /** The number of characters remaining in the string. */
+    unsigned int remaining_length;
+    /** The remaining characters (or suffix) of the integer. */
+    const char* remaining;
+};
+
+static struct parsed_int parse_int(const char* str)
+{
+    struct parsed_int out = {
+        0 /* valid */,
+        0 /* value */,
+        0 /* length */,
+        0 /* remaining length */,
+        NULL /* remaining characters */
+    };
+
+    size_t length = strlen(str);
+    size_t i = 0;
+    int negative = 0;
+
+    if (i < length && str[i] == '-') {
+        negative = 1;
+        i++;
+    }
+
+    while (i < length) {
+        char c = str[i++];
+
+        if (c < '0' || c > '9') {
+            --i;
+            break;
+        }
+
+        out.value *= 10;
+        out.value += c - '0';
+
+        out.length++;
+    }
+
+    if (negative) {
+        out.value *= -1;
+    }
+
+    out.valid = out.length > 0;
+    out.remaining_length = length - out.length;
+    out.remaining = str + out.length;
+
+    return out;
+}
+
+struct control_value
+{
+    int value;
+    int is_percent;
+    int is_relative;
+};
+
+static struct control_value to_control_value(const char* value_string)
+{
+    struct parsed_int parsed_int = parse_int(value_string);
+
+    struct control_value out = {
+        0 /* value */,
+        0 /* is percent */,
+        0 /* is relative */
+    };
+
+    out.value = parsed_int.value;
+
+    unsigned int i = 0;
+
+    if (parsed_int.remaining[i] == '%') {
+        out.is_percent = 1;
+        i++;
+    }
+
+    if (parsed_int.remaining[i] == '+') {
+        out.is_relative = 1;
+    } else if (parsed_int.remaining[i] == '-') {
+        out.is_relative = 1;
+        out.value *= -1;
+    }
+
+    return out;
+}
+
+static int set_control_value(struct mixer_ctl* ctl, unsigned int i,
+                             const struct control_value* value)
+{
+    int next_value = value->value;
+
+    if (value->is_relative) {
+
+        int prev_value = value->is_percent ? mixer_ctl_get_percent(ctl, i)
+                                           : mixer_ctl_get_value(ctl, i);
+
+        if (prev_value < 0) {
+          return prev_value;
+        }
+
+        next_value += prev_value;
+    }
+
+    return value->is_percent ? mixer_ctl_set_percent(ctl, i, next_value)
+                             : mixer_ctl_set_value(ctl, i, next_value);
+}
+
+static int set_control_values(struct mixer_ctl* ctl,
+                              char** values,
+                              unsigned int num_values)
+{
+    unsigned int num_ctl_values = mixer_ctl_get_num_values(ctl);
+
+    if (num_values == 1) {
+
+        /* Set all values the same */
+        struct control_value value = to_control_value(values[0]);
+
+        for (unsigned int i = 0; i < num_values; i++) {
+            int res = set_control_value(ctl, i, &value);
+            if (res != 0) {
+                fprintf(stderr, "Error: invalid value (%d%s%s)\n", value.value,
+                        value.is_relative ? "r" : "", value.is_percent ? "%" : "");
+                return -1;
+            }
+        }
+
+    } else {
+
+        /* Set multiple values */
+        if (num_values > num_ctl_values) {
+            fprintf(stderr,
+                    "Error: %u values given, but control only takes %u\n",
+                    num_values, num_ctl_values);
+            return -1;
+        }
+
+        for (unsigned int i = 0; i < num_values; i++) {
+
+            struct control_value v = to_control_value(values[i]);
+
+            int res = set_control_value(ctl, i, &v);
+            if (res != 0) {
+                fprintf(stderr, "Error: invalid value (%d%s%s) for index %u\n", v.value,
+                        v.is_relative ? "r" : "", v.is_percent ? "%" : "", i);
+                return -1;
+            }
+        }
+    }
+
+    return 0;
+}
+
+static int set_values(struct mixer *mixer, const char *control,
+                             char **values, unsigned int num_values)
+{
+    struct mixer_ctl *ctl;
+    enum mixer_ctl_type type;
+
+    if (isnumber(control))
+        ctl = mixer_get_ctl(mixer, atoi(control));
+    else
+        ctl = mixer_get_ctl_by_name(mixer, control);
+
+    if (!ctl) {
+        fprintf(stderr, "Invalid mixer control\n");
+        return -1;
+    }
+
+    type = mixer_ctl_get_type(ctl);
+
+    if (type == MIXER_CTL_TYPE_BYTE) {
+        tinymix_set_byte_ctl(ctl, values, num_values);
+        return 0;
+    }
+
+    if (is_int(values[0])) {
+        set_control_values(ctl, values, num_values);
+    } else {
+        if (type == MIXER_CTL_TYPE_ENUM) {
+            if (num_values != 1) {
+                fprintf(stderr, "Enclose strings in quotes and try again\n");
+                return -1;
+            }
+            if (mixer_ctl_set_enum_by_string(ctl, values[0])) {
+                fprintf(stderr, "Error: invalid enum value\n");
+                return -1;
+            }
+        } else {
+            fprintf(stderr, "Error: only enum types can be set with strings\n");
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
diff --git a/utils/tinypcminfo.1 b/utils/tinypcminfo.1
new file mode 100644
index 0000000..bcbcf96
--- /dev/null
+++ b/utils/tinypcminfo.1
@@ -0,0 +1,57 @@
+.TH TINYPCMINFO 1 "October 2, 2016" "tinypcminfo" "TinyALSA"
+
+.SH NAME
+tinypcminfo \- prints the hardware parameters of a PCM.
+
+.SH SYNOPSIS
+.B tinypcminfo\fR [ \fIoptions\fR ]
+
+.SH Description
+
+\fBtinypcminfo\fR prints the hardware parameters of a PCM, specified by it's card and device number.
+
+.SH OPTIONS
+
+.TP
+\fB\-D\fR \fIcard\fR
+Card number of the PCM.
+The default is 0.
+
+.TP
+\fB\-d\fR \fIdevice\fR
+Device number of the PCM.
+The default is 0.
+
+.SH EXAMPLES
+
+.TP
+\fBtinypcminfo\fR
+Prints hardware parameters for the PCM of card 0 and device 0.
+
+.TP
+\fBtinypcminfo -D 1
+Prints hardware parameters for the PCM of card 1 and device 0.
+
+.TP
+\fBtinypcminfo -d 1
+Prints hardware parameters for the PCM of card 0 and device 1.
+
+.TP
+\fBtinypcminfo -D 1 -d 1
+Prints hardware parameters for the PCM of card 1 and device 1.
+
+.SH BUGS
+
+Please report bugs to https://github.com/tinyalsa/tinyalsa/issues.
+
+.SH SEE ALSO
+
+.BR tinycap(1),
+.BR tinyplay(1),
+.BR tinymix(1)
+
+.SH AUTHORS
+Simon Wilson
+.P
+For a complete list of authors, visit the project page at https://github.com/tinyalsa/tinyalsa.
+
diff --git a/utils/tinypcminfo.c b/utils/tinypcminfo.c
new file mode 100644
index 0000000..3116b7c
--- /dev/null
+++ b/utils/tinypcminfo.c
@@ -0,0 +1,211 @@
+/* tinypcminfo.c
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <tinyalsa/asoundlib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define OPTPARSE_IMPLEMENTATION
+#include "optparse.h"
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0]))
+#endif
+
+/* The format_lookup is in order of SNDRV_PCM_FORMAT_##index and
+ * matches the grouping in sound/asound.h.  Note this is not
+ * continuous and has an empty gap from (25 - 30).
+ */
+static const char *format_lookup[] = {
+        /*[0] =*/ "S8",
+        "U8",
+        "S16_LE",
+        "S16_BE",
+        "U16_LE",
+        "U16_BE",
+        "S24_LE",
+        "S24_BE",
+        "U24_LE",
+        "U24_BE",
+        "S32_LE",
+        "S32_BE",
+        "U32_LE",
+        "U32_BE",
+        "FLOAT_LE",
+        "FLOAT_BE",
+        "FLOAT64_LE",
+        "FLOAT64_BE",
+        "IEC958_SUBFRAME_LE",
+        "IEC958_SUBFRAME_BE",
+        "MU_LAW",
+        "A_LAW",
+        "IMA_ADPCM",
+        "MPEG",
+        /*[24] =*/ "GSM",
+        [31] = "SPECIAL",
+        "S24_3LE",
+        "S24_3BE",
+        "U24_3LE",
+        "U24_3BE",
+        "S20_3LE",
+        "S20_3BE",
+        "U20_3LE",
+        "U20_3BE",
+        "S18_3LE",
+        "S18_3BE",
+        "U18_3LE",
+        /*[43] =*/ "U18_3BE",
+#if 0
+        /* recent additions, may not be present on local asound.h */
+        "G723_24",
+        "G723_24_1B",
+        "G723_40",
+        "G723_40_1B",
+        "DSD_U8",
+        "DSD_U16_LE",
+#endif
+};
+
+/* Returns a human readable name for the format associated with bit_index,
+ * NULL if bit_index is not known.
+ */
+static inline const char *pcm_get_format_name(unsigned bit_index)
+{
+    return bit_index < ARRAY_SIZE(format_lookup) ? format_lookup[bit_index] : NULL;
+}
+
+int main(int argc, char **argv)
+{
+    unsigned int device = 0;
+    unsigned int card = 0;
+    int i;
+    struct optparse opts;
+    struct optparse_long long_options[] = {
+        { "help",   'h', OPTPARSE_NONE     },
+        { "card",   'D', OPTPARSE_REQUIRED },
+        { "device", 'd', OPTPARSE_REQUIRED },
+        { 0, 0, 0 }
+    };
+
+    (void)argc; /* silence -Wunused-parameter */
+    /* parse command line arguments */
+    optparse_init(&opts, argv);
+    while ((i = optparse_long(&opts, long_options, NULL)) != -1) {
+        switch (i) {
+        case 'D':
+            card = atoi(opts.optarg);
+            break;
+        case 'd':
+            device = atoi(opts.optarg);
+            break;
+        case 'h':
+            fprintf(stderr, "Usage: %s -D card -d device\n", argv[0]);
+            return 0;
+        case '?':
+            fprintf(stderr, "%s\n", opts.errmsg);
+            return EXIT_FAILURE;
+        }
+    }
+
+    printf("Info for card %u, device %u:\n", card, device);
+
+    for (i = 0; i < 2; i++) {
+        struct pcm_params *params;
+        const struct pcm_mask *m;
+        unsigned int min;
+        unsigned int max;
+
+        printf("\nPCM %s:\n", i == 0 ? "out" : "in");
+
+        params = pcm_params_get(card, device, i == 0 ? PCM_OUT : PCM_IN);
+        if (params == NULL) {
+            printf("Device does not exist.\n");
+            continue;
+        }
+
+        m = pcm_params_get_mask(params, PCM_PARAM_ACCESS);
+        if (m) { /* bitmask, refer to SNDRV_PCM_ACCESS_*, generally interleaved */
+            printf("      Access:\t%#08x\n", m->bits[0]);
+        }
+        m = pcm_params_get_mask(params, PCM_PARAM_FORMAT);
+        if (m) { /* bitmask, refer to: SNDRV_PCM_FORMAT_* */
+            unsigned j, k, count = 0;
+            const unsigned bitcount = sizeof(m->bits[0]) * 8;
+
+            /* we only check first two format masks (out of 8) - others are zero. */
+            printf("   Format[0]:\t%#08x\n", m->bits[0]);
+            printf("   Format[1]:\t%#08x\n", m->bits[1]);
+
+            /* print friendly format names, if they exist */
+            for (k = 0; k < 2; ++k) {
+                for (j = 0; j < bitcount; ++j) {
+                    const char *name;
+
+                    if (m->bits[k] & (1 << j)) {
+                        name = pcm_get_format_name(j + k*bitcount);
+                        if (name) {
+                            if (count++ == 0) {
+                                printf(" Format Name:\t");
+                            } else {
+                                printf (", ");
+                            }
+                            printf("%s", name);
+                        }
+                    }
+                }
+            }
+            if (count) {
+                printf("\n");
+            }
+        }
+        m = pcm_params_get_mask(params, PCM_PARAM_SUBFORMAT);
+        if (m) { /* bitmask, should be 1: SNDRV_PCM_SUBFORMAT_STD */
+            printf("   Subformat:\t%#08x\n", m->bits[0]);
+        }
+        min = pcm_params_get_min(params, PCM_PARAM_RATE);
+        max = pcm_params_get_max(params, PCM_PARAM_RATE);
+        printf("        Rate:\tmin=%uHz\tmax=%uHz\n", min, max);
+        min = pcm_params_get_min(params, PCM_PARAM_CHANNELS);
+        max = pcm_params_get_max(params, PCM_PARAM_CHANNELS);
+        printf("    Channels:\tmin=%u\t\tmax=%u\n", min, max);
+        min = pcm_params_get_min(params, PCM_PARAM_SAMPLE_BITS);
+        max = pcm_params_get_max(params, PCM_PARAM_SAMPLE_BITS);
+        printf(" Sample bits:\tmin=%u\t\tmax=%u\n", min, max);
+        min = pcm_params_get_min(params, PCM_PARAM_PERIOD_SIZE);
+        max = pcm_params_get_max(params, PCM_PARAM_PERIOD_SIZE);
+        printf(" Period size:\tmin=%u\t\tmax=%u\n", min, max);
+        min = pcm_params_get_min(params, PCM_PARAM_PERIODS);
+        max = pcm_params_get_max(params, PCM_PARAM_PERIODS);
+        printf("Period count:\tmin=%u\t\tmax=%u\n", min, max);
+
+        pcm_params_free(params);
+    }
+
+    return 0;
+}
diff --git a/utils/tinyplay.1 b/utils/tinyplay.1
new file mode 100644
index 0000000..56bac12
--- /dev/null
+++ b/utils/tinyplay.1
@@ -0,0 +1,94 @@
+.TH TINYPLAY 1 "October 2, 2016" "tinyplay" "TinyALSA"
+
+.SH NAME
+tinyplay \- sends audio to an audio device
+
+.SH SYNOPSIS
+.B tinyplay\fR \fIfile\fR [ \fIoptions\fR ]
+
+.SH Description
+
+\fBtinyplay\fR can send audio to an audio device from a wav file or standard input (as raw samples).
+Options can be used to specify various hardware parameters to open the PCM with.
+
+.SH OPTIONS
+
+.TP
+\fB\-D, --card\fR \fIcard\fR
+Card number of the PCM.
+The default is 0.
+
+.TP
+\fB\-d, --device\fR \fIdevice\fR
+Device number of the PCM.
+The default is 0.
+
+.TP
+\fB\-c, --channels\fR \fIchannels\fR
+Number of channels the PCM will have.
+This option is only valid for raw file types.
+The default is 2 for raw file types.
+
+.TP
+\fB\-r, --rate\fR \fIrate\fR
+Number of frames per second of the PCM.
+This option is only valid for raw file types.
+The default is 48000 for raw file types.
+
+.TP
+\fB\-i, --file-type\fR \fIfile-type\fR
+The file type used for playback.
+Available types are \fIraw\fR and \fIwav\fR.
+Specifying \fIraw\fR means that \fIchannels\fR, \fIrate\fR and \fIbits\fR may have to be specified as well.
+By default, the file type is determined by the file name.
+Specifying the file type with this option will take precedent over the one determined by the file name.
+
+.TP
+\fB\-b, --bits\fR \fIbits\fR
+Number of bits per sample the PCM will have.
+This option is only valid for raw file types.
+The default is 16 for raw file types.
+
+.TP
+\fB\-p, --period-size\fR \fIperiod_size\fR
+Number of frames in a period.
+The default is 1024.
+
+.TP
+\fB\-n, --period-count\fR \fIperiods\fR
+Number of periods the PCM will have.
+The default is 4.
+
+.SH SIGNALS
+
+When playing audio, SIGINT will stop the playback and close the file.
+
+.SH EXAMPLES
+
+.TP
+\fBtinyplay output.wav\fR
+Plays a file called output.wav.
+
+.TP
+\fBtinyplay output.wav -D 1
+Plays a file called output.wav on card 1.
+
+.TP
+\fBtinyplay output.raw -i raw --channels 2 --rate 44100 --bits 32
+Plays a raw audio file called output.raw; using 2 channels, 44100 frames per second and 32 bits per sample.
+
+.SH BUGS
+
+Please report bugs to https://github.com/tinyalsa/tinyalsa/issues.
+
+.SH SEE ALSO
+
+.BR tinycap(1),
+.BR tinymix(1),
+.BR tinypcminfo(1)
+
+.SH AUTHORS
+Simon Wilson
+.P
+For a complete list of authors, visit the project page at https://github.com/tinyalsa/tinyalsa.
+
diff --git a/utils/tinyplay.c b/utils/tinyplay.c
new file mode 100644
index 0000000..2689158
--- /dev/null
+++ b/utils/tinyplay.c
@@ -0,0 +1,435 @@
+/* tinyplay.c
+**
+** Copyright 2011, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <tinyalsa/asoundlib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <signal.h>
+
+#define OPTPARSE_IMPLEMENTATION
+#include "optparse.h"
+
+struct cmd {
+    const char *filename;
+    const char *filetype;
+    unsigned int card;
+    unsigned int device;
+    int flags;
+    struct pcm_config config;
+    unsigned int bits;
+};
+
+void cmd_init(struct cmd *cmd)
+{
+    cmd->filename = NULL;
+    cmd->filetype = NULL;
+    cmd->card = 0;
+    cmd->device = 0;
+    cmd->flags = PCM_OUT;
+    cmd->config.period_size = 1024;
+    cmd->config.period_count = 2;
+    cmd->config.channels = 2;
+    cmd->config.rate = 48000;
+    cmd->config.format = PCM_FORMAT_S16_LE;
+    cmd->config.silence_threshold = cmd->config.period_size * cmd->config.period_count;
+    cmd->config.silence_size = 0;
+    cmd->config.stop_threshold = cmd->config.period_size * cmd->config.period_count;
+    cmd->config.start_threshold = cmd->config.period_size;
+    cmd->bits = 16;
+}
+
+#define ID_RIFF 0x46464952
+#define ID_WAVE 0x45564157
+#define ID_FMT  0x20746d66
+#define ID_DATA 0x61746164
+
+struct riff_wave_header {
+    uint32_t riff_id;
+    uint32_t riff_sz;
+    uint32_t wave_id;
+};
+
+struct chunk_header {
+    uint32_t id;
+    uint32_t sz;
+};
+
+struct chunk_fmt {
+    uint16_t audio_format;
+    uint16_t num_channels;
+    uint32_t sample_rate;
+    uint32_t byte_rate;
+    uint16_t block_align;
+    uint16_t bits_per_sample;
+};
+
+struct ctx {
+    struct pcm *pcm;
+
+    struct riff_wave_header wave_header;
+    struct chunk_header chunk_header;
+    struct chunk_fmt chunk_fmt;
+
+    FILE *file;
+};
+
+int ctx_init(struct ctx* ctx, const struct cmd *cmd)
+{
+    unsigned int bits = cmd->bits;
+    struct pcm_config config = cmd->config;
+
+    if (cmd->filename == NULL) {
+        fprintf(stderr, "filename not specified\n");
+        return -1;
+    }
+    if (strcmp(cmd->filename, "-") == 0) {
+        ctx->file = stdin;
+    } else {
+        ctx->file = fopen(cmd->filename, "rb");
+    }
+
+    if (ctx->file == NULL) {
+        fprintf(stderr, "failed to open '%s'\n", cmd->filename);
+        return -1;
+    }
+
+    if ((cmd->filetype != NULL) && (strcmp(cmd->filetype, "wav") == 0)) {
+        if (fread(&ctx->wave_header, sizeof(ctx->wave_header), 1, ctx->file) != 1){
+            fprintf(stderr, "error: '%s' does not contain a riff/wave header\n", cmd->filename);
+            fclose(ctx->file);
+            return -1;
+        }
+        if ((ctx->wave_header.riff_id != ID_RIFF) ||
+            (ctx->wave_header.wave_id != ID_WAVE)) {
+            fprintf(stderr, "error: '%s' is not a riff/wave file\n", cmd->filename);
+            fclose(ctx->file);
+            return -1;
+        }
+        unsigned int more_chunks = 1;
+        do {
+            if (fread(&ctx->chunk_header, sizeof(ctx->chunk_header), 1, ctx->file) != 1){
+                fprintf(stderr, "error: '%s' does not contain a data chunk\n", cmd->filename);
+                fclose(ctx->file);
+                return -1;
+            }
+            switch (ctx->chunk_header.id) {
+            case ID_FMT:
+                if (fread(&ctx->chunk_fmt, sizeof(ctx->chunk_fmt), 1, ctx->file) != 1){
+                    fprintf(stderr, "error: '%s' has incomplete format chunk\n", cmd->filename);
+                    fclose(ctx->file);
+                    return -1;
+                }
+                /* If the format header is larger, skip the rest */
+                if (ctx->chunk_header.sz > sizeof(ctx->chunk_fmt))
+                    fseek(ctx->file, ctx->chunk_header.sz - sizeof(ctx->chunk_fmt), SEEK_CUR);
+                break;
+            case ID_DATA:
+                /* Stop looking for chunks */
+                more_chunks = 0;
+                break;
+            default:
+                /* Unknown chunk, skip bytes */
+                fseek(ctx->file, ctx->chunk_header.sz, SEEK_CUR);
+            }
+        } while (more_chunks);
+        config.channels = ctx->chunk_fmt.num_channels;
+        config.rate = ctx->chunk_fmt.sample_rate;
+        bits = ctx->chunk_fmt.bits_per_sample;
+    }
+
+    if (bits == 8) {
+        config.format = PCM_FORMAT_S8;
+    } else if (bits == 16) {
+        config.format = PCM_FORMAT_S16_LE;
+    } else if (bits == 24) {
+        config.format = PCM_FORMAT_S24_3LE;
+    } else if (bits == 32) {
+        config.format = PCM_FORMAT_S32_LE;
+    } else {
+        fprintf(stderr, "bit count '%u' not supported\n", bits);
+        fclose(ctx->file);
+        return -1;
+    }
+
+    ctx->pcm = pcm_open(cmd->card,
+                        cmd->device,
+                        cmd->flags,
+                        &config);
+    if (ctx->pcm == NULL) {
+        fprintf(stderr, "failed to allocate memory for pcm\n");
+        fclose(ctx->file);
+        return -1;
+    } else if (!pcm_is_ready(ctx->pcm)) {
+        fprintf(stderr, "failed to open for pcm %u,%u\n", cmd->card, cmd->device);
+        fclose(ctx->file);
+        pcm_close(ctx->pcm);
+        return -1;
+    }
+
+    return 0;
+}
+
+void ctx_free(struct ctx *ctx)
+{
+    if (ctx == NULL) {
+        return;
+    }
+    if (ctx->pcm != NULL) {
+        pcm_close(ctx->pcm);
+    }
+    if (ctx->file != NULL) {
+        fclose(ctx->file);
+    }
+}
+
+static int close = 0;
+
+int play_sample(struct ctx *ctx);
+
+void stream_close(int sig)
+{
+    /* allow the stream to be closed gracefully */
+    signal(sig, SIG_IGN);
+    close = 1;
+}
+
+void print_usage(const char *argv0)
+{
+    fprintf(stderr, "usage: %s file.wav [options]\n", argv0);
+    fprintf(stderr, "options:\n");
+    fprintf(stderr, "-D | --card   <card number>    The card to receive the audio\n");
+    fprintf(stderr, "-d | --device <device number>  The device to receive the audio\n");
+    fprintf(stderr, "-p | --period-size <size>      The size of the PCM's period\n");
+    fprintf(stderr, "-n | --period-count <count>    The number of PCM periods\n");
+    fprintf(stderr, "-i | --file-type <file-type >  The type of file to read (raw or wav)\n");
+    fprintf(stderr, "-c | --channels <count>        The amount of channels per frame\n");
+    fprintf(stderr, "-r | --rate <rate>             The amount of frames per second\n");
+    fprintf(stderr, "-b | --bits <bit-count>        The number of bits in one sample\n");
+    fprintf(stderr, "-M | --mmap                    Use memory mapped IO to play audio\n");
+}
+
+int main(int argc, char **argv)
+{
+    int c;
+    struct cmd cmd;
+    struct ctx ctx;
+    struct optparse opts;
+    struct optparse_long long_options[] = {
+        { "card",         'D', OPTPARSE_REQUIRED },
+        { "device",       'd', OPTPARSE_REQUIRED },
+        { "period-size",  'p', OPTPARSE_REQUIRED },
+        { "period-count", 'n', OPTPARSE_REQUIRED },
+        { "file-type",    'i', OPTPARSE_REQUIRED },
+        { "channels",     'c', OPTPARSE_REQUIRED },
+        { "rate",         'r', OPTPARSE_REQUIRED },
+        { "bits",         'b', OPTPARSE_REQUIRED },
+        { "mmap",         'M', OPTPARSE_NONE     },
+        { "help",         'h', OPTPARSE_NONE     },
+        { 0, 0, 0 }
+    };
+
+    if (argc < 2) {
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+    }
+
+    cmd_init(&cmd);
+    optparse_init(&opts, argv);
+    while ((c = optparse_long(&opts, long_options, NULL)) != -1) {
+        switch (c) {
+        case 'D':
+            if (sscanf(opts.optarg, "%u", &cmd.card) != 1) {
+                fprintf(stderr, "failed parsing card number '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'd':
+            if (sscanf(opts.optarg, "%u", &cmd.device) != 1) {
+                fprintf(stderr, "failed parsing device number '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'p':
+            if (sscanf(opts.optarg, "%u", &cmd.config.period_size) != 1) {
+                fprintf(stderr, "failed parsing period size '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'n':
+            if (sscanf(opts.optarg, "%u", &cmd.config.period_count) != 1) {
+                fprintf(stderr, "failed parsing period count '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'c':
+            if (sscanf(opts.optarg, "%u", &cmd.config.channels) != 1) {
+                fprintf(stderr, "failed parsing channel count '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'r':
+            if (sscanf(opts.optarg, "%u", &cmd.config.rate) != 1) {
+                fprintf(stderr, "failed parsing rate '%s'\n", argv[1]);
+                return EXIT_FAILURE;
+            }
+            break;
+        case 'i':
+            cmd.filetype = opts.optarg;
+            break;
+        case 'h':
+            print_usage(argv[0]);
+            return EXIT_SUCCESS;
+        case '?':
+            fprintf(stderr, "%s\n", opts.errmsg);
+            return EXIT_FAILURE;
+        }
+    }
+    cmd.filename = optparse_arg(&opts);
+
+    if (cmd.filename != NULL && cmd.filetype == NULL &&
+        (cmd.filetype = strrchr(cmd.filename, '.')) != NULL) {
+        cmd.filetype++;
+    }
+
+    cmd.config.silence_threshold = cmd.config.period_size * cmd.config.period_count;
+    cmd.config.stop_threshold = cmd.config.period_size * cmd.config.period_count;
+    cmd.config.start_threshold = cmd.config.period_size;
+
+    if (ctx_init(&ctx, &cmd) < 0) {
+        return EXIT_FAILURE;
+    }
+
+    /* TODO get parameters from context */
+    printf("playing '%s': %u ch, %u hz, %u bit\n",
+           cmd.filename,
+           cmd.config.channels,
+           cmd.config.rate,
+           cmd.bits);
+
+    if (play_sample(&ctx) < 0) {
+        ctx_free(&ctx);
+        return EXIT_FAILURE;
+    }
+
+    ctx_free(&ctx);
+    return EXIT_SUCCESS;
+}
+
+int check_param(struct pcm_params *params, unsigned int param, unsigned int value,
+                 char *param_name, char *param_unit)
+{
+    unsigned int min;
+    unsigned int max;
+    int is_within_bounds = 1;
+
+    min = pcm_params_get_min(params, param);
+    if (value < min) {
+        fprintf(stderr, "%s is %u%s, device only supports >= %u%s\n", param_name, value,
+                param_unit, min, param_unit);
+        is_within_bounds = 0;
+    }
+
+    max = pcm_params_get_max(params, param);
+    if (value > max) {
+        fprintf(stderr, "%s is %u%s, device only supports <= %u%s\n", param_name, value,
+                param_unit, max, param_unit);
+        is_within_bounds = 0;
+    }
+
+    return is_within_bounds;
+}
+
+int sample_is_playable(const struct cmd *cmd)
+{
+    struct pcm_params *params;
+    int can_play;
+
+    params = pcm_params_get(cmd->card, cmd->device, PCM_OUT);
+    if (params == NULL) {
+        fprintf(stderr, "unable to open PCM %u,%u\n", cmd->card, cmd->device);
+        return 0;
+    }
+
+    can_play = check_param(params, PCM_PARAM_RATE, cmd->config.rate, "sample rate", "hz");
+    can_play &= check_param(params, PCM_PARAM_CHANNELS, cmd->config.channels, "sample", " channels");
+    can_play &= check_param(params, PCM_PARAM_SAMPLE_BITS, cmd->bits, "bits", " bits");
+    can_play &= check_param(params, PCM_PARAM_PERIOD_SIZE, cmd->config.period_size, "period size",
+                            " frames");
+    can_play &= check_param(params, PCM_PARAM_PERIODS, cmd->config.period_count, "period count",
+                            " frames");
+
+    pcm_params_free(params);
+
+    return can_play;
+}
+
+int play_sample(struct ctx *ctx)
+{
+    char *buffer;
+    size_t buffer_size = 0;
+    size_t num_read = 0;
+    size_t remaining_data_size = ctx->chunk_header.sz;
+    size_t read_size = 0;
+    const struct pcm_config *config = pcm_get_config(ctx->pcm);
+
+    if (config == NULL) {
+        fprintf(stderr, "unable to get pcm config\n");
+        return -1;
+    }
+
+    buffer_size = pcm_frames_to_bytes(ctx->pcm, config->period_size);
+    buffer = malloc(buffer_size);
+    if (!buffer) {
+        fprintf(stderr, "unable to allocate %zu bytes\n", buffer_size);
+        return -1;
+    }
+
+    /* catch ctrl-c to shutdown cleanly */
+    signal(SIGINT, stream_close);
+
+    do {
+        read_size = remaining_data_size > buffer_size ? buffer_size : remaining_data_size;
+        num_read = fread(buffer, 1, read_size, ctx->file);
+        if (num_read > 0) {
+            if (pcm_writei(ctx->pcm, buffer,
+                pcm_bytes_to_frames(ctx->pcm, num_read)) < 0) {
+                fprintf(stderr, "error playing sample\n");
+                break;
+            }
+            remaining_data_size -= num_read;
+        }
+    } while (!close && num_read > 0 && remaining_data_size > 0);
+
+    pcm_wait(ctx->pcm, -1);
+
+    free(buffer);
+    return 0;
+}
+
diff --git a/utils/tinywavinfo.c b/utils/tinywavinfo.c
new file mode 100644
index 0000000..a74ca7d
--- /dev/null
+++ b/utils/tinywavinfo.c
@@ -0,0 +1,224 @@
+/* tinywavinfo.c
+**
+** Copyright 2015, The Android Open Source Project
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are met:
+**     * Redistributions of source code must retain the above copyright
+**       notice, this list of conditions and the following disclaimer.
+**     * Redistributions in binary form must reproduce the above copyright
+**       notice, this list of conditions and the following disclaimer in the
+**       documentation and/or other materials provided with the distribution.
+**     * Neither the name of The Android Open Source Project nor the names of
+**       its contributors may be used to endorse or promote products derived
+**       from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY The Android Open Source Project ``AS IS'' AND
+** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+** ARE DISCLAIMED. IN NO EVENT SHALL The Android Open Source Project BE LIABLE
+** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+** DAMAGE.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <signal.h>
+#include <math.h>
+
+#define ID_RIFF 0x46464952
+#define ID_WAVE 0x45564157
+#define ID_FMT  0x20746d66
+#define ID_DATA 0x61746164
+
+struct riff_wave_header {
+    uint32_t riff_id;
+    uint32_t riff_sz;
+    uint32_t wave_id;
+};
+
+struct chunk_header {
+    uint32_t id;
+    uint32_t sz;
+};
+
+struct chunk_fmt {
+    uint16_t audio_format;
+    uint16_t num_channels;
+    uint32_t sample_rate;
+    uint32_t byte_rate;
+    uint16_t block_align;
+    uint16_t bits_per_sample;
+};
+
+static int close = 0;
+
+void analyse_sample(FILE *file, unsigned int channels, unsigned int bits,
+                    unsigned int data_chunk_size);
+
+void stream_close(int sig)
+{
+    /* allow the stream to be closed gracefully */
+    signal(sig, SIG_IGN);
+    close = 1;
+}
+
+size_t xfread(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+    size_t sz = fread(ptr, size, nmemb, stream);
+
+    if (sz != nmemb && ferror(stream)) {
+        fprintf(stderr, "Error: fread failed\n");
+        exit(1);
+    }
+    return sz;
+}
+
+int main(int argc, char **argv)
+{
+    FILE *file;
+    struct riff_wave_header riff_wave_header;
+    struct chunk_header chunk_header;
+    struct chunk_fmt chunk_fmt;
+    char *filename;
+    int more_chunks = 1;
+
+    if (argc < 2) {
+        fprintf(stderr, "Usage: %s file.wav \n", argv[0]);
+        return 1;
+    }
+
+    filename = argv[1];
+    file = fopen(filename, "rb");
+    if (!file) {
+        fprintf(stderr, "Unable to open file '%s'\n", filename);
+        return 1;
+    }
+
+    xfread(&riff_wave_header, sizeof(riff_wave_header), 1, file);
+    if ((riff_wave_header.riff_id != ID_RIFF) ||
+        (riff_wave_header.wave_id != ID_WAVE)) {
+        fprintf(stderr, "Error: '%s' is not a riff/wave file\n", filename);
+        fclose(file);
+        return 1;
+    }
+
+    do {
+        xfread(&chunk_header, sizeof(chunk_header), 1, file);
+
+        switch (chunk_header.id) {
+        case ID_FMT:
+            xfread(&chunk_fmt, sizeof(chunk_fmt), 1, file);
+            /* If the format header is larger, skip the rest */
+            if (chunk_header.sz > sizeof(chunk_fmt))
+                fseek(file, chunk_header.sz - sizeof(chunk_fmt), SEEK_CUR);
+            break;
+        case ID_DATA:
+            /* Stop looking for chunks */
+            more_chunks = 0;
+            break;
+        default:
+            /* Unknown chunk, skip bytes */
+            fseek(file, chunk_header.sz, SEEK_CUR);
+        }
+    } while (more_chunks);
+
+    printf("Input File       : %s \n", filename);
+    printf("Channels         : %u \n", chunk_fmt.num_channels);
+    printf("Sample Rate      : %u \n", chunk_fmt.sample_rate);
+    printf("Bits per sample  : %u \n\n", chunk_fmt.bits_per_sample);
+
+    analyse_sample(file, chunk_fmt.num_channels, chunk_fmt.bits_per_sample,
+                    chunk_header.sz);
+
+    fclose(file);
+
+    return 0;
+}
+
+void analyse_sample(FILE *file, unsigned int channels, unsigned int bits,
+                    unsigned int data_chunk_size)
+{
+    void *buffer;
+    int size;
+    int num_read;
+    int i;
+    unsigned int ch;
+    int frame_size = 1024;
+    unsigned int bytes_per_sample = 0;
+    float *power;
+    int total_sample_per_channel;
+    float normalization_factor;
+
+    if (bits == 32)
+        bytes_per_sample = 4;
+    else if (bits == 16)
+        bytes_per_sample = 2;
+
+    normalization_factor = (float)pow(2.0, (bits-1));
+
+    size = channels * bytes_per_sample * frame_size;
+
+    buffer = malloc(size);
+    if (!buffer) {
+        fprintf(stderr, "Unable to allocate %d bytes\n", size);
+        free(buffer);
+        return;
+    }
+
+    power = (float *) calloc(channels, sizeof(float));
+
+    total_sample_per_channel = data_chunk_size / (channels * bytes_per_sample);
+
+    /* catch ctrl-c to shutdown cleanly */
+    signal(SIGINT, stream_close);
+
+    do {
+        num_read = xfread(buffer, 1, size, file);
+        if (num_read > 0) {
+            if (2 == bytes_per_sample) {
+                short *buffer_ptr = (short *)buffer;
+                for (i = 0; i < num_read; i += channels) {
+                    for (ch = 0; ch < channels; ch++) {
+                        int temp = *buffer_ptr++;
+                        /* Signal Normalization */
+                        float f = (float) temp / normalization_factor;
+                        *(power + ch) += (float) (f * f);
+                    }
+                }
+            }
+            if (4 == bytes_per_sample) {
+                int *buffer_ptr = (int *)buffer;
+                for (i = 0; i < num_read; i += channels) {
+                    for (ch = 0; ch < channels; ch++) {
+                        int temp = *buffer_ptr++;
+                        /* Signal Normalization */
+                        float f = (float) temp / normalization_factor;
+                        *(power + ch) += (float) (f * f);
+                    }
+                }
+            }
+        }
+    }while (!close && num_read > 0);
+
+    for (ch = 0; ch < channels; ch++) {
+        float average_power = 10 * log10((*(power + ch)) / total_sample_per_channel);
+        if(isinf (average_power)) {
+            printf("Channel [%2u] Average Power : NO signal or ZERO signal\n", ch);
+        } else {
+            printf("Channel [%2u] Average Power : %.2f dB\n", ch, average_power);
+        }
+    }
+
+    free(buffer);
+    free(power);
+
+}
+