xref: /freebsd/share/examples/sound/mmap.c (revision 3524d4ebbe1f562dd76dc553c085386aadfd2682)
1*3524d4ebSGoran Mekić /*
2*3524d4ebSGoran Mekić  * SPDX-License-Identifier: BSD-2-Clause
3*3524d4ebSGoran Mekić  *
4*3524d4ebSGoran Mekić  * Copyright (c) 2026 Goran Mekić
5*3524d4ebSGoran Mekić  *
6*3524d4ebSGoran Mekić  * Redistribution and use in source and binary forms, with or without
7*3524d4ebSGoran Mekić  * modification, are permitted provided that the following conditions
8*3524d4ebSGoran Mekić  * are met:
9*3524d4ebSGoran Mekić  * 1. Redistributions of source code must retain the above copyright
10*3524d4ebSGoran Mekić  *    notice, this list of conditions and the following disclaimer.
11*3524d4ebSGoran Mekić  * 2. Redistributions in binary form must reproduce the above copyright
12*3524d4ebSGoran Mekić  *    notice, this list of conditions and the following disclaimer in the
13*3524d4ebSGoran Mekić  *    documentation and/or other materials provided with the distribution.
14*3524d4ebSGoran Mekić  *
15*3524d4ebSGoran Mekić  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16*3524d4ebSGoran Mekić  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17*3524d4ebSGoran Mekić  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18*3524d4ebSGoran Mekić  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19*3524d4ebSGoran Mekić  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20*3524d4ebSGoran Mekić  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21*3524d4ebSGoran Mekić  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22*3524d4ebSGoran Mekić  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23*3524d4ebSGoran Mekić  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24*3524d4ebSGoran Mekić  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25*3524d4ebSGoran Mekić  * SUCH DAMAGE.
26*3524d4ebSGoran Mekić  */
27*3524d4ebSGoran Mekić 
28*3524d4ebSGoran Mekić /*
29*3524d4ebSGoran Mekić  * This program demonstrates low-latency audio pass-through using mmap.
30*3524d4ebSGoran Mekić  * Opens input and output audio devices using memory-mapped I/O,
31*3524d4ebSGoran Mekić  * synchronizes them in a sync group for simultaneous start,
32*3524d4ebSGoran Mekić  * then continuously copies audio data from input to output.
33*3524d4ebSGoran Mekić  */
34*3524d4ebSGoran Mekić 
35*3524d4ebSGoran Mekić #include <time.h>
36*3524d4ebSGoran Mekić 
37*3524d4ebSGoran Mekić #include "oss.h"
38*3524d4ebSGoran Mekić 
39*3524d4ebSGoran Mekić /*
40*3524d4ebSGoran Mekić  * Get current time in nanoseconds using monotonic clock.
41*3524d4ebSGoran Mekić  * Monotonic clock is not affected by system time changes.
42*3524d4ebSGoran Mekić  */
43*3524d4ebSGoran Mekić static int64_t
gettime_ns(void)44*3524d4ebSGoran Mekić gettime_ns(void)
45*3524d4ebSGoran Mekić {
46*3524d4ebSGoran Mekić 	struct timespec ts;
47*3524d4ebSGoran Mekić 
48*3524d4ebSGoran Mekić 	if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
49*3524d4ebSGoran Mekić 		err(1, "clock_gettime failed");
50*3524d4ebSGoran Mekić 	return ((int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec);
51*3524d4ebSGoran Mekić }
52*3524d4ebSGoran Mekić 
53*3524d4ebSGoran Mekić /*
54*3524d4ebSGoran Mekić  * Sleep until the specified absolute time (in nanoseconds).
55*3524d4ebSGoran Mekić  * Uses TIMER_ABSTIME for precise timing synchronization.
56*3524d4ebSGoran Mekić  */
57*3524d4ebSGoran Mekić static void
sleep_until_ns(int64_t target_ns)58*3524d4ebSGoran Mekić sleep_until_ns(int64_t target_ns)
59*3524d4ebSGoran Mekić {
60*3524d4ebSGoran Mekić 	struct timespec ts;
61*3524d4ebSGoran Mekić 
62*3524d4ebSGoran Mekić 	ts.tv_sec = target_ns / 1000000000LL;
63*3524d4ebSGoran Mekić 	ts.tv_nsec = target_ns % 1000000000LL;
64*3524d4ebSGoran Mekić 	if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) != 0)
65*3524d4ebSGoran Mekić 		err(1, "clock_nanosleep failed");
66*3524d4ebSGoran Mekić }
67*3524d4ebSGoran Mekić 
68*3524d4ebSGoran Mekić /*
69*3524d4ebSGoran Mekić  * Calculate the number of frames to process per iteration.
70*3524d4ebSGoran Mekić  * Higher sample rates require larger steps to maintain efficiency.
71*3524d4ebSGoran Mekić  */
72*3524d4ebSGoran Mekić static unsigned
frame_stepping(unsigned sample_rate)73*3524d4ebSGoran Mekić frame_stepping(unsigned sample_rate)
74*3524d4ebSGoran Mekić {
75*3524d4ebSGoran Mekić 	return (16U * (1U + (sample_rate / 50000U)));
76*3524d4ebSGoran Mekić }
77*3524d4ebSGoran Mekić 
78*3524d4ebSGoran Mekić /*
79*3524d4ebSGoran Mekić  * Update the mmap pointer and calculate progress.
80*3524d4ebSGoran Mekić  * Returns the absolute progress in bytes.
81*3524d4ebSGoran Mekić  *
82*3524d4ebSGoran Mekić  * fd: file descriptor for the audio device
83*3524d4ebSGoran Mekić  * request: ioctl request (SNDCTL_DSP_GETIPTR or SNDCTL_DSP_GETOPTR)
84*3524d4ebSGoran Mekić  * map_pointer: current pointer position in the ring buffer
85*3524d4ebSGoran Mekić  * map_progress: absolute progress in bytes
86*3524d4ebSGoran Mekić  * buffer_bytes: total size of the ring buffer
87*3524d4ebSGoran Mekić  * frag_size: size of each fragment
88*3524d4ebSGoran Mekić  * frame_size: size of one audio frame in bytes
89*3524d4ebSGoran Mekić  */
90*3524d4ebSGoran Mekić 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)91*3524d4ebSGoran Mekić update_map_progress(int fd, unsigned long request, int *map_pointer,
92*3524d4ebSGoran Mekić     int64_t *map_progress, int buffer_bytes, int frag_size, int frame_size)
93*3524d4ebSGoran Mekić {
94*3524d4ebSGoran Mekić 	count_info info = {};
95*3524d4ebSGoran Mekić 	unsigned delta, max_bytes, cycles;
96*3524d4ebSGoran Mekić 	int fragments;
97*3524d4ebSGoran Mekić 
98*3524d4ebSGoran Mekić 	if (ioctl(fd, request, &info) < 0)
99*3524d4ebSGoran Mekić 		err(1, "Failed to get mmap pointer");
100*3524d4ebSGoran Mekić 	if (info.ptr < 0 || info.ptr >= buffer_bytes)
101*3524d4ebSGoran Mekić 		errx(1, "Pointer out of bounds: %d", info.ptr);
102*3524d4ebSGoran Mekić 	if ((info.ptr % frame_size) != 0)
103*3524d4ebSGoran Mekić 		errx(1, "Pointer %d not aligned to frame size %d", info.ptr,
104*3524d4ebSGoran Mekić 		    frame_size);
105*3524d4ebSGoran Mekić 	if (info.blocks < 0)
106*3524d4ebSGoran Mekić 		errx(1, "Invalid block count %d", info.blocks);
107*3524d4ebSGoran Mekić 
108*3524d4ebSGoran Mekić 	/*
109*3524d4ebSGoran Mekić 	 * Calculate delta: how many bytes have been processed since last check.
110*3524d4ebSGoran Mekić 	 * Handle ring buffer wraparound using modulo arithmetic.
111*3524d4ebSGoran Mekić 	 */
112*3524d4ebSGoran Mekić 	delta = (info.ptr + buffer_bytes - *map_pointer) % buffer_bytes;
113*3524d4ebSGoran Mekić 
114*3524d4ebSGoran Mekić 	/*
115*3524d4ebSGoran Mekić 	 * Adjust delta based on reported blocks available.
116*3524d4ebSGoran Mekić 	 * This accounts for cases where the pointer has wrapped multiple times.
117*3524d4ebSGoran Mekić 	 */
118*3524d4ebSGoran Mekić 	max_bytes = (info.blocks + 1) * frag_size - 1;
119*3524d4ebSGoran Mekić 	if (max_bytes >= delta) {
120*3524d4ebSGoran Mekić 		cycles = max_bytes - delta;
121*3524d4ebSGoran Mekić 		cycles -= cycles % buffer_bytes;
122*3524d4ebSGoran Mekić 		delta += cycles;
123*3524d4ebSGoran Mekić 	}
124*3524d4ebSGoran Mekić 
125*3524d4ebSGoran Mekić 	/* Verify fragment count matches expected value */
126*3524d4ebSGoran Mekić 	fragments = delta / frag_size;
127*3524d4ebSGoran Mekić 	if (info.blocks < fragments || info.blocks > fragments + 1)
128*3524d4ebSGoran Mekić 		warnx("Pointer block mismatch: ptr=%d blocks=%d delta=%u",
129*3524d4ebSGoran Mekić 		    info.ptr, info.blocks, delta);
130*3524d4ebSGoran Mekić 
131*3524d4ebSGoran Mekić 	/* Update pointer and progress tracking */
132*3524d4ebSGoran Mekić 	*map_pointer = info.ptr;
133*3524d4ebSGoran Mekić 	*map_progress += delta;
134*3524d4ebSGoran Mekić 	return (*map_progress);
135*3524d4ebSGoran Mekić }
136*3524d4ebSGoran Mekić 
137*3524d4ebSGoran Mekić /*
138*3524d4ebSGoran Mekić  * Copy data between ring buffers, handling wraparound.
139*3524d4ebSGoran Mekić  * The copy starts at 'offset' and copies 'length' bytes.
140*3524d4ebSGoran Mekić  * If the copy crosses the buffer boundary, it wraps to the beginning.
141*3524d4ebSGoran Mekić  */
142*3524d4ebSGoran Mekić static void
copy_ring(void * dstv,const void * srcv,int buffer_bytes,int offset,int length)143*3524d4ebSGoran Mekić copy_ring(void *dstv, const void *srcv, int buffer_bytes, int offset,
144*3524d4ebSGoran Mekić     int length)
145*3524d4ebSGoran Mekić {
146*3524d4ebSGoran Mekić 	uint8_t *dst = dstv;
147*3524d4ebSGoran Mekić 	const uint8_t *src = srcv;
148*3524d4ebSGoran Mekić 	int first;
149*3524d4ebSGoran Mekić 
150*3524d4ebSGoran Mekić 	if (length <= 0)
151*3524d4ebSGoran Mekić 		return;
152*3524d4ebSGoran Mekić 
153*3524d4ebSGoran Mekić 	/* Calculate bytes to copy before wraparound */
154*3524d4ebSGoran Mekić 	first = buffer_bytes - offset;
155*3524d4ebSGoran Mekić 	if (first > length)
156*3524d4ebSGoran Mekić 		first = length;
157*3524d4ebSGoran Mekić 
158*3524d4ebSGoran Mekić 	/* Copy first part (up to buffer end or length) */
159*3524d4ebSGoran Mekić 	memcpy(dst + offset, src + offset, first);
160*3524d4ebSGoran Mekić 
161*3524d4ebSGoran Mekić 	/* Copy remaining part from beginning of buffer if needed */
162*3524d4ebSGoran Mekić 	if (first < length)
163*3524d4ebSGoran Mekić 		memcpy(dst, src, length - first);
164*3524d4ebSGoran Mekić }
165*3524d4ebSGoran Mekić 
166*3524d4ebSGoran Mekić int
main(int argc,char * argv[])167*3524d4ebSGoran Mekić main(int argc, char *argv[])
168*3524d4ebSGoran Mekić {
169*3524d4ebSGoran Mekić 	int ch, bytes;
170*3524d4ebSGoran Mekić 	int frag_size, frame_size, verbose = 0;
171*3524d4ebSGoran Mekić 	int map_pointer = 0;
172*3524d4ebSGoran Mekić 	unsigned step_frames;
173*3524d4ebSGoran Mekić 	int64_t frame_ns, start_ns, next_wakeup_ns;
174*3524d4ebSGoran Mekić 	int64_t read_progress = 0, write_progress = 0;
175*3524d4ebSGoran Mekić 	oss_syncgroup sync_group = { 0, 0, { 0 } };
176*3524d4ebSGoran Mekić 	struct config config_in = {
177*3524d4ebSGoran Mekić 		.device = "/dev/dsp",
178*3524d4ebSGoran Mekić 		.mode = O_RDONLY | O_EXCL | O_NONBLOCK,
179*3524d4ebSGoran Mekić 		.format = AFMT_S32_NE,
180*3524d4ebSGoran Mekić 		.sample_rate = 48000,
181*3524d4ebSGoran Mekić 		.mmap = 1,
182*3524d4ebSGoran Mekić 	};
183*3524d4ebSGoran Mekić 	struct config config_out = {
184*3524d4ebSGoran Mekić 		.device = "/dev/dsp",
185*3524d4ebSGoran Mekić 		.mode = O_WRONLY | O_EXCL | O_NONBLOCK,
186*3524d4ebSGoran Mekić 		.format = AFMT_S32_NE,
187*3524d4ebSGoran Mekić 		.sample_rate = 48000,
188*3524d4ebSGoran Mekić 		.mmap = 1,
189*3524d4ebSGoran Mekić 	};
190*3524d4ebSGoran Mekić 
191*3524d4ebSGoran Mekić 	while ((ch = getopt(argc, argv, "v")) != -1) {
192*3524d4ebSGoran Mekić 		switch (ch) {
193*3524d4ebSGoran Mekić 		case 'v':
194*3524d4ebSGoran Mekić 			verbose = 1;
195*3524d4ebSGoran Mekić 			break;
196*3524d4ebSGoran Mekić 		}
197*3524d4ebSGoran Mekić 	}
198*3524d4ebSGoran Mekić 	argc -= optind;
199*3524d4ebSGoran Mekić 	argv += optind;
200*3524d4ebSGoran Mekić 
201*3524d4ebSGoran Mekić 	if (!verbose)
202*3524d4ebSGoran Mekić 		printf("Use -v for verbose mode\n");
203*3524d4ebSGoran Mekić 
204*3524d4ebSGoran Mekić 	oss_init(&config_in);
205*3524d4ebSGoran Mekić 	oss_init(&config_out);
206*3524d4ebSGoran Mekić 
207*3524d4ebSGoran Mekić 	/*
208*3524d4ebSGoran Mekić 	 * Verify input and output have matching ring-buffer geometry.
209*3524d4ebSGoran Mekić 	 * The passthrough loop copies raw bytes at the same offset in both mmap
210*3524d4ebSGoran Mekić 	 * buffers, so both devices must expose the same total byte count.
211*3524d4ebSGoran Mekić 	 * They must also use the same max_channels because frame_size is
212*3524d4ebSGoran Mekić 	 * derived from that value and all mmap pointers/lengths are expected to
213*3524d4ebSGoran Mekić 	 * stay aligned to whole frames on both sides. If channels differed, the
214*3524d4ebSGoran Mekić 	 * same byte offset could land in the middle of a frame on one device.
215*3524d4ebSGoran Mekić 	 */
216*3524d4ebSGoran Mekić 	if (config_in.buffer_info.bytes != config_out.buffer_info.bytes)
217*3524d4ebSGoran Mekić 		errx(1,
218*3524d4ebSGoran Mekić 		    "Input and output configurations have different buffer sizes");
219*3524d4ebSGoran Mekić 	if (config_in.audio_info.max_channels !=
220*3524d4ebSGoran Mekić 	    config_out.audio_info.max_channels)
221*3524d4ebSGoran Mekić 		errx(1,
222*3524d4ebSGoran Mekić 		    "Input and output configurations have different number of channels");
223*3524d4ebSGoran Mekić 
224*3524d4ebSGoran Mekić 	bytes = config_in.buffer_info.bytes;
225*3524d4ebSGoran Mekić 	frag_size = config_in.buffer_info.fragsize;
226*3524d4ebSGoran Mekić 	frame_size = config_in.sample_size * config_in.audio_info.max_channels;
227*3524d4ebSGoran Mekić 	if (frag_size != config_out.buffer_info.fragsize)
228*3524d4ebSGoran Mekić 		errx(1,
229*3524d4ebSGoran Mekić 		    "Input and output configurations have different fragment sizes");
230*3524d4ebSGoran Mekić 
231*3524d4ebSGoran Mekić 	/* Calculate timing parameters */
232*3524d4ebSGoran Mekić 	step_frames = frame_stepping(config_in.sample_rate);
233*3524d4ebSGoran Mekić 	frame_ns = 1000000000LL / config_in.sample_rate;
234*3524d4ebSGoran Mekić 
235*3524d4ebSGoran Mekić 	/* Clear output buffer to prevent noise on startup */
236*3524d4ebSGoran Mekić 	memset(config_out.buf, 0, bytes);
237*3524d4ebSGoran Mekić 
238*3524d4ebSGoran Mekić 	/* Configure and start sync group */
239*3524d4ebSGoran Mekić 	sync_group.mode = PCM_ENABLE_INPUT;
240*3524d4ebSGoran Mekić 	if (ioctl(config_in.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
241*3524d4ebSGoran Mekić 		err(1, "Failed to add input to syncgroup");
242*3524d4ebSGoran Mekić 	sync_group.mode = PCM_ENABLE_OUTPUT;
243*3524d4ebSGoran Mekić 	if (ioctl(config_out.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
244*3524d4ebSGoran Mekić 		err(1, "Failed to add output to syncgroup");
245*3524d4ebSGoran Mekić 	if (ioctl(config_in.fd, SNDCTL_DSP_SYNCSTART, &sync_group.id) < 0)
246*3524d4ebSGoran Mekić 		err(1, "Starting sync group failed");
247*3524d4ebSGoran Mekić 
248*3524d4ebSGoran Mekić 	/* Initialize timing and progress tracking */
249*3524d4ebSGoran Mekić 	start_ns = gettime_ns();
250*3524d4ebSGoran Mekić 	read_progress = update_map_progress(config_in.fd, SNDCTL_DSP_GETIPTR,
251*3524d4ebSGoran Mekić 	    &map_pointer, &read_progress, bytes, frag_size, frame_size);
252*3524d4ebSGoran Mekić 	write_progress = read_progress;
253*3524d4ebSGoran Mekić 	next_wakeup_ns = start_ns;
254*3524d4ebSGoran Mekić 
255*3524d4ebSGoran Mekić 	/*
256*3524d4ebSGoran Mekić 	 * Main processing loop:
257*3524d4ebSGoran Mekić 	 * 1. Sleep until next scheduled wakeup
258*3524d4ebSGoran Mekić 	 * 2. Check how much new audio data is available
259*3524d4ebSGoran Mekić 	 * 3. Copy available data from input to output buffer
260*3524d4ebSGoran Mekić 	 * 4. Schedule next wakeup
261*3524d4ebSGoran Mekić 	 */
262*3524d4ebSGoran Mekić 	for (;;) {
263*3524d4ebSGoran Mekić 		sleep_until_ns(next_wakeup_ns);
264*3524d4ebSGoran Mekić 		read_progress = update_map_progress(config_in.fd,
265*3524d4ebSGoran Mekić 		    SNDCTL_DSP_GETIPTR, &map_pointer, &read_progress, bytes,
266*3524d4ebSGoran Mekić 		    frag_size, frame_size);
267*3524d4ebSGoran Mekić 
268*3524d4ebSGoran Mekić 		/* Copy new audio data if available */
269*3524d4ebSGoran Mekić 		if (read_progress > write_progress) {
270*3524d4ebSGoran Mekić 			int offset = write_progress % bytes;
271*3524d4ebSGoran Mekić 			int length = read_progress - write_progress;
272*3524d4ebSGoran Mekić 
273*3524d4ebSGoran Mekić 			copy_ring(config_out.buf, config_in.buf, bytes, offset,
274*3524d4ebSGoran Mekić 			    length);
275*3524d4ebSGoran Mekić 			write_progress = read_progress;
276*3524d4ebSGoran Mekić 			if (verbose)
277*3524d4ebSGoran Mekić 				printf("copied %d bytes at %d (abs %lld)\n",
278*3524d4ebSGoran Mekić 				    length, offset, (long long)write_progress);
279*3524d4ebSGoran Mekić 		}
280*3524d4ebSGoran Mekić 
281*3524d4ebSGoran Mekić 		/* Schedule next wakeup based on frame timing */
282*3524d4ebSGoran Mekić 		next_wakeup_ns += (int64_t)step_frames * frame_ns;
283*3524d4ebSGoran Mekić 		if (next_wakeup_ns < gettime_ns())
284*3524d4ebSGoran Mekić 			next_wakeup_ns = gettime_ns();
285*3524d4ebSGoran Mekić 	}
286*3524d4ebSGoran Mekić 
287*3524d4ebSGoran Mekić 	if (munmap(config_in.buf, bytes) != 0)
288*3524d4ebSGoran Mekić 		err(1, "Memory unmap failed");
289*3524d4ebSGoran Mekić 	config_in.buf = NULL;
290*3524d4ebSGoran Mekić 	if (munmap(config_out.buf, bytes) != 0)
291*3524d4ebSGoran Mekić 		err(1, "Memory unmap failed");
292*3524d4ebSGoran Mekić 	config_out.buf = NULL;
293*3524d4ebSGoran Mekić 	close(config_in.fd);
294*3524d4ebSGoran Mekić 	close(config_out.fd);
295*3524d4ebSGoran Mekić 
296*3524d4ebSGoran Mekić 	return (0);
297*3524d4ebSGoran Mekić }
298