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