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