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