From nobody Tue Apr 14 10:59:18 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4fw1Vt4Ty8z6ZcDM for ; Tue, 14 Apr 2026 10:59:18 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4fw1Vt2BpCz3XtF for ; Tue, 14 Apr 2026 10:59:18 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1776164358; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=K6hatQ3XTpWRxLyk6iMutk0D9dtVVeON6u+iIyJGsag=; b=lxVxM1a/42Y6NYpOVYoLIW4MxYuRxs24a6RNS0KP+l/UHYByBnuGV6RgyfsnkyhvT2Dr4R cZ7G3N6weba9E/+7h8sAmNl0l9FqLOE3Pac+V5NfpZ8DGivCmUkTVnEa0fCVC4gj0F7JQz FMXRyvi809SG/4mZY/Tlju88Io5Swk76LkyOlJ6KAlvAnyM4lMNAB5IJZAP9T0J1HM19oh hKiGDKc4m73smRw4ZPgDltErFedJWH70NecucrFNtD/wlAGrNcdYUrDDBJ+lcgZ06GWlaO cSYHJ/1j5yhyN1PTQZSkZcCLNP5TRvWF28oBC0nCqZg2o9jl/T+h9J+8WfkK0g== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1776164358; a=rsa-sha256; cv=none; b=H0kfc73epzlIXsW/NdXTHHes2szSTyPfGJ5V5YjWdTfop0SsugpTXcrxJwQDdH98N8BDTO efoSAr11Pv3Nk4FUrnVXYLzoP7C9+NzRRkM0rdt6uKRZpzA6/oXj45BAoi9dGpP9NBMO3U FigNLxllWj+vM1ajgehPiomD71JZFXOkg5oIIiPONNyFPuwPT25kuDmf0PB4Y9dTxzYpvu g1IqO9BoN1w5Cur0WeR2WmVQoLlhy17z0b5HLlUEcYS+1ByFk6B6NY69uf0s0NgRH9/uZw ExY4NFAV122adxjktgfUZ8NKXv818bITdxcmqmPA0U0ydcMf1+nw2M6NmDxRNA== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1776164358; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=K6hatQ3XTpWRxLyk6iMutk0D9dtVVeON6u+iIyJGsag=; b=cnxSLpr3Qnk1n1ZuV3xVnF63e0EbITeTrRaIHEn194zfTJja0t2OAEKbwlLHbIy+sx55Z3 qqs1IZpyqhveFXe7nX6tX9z7GGUVZ1cw2HRTQRx60SNGIiBZ7UxGBU5s46sTj8amGvnjOy 3Zd3KzKCSIxEpjf+NnE4e1roMlL7N4W6DHbNYnUU0baVhesaz8JSPH4fcasdqYys/oGCzq IOtCZ1D/UhsYG9SVnVzCqBQG1LaS4pA+I4/XuArQ6/icIEMXXcD/EJYUcikWhK1F5TjeBK S9mYZYmC7TGsmrpHI+e1KScvgbpCpSizNHayQoFMO+xfx4htNGoKcWOLkfFRMQ== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4fw1Vt13m1znTD for ; Tue, 14 Apr 2026 10:59:18 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 32f30 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Tue, 14 Apr 2026 10:59:18 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Goran=?utf-8?Q? Meki=C4=87?= From: Christos Margiolis Subject: git: 3524d4ebbe1f - main - sound examples: Add mmap example List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: christos X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 3524d4ebbe1f562dd76dc553c085386aadfd2682 Auto-Submitted: auto-generated Date: Tue, 14 Apr 2026 10:59:18 +0000 Message-Id: <69de1e06.32f30.7f998dfa@gitrepo.freebsd.org> The branch main has been updated by christos: URL: https://cgit.FreeBSD.org/src/commit/?id=3524d4ebbe1f562dd76dc553c085386aadfd2682 commit 3524d4ebbe1f562dd76dc553c085386aadfd2682 Author: Goran Mekić AuthorDate: 2026-04-14 10:57:53 +0000 Commit: Christos Margiolis 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 + +#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); +}