git: 3524d4ebbe1f - main - sound examples: Add mmap example
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 14 Apr 2026 10:59:18 UTC
The branch main has been updated by christos:
URL: https://cgit.FreeBSD.org/src/commit/?id=3524d4ebbe1f562dd76dc553c085386aadfd2682
commit 3524d4ebbe1f562dd76dc553c085386aadfd2682
Author: Goran Mekić <meka@tilda.center>
AuthorDate: 2026-04-14 10:57:53 +0000
Commit: Christos Margiolis <christos@FreeBSD.org>
CommitDate: 2026-04-14 10:59:14 +0000
sound examples: Add mmap example
This example opens separate OSS capture and playback channels in mmap
mode, places them into a sync group, and starts them together so both
ring buffers advance on the same device timeline. It then monitors the
capture mmap pointer with SNDCTL_DSP_GETIPTR, converts that pointer into
monotonic absolute progress using the reported block count, and copies
newly recorded audio from the input ring to the matching region of the
output ring.
The main loop is driven by an absolute monotonic frame clock rather than
a fixed relative usleep delay. Wakeups are scheduled from the sample
rate using a small frame step similar to the SOSSO timing model, while
the audio path itself stays intentionally simple: just copy input to
output, with no explicit xrun recovery or processing beyond ring
wraparound handling.
MFC after: 1 week
Reviewed by: christos
Differential Revision: https://reviews.freebsd.org/D53749
---
share/examples/Makefile | 1 +
share/examples/sound/mmap.c | 297 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 298 insertions(+)
diff --git a/share/examples/Makefile b/share/examples/Makefile
index 0174792d2ecb..d977f2e5a0da 100644
--- a/share/examples/Makefile
+++ b/share/examples/Makefile
@@ -323,6 +323,7 @@ SE_DIRS+= sound
SE_SOUND= \
kqueue.c \
midi.c \
+ mmap.c \
oss.h \
poll.c \
select.c \
diff --git a/share/examples/sound/mmap.c b/share/examples/sound/mmap.c
new file mode 100644
index 000000000000..7f165d417020
--- /dev/null
+++ b/share/examples/sound/mmap.c
@@ -0,0 +1,297 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Goran Mekić
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ */
+
+/*
+ * This program demonstrates low-latency audio pass-through using mmap.
+ * Opens input and output audio devices using memory-mapped I/O,
+ * synchronizes them in a sync group for simultaneous start,
+ * then continuously copies audio data from input to output.
+ */
+
+#include <time.h>
+
+#include "oss.h"
+
+/*
+ * Get current time in nanoseconds using monotonic clock.
+ * Monotonic clock is not affected by system time changes.
+ */
+static int64_t
+gettime_ns(void)
+{
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
+ err(1, "clock_gettime failed");
+ return ((int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec);
+}
+
+/*
+ * Sleep until the specified absolute time (in nanoseconds).
+ * Uses TIMER_ABSTIME for precise timing synchronization.
+ */
+static void
+sleep_until_ns(int64_t target_ns)
+{
+ struct timespec ts;
+
+ ts.tv_sec = target_ns / 1000000000LL;
+ ts.tv_nsec = target_ns % 1000000000LL;
+ if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) != 0)
+ err(1, "clock_nanosleep failed");
+}
+
+/*
+ * Calculate the number of frames to process per iteration.
+ * Higher sample rates require larger steps to maintain efficiency.
+ */
+static unsigned
+frame_stepping(unsigned sample_rate)
+{
+ return (16U * (1U + (sample_rate / 50000U)));
+}
+
+/*
+ * Update the mmap pointer and calculate progress.
+ * Returns the absolute progress in bytes.
+ *
+ * fd: file descriptor for the audio device
+ * request: ioctl request (SNDCTL_DSP_GETIPTR or SNDCTL_DSP_GETOPTR)
+ * map_pointer: current pointer position in the ring buffer
+ * map_progress: absolute progress in bytes
+ * buffer_bytes: total size of the ring buffer
+ * frag_size: size of each fragment
+ * frame_size: size of one audio frame in bytes
+ */
+static int64_t
+update_map_progress(int fd, unsigned long request, int *map_pointer,
+ int64_t *map_progress, int buffer_bytes, int frag_size, int frame_size)
+{
+ count_info info = {};
+ unsigned delta, max_bytes, cycles;
+ int fragments;
+
+ if (ioctl(fd, request, &info) < 0)
+ err(1, "Failed to get mmap pointer");
+ if (info.ptr < 0 || info.ptr >= buffer_bytes)
+ errx(1, "Pointer out of bounds: %d", info.ptr);
+ if ((info.ptr % frame_size) != 0)
+ errx(1, "Pointer %d not aligned to frame size %d", info.ptr,
+ frame_size);
+ if (info.blocks < 0)
+ errx(1, "Invalid block count %d", info.blocks);
+
+ /*
+ * Calculate delta: how many bytes have been processed since last check.
+ * Handle ring buffer wraparound using modulo arithmetic.
+ */
+ delta = (info.ptr + buffer_bytes - *map_pointer) % buffer_bytes;
+
+ /*
+ * Adjust delta based on reported blocks available.
+ * This accounts for cases where the pointer has wrapped multiple times.
+ */
+ max_bytes = (info.blocks + 1) * frag_size - 1;
+ if (max_bytes >= delta) {
+ cycles = max_bytes - delta;
+ cycles -= cycles % buffer_bytes;
+ delta += cycles;
+ }
+
+ /* Verify fragment count matches expected value */
+ fragments = delta / frag_size;
+ if (info.blocks < fragments || info.blocks > fragments + 1)
+ warnx("Pointer block mismatch: ptr=%d blocks=%d delta=%u",
+ info.ptr, info.blocks, delta);
+
+ /* Update pointer and progress tracking */
+ *map_pointer = info.ptr;
+ *map_progress += delta;
+ return (*map_progress);
+}
+
+/*
+ * Copy data between ring buffers, handling wraparound.
+ * The copy starts at 'offset' and copies 'length' bytes.
+ * If the copy crosses the buffer boundary, it wraps to the beginning.
+ */
+static void
+copy_ring(void *dstv, const void *srcv, int buffer_bytes, int offset,
+ int length)
+{
+ uint8_t *dst = dstv;
+ const uint8_t *src = srcv;
+ int first;
+
+ if (length <= 0)
+ return;
+
+ /* Calculate bytes to copy before wraparound */
+ first = buffer_bytes - offset;
+ if (first > length)
+ first = length;
+
+ /* Copy first part (up to buffer end or length) */
+ memcpy(dst + offset, src + offset, first);
+
+ /* Copy remaining part from beginning of buffer if needed */
+ if (first < length)
+ memcpy(dst, src, length - first);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ch, bytes;
+ int frag_size, frame_size, verbose = 0;
+ int map_pointer = 0;
+ unsigned step_frames;
+ int64_t frame_ns, start_ns, next_wakeup_ns;
+ int64_t read_progress = 0, write_progress = 0;
+ oss_syncgroup sync_group = { 0, 0, { 0 } };
+ struct config config_in = {
+ .device = "/dev/dsp",
+ .mode = O_RDONLY | O_EXCL | O_NONBLOCK,
+ .format = AFMT_S32_NE,
+ .sample_rate = 48000,
+ .mmap = 1,
+ };
+ struct config config_out = {
+ .device = "/dev/dsp",
+ .mode = O_WRONLY | O_EXCL | O_NONBLOCK,
+ .format = AFMT_S32_NE,
+ .sample_rate = 48000,
+ .mmap = 1,
+ };
+
+ while ((ch = getopt(argc, argv, "v")) != -1) {
+ switch (ch) {
+ case 'v':
+ verbose = 1;
+ break;
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (!verbose)
+ printf("Use -v for verbose mode\n");
+
+ oss_init(&config_in);
+ oss_init(&config_out);
+
+ /*
+ * Verify input and output have matching ring-buffer geometry.
+ * The passthrough loop copies raw bytes at the same offset in both mmap
+ * buffers, so both devices must expose the same total byte count.
+ * They must also use the same max_channels because frame_size is
+ * derived from that value and all mmap pointers/lengths are expected to
+ * stay aligned to whole frames on both sides. If channels differed, the
+ * same byte offset could land in the middle of a frame on one device.
+ */
+ if (config_in.buffer_info.bytes != config_out.buffer_info.bytes)
+ errx(1,
+ "Input and output configurations have different buffer sizes");
+ if (config_in.audio_info.max_channels !=
+ config_out.audio_info.max_channels)
+ errx(1,
+ "Input and output configurations have different number of channels");
+
+ bytes = config_in.buffer_info.bytes;
+ frag_size = config_in.buffer_info.fragsize;
+ frame_size = config_in.sample_size * config_in.audio_info.max_channels;
+ if (frag_size != config_out.buffer_info.fragsize)
+ errx(1,
+ "Input and output configurations have different fragment sizes");
+
+ /* Calculate timing parameters */
+ step_frames = frame_stepping(config_in.sample_rate);
+ frame_ns = 1000000000LL / config_in.sample_rate;
+
+ /* Clear output buffer to prevent noise on startup */
+ memset(config_out.buf, 0, bytes);
+
+ /* Configure and start sync group */
+ sync_group.mode = PCM_ENABLE_INPUT;
+ if (ioctl(config_in.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
+ err(1, "Failed to add input to syncgroup");
+ sync_group.mode = PCM_ENABLE_OUTPUT;
+ if (ioctl(config_out.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
+ err(1, "Failed to add output to syncgroup");
+ if (ioctl(config_in.fd, SNDCTL_DSP_SYNCSTART, &sync_group.id) < 0)
+ err(1, "Starting sync group failed");
+
+ /* Initialize timing and progress tracking */
+ start_ns = gettime_ns();
+ read_progress = update_map_progress(config_in.fd, SNDCTL_DSP_GETIPTR,
+ &map_pointer, &read_progress, bytes, frag_size, frame_size);
+ write_progress = read_progress;
+ next_wakeup_ns = start_ns;
+
+ /*
+ * Main processing loop:
+ * 1. Sleep until next scheduled wakeup
+ * 2. Check how much new audio data is available
+ * 3. Copy available data from input to output buffer
+ * 4. Schedule next wakeup
+ */
+ for (;;) {
+ sleep_until_ns(next_wakeup_ns);
+ read_progress = update_map_progress(config_in.fd,
+ SNDCTL_DSP_GETIPTR, &map_pointer, &read_progress, bytes,
+ frag_size, frame_size);
+
+ /* Copy new audio data if available */
+ if (read_progress > write_progress) {
+ int offset = write_progress % bytes;
+ int length = read_progress - write_progress;
+
+ copy_ring(config_out.buf, config_in.buf, bytes, offset,
+ length);
+ write_progress = read_progress;
+ if (verbose)
+ printf("copied %d bytes at %d (abs %lld)\n",
+ length, offset, (long long)write_progress);
+ }
+
+ /* Schedule next wakeup based on frame timing */
+ next_wakeup_ns += (int64_t)step_frames * frame_ns;
+ if (next_wakeup_ns < gettime_ns())
+ next_wakeup_ns = gettime_ns();
+ }
+
+ if (munmap(config_in.buf, bytes) != 0)
+ err(1, "Memory unmap failed");
+ config_in.buf = NULL;
+ if (munmap(config_out.buf, bytes) != 0)
+ err(1, "Memory unmap failed");
+ config_out.buf = NULL;
+ close(config_in.fd);
+ close(config_out.fd);
+
+ return (0);
+}