xref: /freebsd/share/examples/sound/mmap.c (revision a48bbef5eb32508a8d7b3b986c9b1d28176d1694)
1 /*
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2026 Goran Mekić
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  */
27 
28 /*
29  * This program demonstrates low-latency audio pass-through using mmap
30  * and kqueue.  It opens input and output audio devices using memory-
31  * mapped I/O, synchronizes them in a sync group for simultaneous start,
32  * then continuously copies audio data from input to output.  Buffer
33  * positions are obtained from kqueue's ext[0] (replacing GETIPTR/
34  * GETOPTR ioctls) and error counters from ext[1] (replacing GETERROR).
35  */
36 
37 #include <sys/event.h>
38 
39 #include "oss.h"
40 
41 /*
42  * Copy data between ring buffers, handling wraparound.
43  * The copy starts at 'offset' and copies 'length' bytes.
44  * If the copy crosses the buffer boundary, it wraps to the beginning.
45  */
46 static void
copy_ring(void * dstv,const void * srcv,int buffer_bytes,int offset,int length)47 copy_ring(void *dstv, const void *srcv, int buffer_bytes, int offset,
48     int length)
49 {
50 	uint8_t *dst = dstv;
51 	const uint8_t *src = srcv;
52 	int first;
53 
54 	if (length <= 0)
55 		return;
56 
57 	/* Calculate bytes to copy before wraparound */
58 	first = buffer_bytes - offset;
59 	if (first > length)
60 		first = length;
61 
62 	/* Copy first part (up to buffer end or length) */
63 	memcpy(dst + offset, src + offset, first);
64 
65 	/* Copy remaining part from beginning of buffer if needed */
66 	if (first < length)
67 		memcpy(dst, src, length - first);
68 }
69 
70 int
main(int argc,char * argv[])71 main(int argc, char *argv[])
72 {
73 	int ch, bytes;
74 	int frag_size, frame_size, verbose = 0;
75 	int map_pointer = 0;
76 	int64_t read_progress = 0, write_progress = 0;
77 	oss_syncgroup sync_group = { 0, 0, { 0 } };
78 	struct config config_in = {
79 		.device = "/dev/dsp",
80 		.mode = O_RDONLY | O_EXCL | O_NONBLOCK,
81 		.format = AFMT_S32_NE,
82 		.sample_rate = 48000,
83 		.mmap = 1,
84 	};
85 	struct config config_out = {
86 		.device = "/dev/dsp",
87 		.mode = O_WRONLY | O_EXCL | O_NONBLOCK,
88 		.format = AFMT_S32_NE,
89 		.sample_rate = 48000,
90 		.mmap = 1,
91 	};
92 	struct kevent ev;
93 	int kq;
94 
95 	while ((ch = getopt(argc, argv, "v")) != -1) {
96 		switch (ch) {
97 		case 'v':
98 			verbose = 1;
99 			break;
100 		}
101 	}
102 	argc -= optind;
103 	argv += optind;
104 
105 	if (!verbose)
106 		printf("Use -v for verbose mode\n");
107 
108 	oss_init(&config_in);
109 	oss_init(&config_out);
110 
111 	/*
112 	 * Verify input and output have matching ring-buffer geometry.
113 	 * The passthrough loop copies raw bytes at the same offset in both mmap
114 	 * buffers, so both devices must expose the same total byte count.
115 	 * They must also use the same max_channels because frame_size is
116 	 * derived from that value and all mmap pointers/lengths are expected to
117 	 * stay aligned to whole frames on both sides. If channels differed, the
118 	 * same byte offset could land in the middle of a frame on one device.
119 	 */
120 	if (config_in.buffer_info.bytes != config_out.buffer_info.bytes)
121 		errx(1,
122 		    "Input and output configurations have different buffer sizes");
123 	if (config_in.audio_info.max_channels !=
124 	    config_out.audio_info.max_channels)
125 		errx(1,
126 		    "Input and output configurations have different number of channels");
127 
128 	bytes = config_in.buffer_info.bytes;
129 	frag_size = config_in.buffer_info.fragsize;
130 	frame_size = config_in.sample_size * config_in.audio_info.max_channels;
131 	if (frag_size != config_out.buffer_info.fragsize)
132 		errx(1,
133 		    "Input and output configurations have different fragment sizes");
134 
135 	/* Clear output buffer to prevent noise on startup */
136 	memset(config_out.buf, 0, bytes);
137 
138 	/* Configure and start sync group */
139 	sync_group.mode = PCM_ENABLE_INPUT;
140 	if (ioctl(config_in.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
141 		err(1, "Failed to add input to syncgroup");
142 	sync_group.mode = PCM_ENABLE_OUTPUT;
143 	if (ioctl(config_out.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0)
144 		err(1, "Failed to add output to syncgroup");
145 	if (ioctl(config_in.fd, SNDCTL_DSP_SYNCSTART, &sync_group.id) < 0)
146 		err(1, "Starting sync group failed");
147 
148 	/* Create kqueue and register input device for read events */
149 	kq = kqueue();
150 	if (kq < 0)
151 		err(1, "kqueue failed");
152 	EV_SET(&ev, config_in.fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
153 	if (kevent(kq, &ev, 1, NULL, 0, NULL) < 0)
154 		err(1, "kevent register failed");
155 
156 	/*
157 	 * Main processing loop:
158 	 * Block on kevent() until input data is available.
159 	 * ext[0] holds the current DMA pointer (GETIPTR/GETOPTR equivalent).
160 	 * ext[1] holds the xrun count for the channel (GETERROR equivalent).
161 	 */
162 	for (;;) {
163 		int n;
164 		int ptr;
165 		unsigned delta;
166 
167 		n = kevent(kq, NULL, 0, &ev, 1, NULL);
168 		if (n < 0)
169 			err(1, "kevent failed");
170 		if (n == 0)
171 			continue;
172 
173 		ptr = (int)ev.ext[0];
174 		if (ptr < 0 || ptr >= bytes)
175 			errx(1, "Pointer out of bounds: %d", ptr);
176 		if ((ptr % frame_size) != 0)
177 			errx(1, "Pointer %d not aligned to frame size %d", ptr,
178 			    frame_size);
179 
180 		/*
181 		 * Calculate delta: how many bytes have been processed since
182 		 * last check. Handle ring buffer wraparound.
183 		 */
184 		delta = (ptr + bytes - map_pointer) % bytes;
185 
186 		/* Update pointer and progress tracking */
187 		map_pointer = ptr;
188 		read_progress += delta;
189 
190 		/* Report xruns if any */
191 		if (ev.ext[1] != 0 && verbose)
192 			warnx("xruns: %llu", (unsigned long long)ev.ext[1]);
193 
194 		/* Copy new audio data if available */
195 		if (read_progress > write_progress) {
196 			int offset = write_progress % bytes;
197 			int length = read_progress - write_progress;
198 
199 			copy_ring(config_out.buf, config_in.buf, bytes, offset,
200 			    length);
201 			write_progress = read_progress;
202 			if (verbose)
203 				printf("copied %d bytes at %d (abs %lld)\n",
204 				    length, offset, (long long)write_progress);
205 		}
206 	}
207 
208 	close(kq);
209 	if (munmap(config_in.buf, bytes) != 0)
210 		err(1, "Memory unmap failed");
211 	config_in.buf = NULL;
212 	if (munmap(config_out.buf, bytes) != 0)
213 		err(1, "Memory unmap failed");
214 	config_out.buf = NULL;
215 	close(config_in.fd);
216 	close(config_out.fd);
217 
218 	return (0);
219 }
220