diff --git a/share/examples/Makefile b/share/examples/Makefile index 0174792d2ecb..d977f2e5a0da 100644 --- a/share/examples/Makefile +++ b/share/examples/Makefile @@ -1,427 +1,428 @@ # # Doing a make install builds /usr/share/examples .include PACKAGE=examples FILESDIR= ${SHAREDIR}/examples LDIRS= BSD_daemon \ FreeBSD_version \ bootforth \ csh \ etc \ find_interface \ flua \ indent \ inotify \ ipfw \ jails \ kld \ libvgl \ mdoc \ netgraph \ oci \ perfmon \ ppi \ ppp \ ses \ scsi_target \ sound \ sunrpc \ ypldap SE_DIRS+= BSD_daemon SE_BSD_DAEMON= \ FreeBSD.pfa \ README \ beastie.eps \ beastie.fig \ eps.patch \ poster.sh .if ${MACHINE_CPUARCH} == "amd64" || ${MACHINE_CPUARCH} == "aarch64" .if ${MK_BHYVE} != "no" LDIRS+= bhyve SE_DIRS+= bhyve SE_BHYVEPACKAGE=bhyve SE_BHYVE= vmrun.sh PACKAGE_bhyve/vmrun.sh= bhyve .endif .endif SE_DIRS+= FreeBSD_version SE_FREEBSD_VERSION= \ FreeBSD_version.c \ Makefile \ README SE_DIRS+= bootforth SE_BOOTFORTH_PACKAGE=bootloader SE_BOOTFORTH= \ README \ boot.4th \ frames.4th \ loader.rc \ menu.4th \ menuconf.4th \ screen.4th SE_DIRS+= csh SE_CSHPACKAGE= csh SE_CSH= dot.cshrc SE_DIRS+= etc SE_ETC= \ README.examples \ bsd-style-copyright \ make.conf SE_DIRS+= find_interface SE_FIND_INTERFACE= \ Makefile \ README \ find_interface.c SE_DIRS+= flua SE_FLUA= libjail.lua SE_DIRS+= indent SE_INDENT= indent.pro SE_DIRS+= inotify SE_INOTIFY= inotify.c \ Makefile .if ${MK_IPFILTER} != "no" SUBDIR+= ipfilter .endif SE_DIRS+= ipfw SE_IPFWPACKAGE= ipfw SE_IPFW= change_rules.sh SE_DIRS+= jails SE_JAILPACKAGE= jail SE_JAILS= \ README \ VIMAGE \ jail.xxx.conf \ jib \ jng \ rc.conf.jails \ rcjail.xxx.conf SE_DIRS+= kld SE_KLD= Makefile SE_DIRS+= kld/cdev SE_KLD_CDEV= \ Makefile \ README \ SE_DIRS+= kld/cdev/module SE_KLD_CDEV_MODULE= \ Makefile \ cdev.c \ cdev.h \ cdevmod.c SE_DIRS+= kld/cdev/test SE_KLD_CDEV_TEST= \ Makefile \ testcdev.c SE_DIRS+= kld/dyn_sysctl SE_KLD_DYN_SYSCTL= \ Makefile \ README \ dyn_sysctl.c SE_DIRS+= kld/firmware SE_KLD_FIRMWARE= \ Makefile \ README SE_DIRS+= kld/firmware/fwconsumer SE_KLD_FIRMWARE_FWCONSUMER= \ Makefile \ fw_consumer.c SE_DIRS+= kld/firmware/fwimage SE_KLD_FIRMWARE_FWIMAGE= \ Makefile \ firmware.img.uu SE_DIRS+= kld/khelp SE_KLD_KHELP= \ Makefile \ README \ h_example.c SE_DIRS+= kld/syscall SE_KLD_SYSCALL= Makefile SE_DIRS+= kld/syscall/module SE_KLD_SYSCALL_MODULE= \ Makefile \ syscall.c SE_DIRS+= kld/syscall/test SE_KLD_SYSCALL_TEST= \ Makefile \ call.c SE_DIRS+= libvgl SE_LIBVGL= \ Makefile \ demo.c SE_DIRS+= mdoc SE_MDOC= \ POSIX-copyright \ deshallify.sh \ example.1 \ example.3 \ example.4 \ example.9 SE_DIRS+= netgraph SE_NETGRAPH= \ ether.bridge \ frame_relay \ ngctl \ raw \ udp.tunnel \ virtual.chain \ virtual.lan \ SE_DIRS+= oci SE_OCI= \ README \ Containerfile.pkg SE_DIRS+= perfmon SE_PERFMON= \ Makefile \ README \ perfmon.c \ .if ${MK_PF} != "no" SE_DIRS+= pf .if ${MK_STAGING} == "no" SE_PFPACKAGE= pf SE_PF= \ ackpri \ faq-example1 \ faq-example2 \ faq-example3 \ pf.conf \ queue1 \ queue2 \ queue3 \ queue4 \ spamd .endif .endif SE_DIRS+= ppi SE_PPI= \ Makefile \ ppilcd.c SE_DIRS+= ppp SE_PPPPACKAGE= ppp SE_PPP= \ chap-auth \ login-auth \ ppp.conf.sample \ ppp.conf.span-isp \ ppp.conf.span-isp.working \ ppp.linkdown.sample \ ppp.linkdown.span-isp \ ppp.linkdown.span-isp.working \ ppp.linkup.sample \ ppp.linkup.span-isp \ ppp.linkup.span-isp.working \ ppp.secret.sample \ ppp.secret.span-isp \ ppp.secret.span-isp.working .if ${MK_LPR} != "no" LDIRS+= printing SE_DIRS+= printing SE_PRINTINGPACKAGE=lp SE_PRINTING= \ diablo-if-net \ hpdf \ hpif \ hpof \ hprf \ hpvf \ if-simple \ if-simpleX \ ifhp \ make-ps-header \ netprint \ psdf \ psdfX \ psif \ pstf \ pstfX .endif SE_DIRS+= ses SE_SES= \ Makefile \ Makefile.inc SE_DIRS+= ses/getencstat SE_SES_GETENCSTAT= \ Makefile \ getencstat.0 SE_DIRS+= ses/sesd SE_SES_SESD= \ Makefile \ sesd.0 SE_DIRS+= ses/setencstat SE_SES_SETENCSTAT= \ Makefile \ setencstat.0 SE_DIRS+= ses/setobjstat SE_SES_SETOBJSTAT= \ Makefile \ setobjstat.0 SE_DIRS+= ses/srcs SE_SES_SRCS= \ chpmon.c \ eltsub.c \ eltsub.h \ getencstat.c \ getnobj.c \ getobjmap.c \ getobjstat.c \ inienc.c \ sesd.c \ setencstat.c \ setobjstat.c SE_DIRS+= scsi_target SE_SCSI_TARGET= \ Makefile \ scsi_target.c \ scsi_target.h \ scsi_target.8 \ scsi_cmds.c SE_DIRS+= sound SE_SOUND= \ kqueue.c \ midi.c \ + mmap.c \ oss.h \ poll.c \ select.c \ simple.c \ sndstat_nv.c SE_DIRS+= sunrpc SE_SUNRPC= Makefile SE_DIRS+= sunrpc/dir SE_SUNRPC_DIR= \ Makefile \ dir.x \ dir_proc.c \ rls.c SE_DIRS+= sunrpc/msg SE_SUNRPC_MSG= \ Makefile \ msg.x \ msg_proc.c \ printmsg.c \ rprintmsg.c SE_DIRS+= sunrpc/sort SE_SUNRPC_SORT= \ Makefile \ rsort.c \ sort.x \ sort_proc.c .if ${MK_EFI} != "no" LDIRS+= uefisign SE_DIRS+= uefisign SE_UEFISIGN= uefikeys SE_UEFISIGNPACKAGE=efi-tools .endif SE_DIRS+= ypldap SE_YPLDAP= ypldap.conf SE_YPLDAPPACKAGE=yp .if ${MK_HAST} != "no" LDIRS+= hast SE_HASTPACKAGE= hast SE_DIRS+= hast SE_HAST= ucarp.sh \ ucarp_down.sh \ ucarp_up.sh \ vip-down.sh \ vip-up.sh .endif .if ${MK_USB} != "no" LDIRS+= libusb20 SE_DIRS+= libusb20 SE_LIBUSB20= \ Makefile \ README \ util.c \ util.h \ bulk.c \ control.c .endif # Setup the FILES_GROUPS for all DIRS variables above. # The variables are prefixed by 'SE_' to prevent variable collision in # other parts of the system .for d in ${SE_DIRS} .for f in ${SE_${d:tu:C/\//_/g}} SER_${d:tu:C/\//_/g}+= ${d}/${f} .endfor FILESGROUPS+= SER_${d:tu:C/\//_/g} SER_${d:tu:C/\//_/g}DIR+= ${SHAREDIR}/examples/${d} .if ${SE_${d:tu:C/\//_/g}PACKAGE:U} != "" SER_${d:tu:C/\//_/g}PACKAGE= ${SE_${d:tu:C/\//_/g}PACKAGE} .else SER_${d:tu:C/\//_/g}PACKAGE= examples .endif .endfor BINDIR= ${SHAREDIR}/examples beforeinstall: copies META_TARGETS+= copies copies: .for i in ${LDIRS} if [ -L ${DESTDIR}${BINDIR}/$i ]; then \ rm -f ${DESTDIR}${BINDIR}/$i; \ fi .endfor SUBDIR+= smbfs HAS_TESTS= SUBDIR.${MK_TESTS}+= tests SUBDIR_PARALLEL= .include diff --git a/share/examples/sound/mmap.c b/share/examples/sound/mmap.c new file mode 100644 index 000000000000..7f165d417020 --- /dev/null +++ b/share/examples/sound/mmap.c @@ -0,0 +1,297 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 Goran Mekić + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This program demonstrates low-latency audio pass-through using mmap. + * Opens input and output audio devices using memory-mapped I/O, + * synchronizes them in a sync group for simultaneous start, + * then continuously copies audio data from input to output. + */ + +#include + +#include "oss.h" + +/* + * Get current time in nanoseconds using monotonic clock. + * Monotonic clock is not affected by system time changes. + */ +static int64_t +gettime_ns(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + err(1, "clock_gettime failed"); + return ((int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec); +} + +/* + * Sleep until the specified absolute time (in nanoseconds). + * Uses TIMER_ABSTIME for precise timing synchronization. + */ +static void +sleep_until_ns(int64_t target_ns) +{ + struct timespec ts; + + ts.tv_sec = target_ns / 1000000000LL; + ts.tv_nsec = target_ns % 1000000000LL; + if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) != 0) + err(1, "clock_nanosleep failed"); +} + +/* + * Calculate the number of frames to process per iteration. + * Higher sample rates require larger steps to maintain efficiency. + */ +static unsigned +frame_stepping(unsigned sample_rate) +{ + return (16U * (1U + (sample_rate / 50000U))); +} + +/* + * Update the mmap pointer and calculate progress. + * Returns the absolute progress in bytes. + * + * fd: file descriptor for the audio device + * request: ioctl request (SNDCTL_DSP_GETIPTR or SNDCTL_DSP_GETOPTR) + * map_pointer: current pointer position in the ring buffer + * map_progress: absolute progress in bytes + * buffer_bytes: total size of the ring buffer + * frag_size: size of each fragment + * frame_size: size of one audio frame in bytes + */ +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) +{ + count_info info = {}; + unsigned delta, max_bytes, cycles; + int fragments; + + if (ioctl(fd, request, &info) < 0) + err(1, "Failed to get mmap pointer"); + if (info.ptr < 0 || info.ptr >= buffer_bytes) + errx(1, "Pointer out of bounds: %d", info.ptr); + if ((info.ptr % frame_size) != 0) + errx(1, "Pointer %d not aligned to frame size %d", info.ptr, + frame_size); + if (info.blocks < 0) + errx(1, "Invalid block count %d", info.blocks); + + /* + * Calculate delta: how many bytes have been processed since last check. + * Handle ring buffer wraparound using modulo arithmetic. + */ + delta = (info.ptr + buffer_bytes - *map_pointer) % buffer_bytes; + + /* + * Adjust delta based on reported blocks available. + * This accounts for cases where the pointer has wrapped multiple times. + */ + max_bytes = (info.blocks + 1) * frag_size - 1; + if (max_bytes >= delta) { + cycles = max_bytes - delta; + cycles -= cycles % buffer_bytes; + delta += cycles; + } + + /* Verify fragment count matches expected value */ + fragments = delta / frag_size; + if (info.blocks < fragments || info.blocks > fragments + 1) + warnx("Pointer block mismatch: ptr=%d blocks=%d delta=%u", + info.ptr, info.blocks, delta); + + /* Update pointer and progress tracking */ + *map_pointer = info.ptr; + *map_progress += delta; + return (*map_progress); +} + +/* + * Copy data between ring buffers, handling wraparound. + * The copy starts at 'offset' and copies 'length' bytes. + * If the copy crosses the buffer boundary, it wraps to the beginning. + */ +static void +copy_ring(void *dstv, const void *srcv, int buffer_bytes, int offset, + int length) +{ + uint8_t *dst = dstv; + const uint8_t *src = srcv; + int first; + + if (length <= 0) + return; + + /* Calculate bytes to copy before wraparound */ + first = buffer_bytes - offset; + if (first > length) + first = length; + + /* Copy first part (up to buffer end or length) */ + memcpy(dst + offset, src + offset, first); + + /* Copy remaining part from beginning of buffer if needed */ + if (first < length) + memcpy(dst, src, length - first); +} + +int +main(int argc, char *argv[]) +{ + int ch, bytes; + int frag_size, frame_size, verbose = 0; + int map_pointer = 0; + unsigned step_frames; + int64_t frame_ns, start_ns, next_wakeup_ns; + int64_t read_progress = 0, write_progress = 0; + oss_syncgroup sync_group = { 0, 0, { 0 } }; + struct config config_in = { + .device = "/dev/dsp", + .mode = O_RDONLY | O_EXCL | O_NONBLOCK, + .format = AFMT_S32_NE, + .sample_rate = 48000, + .mmap = 1, + }; + struct config config_out = { + .device = "/dev/dsp", + .mode = O_WRONLY | O_EXCL | O_NONBLOCK, + .format = AFMT_S32_NE, + .sample_rate = 48000, + .mmap = 1, + }; + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = 1; + break; + } + } + argc -= optind; + argv += optind; + + if (!verbose) + printf("Use -v for verbose mode\n"); + + oss_init(&config_in); + oss_init(&config_out); + + /* + * Verify input and output have matching ring-buffer geometry. + * The passthrough loop copies raw bytes at the same offset in both mmap + * buffers, so both devices must expose the same total byte count. + * They must also use the same max_channels because frame_size is + * derived from that value and all mmap pointers/lengths are expected to + * stay aligned to whole frames on both sides. If channels differed, the + * same byte offset could land in the middle of a frame on one device. + */ + if (config_in.buffer_info.bytes != config_out.buffer_info.bytes) + errx(1, + "Input and output configurations have different buffer sizes"); + if (config_in.audio_info.max_channels != + config_out.audio_info.max_channels) + errx(1, + "Input and output configurations have different number of channels"); + + bytes = config_in.buffer_info.bytes; + frag_size = config_in.buffer_info.fragsize; + frame_size = config_in.sample_size * config_in.audio_info.max_channels; + if (frag_size != config_out.buffer_info.fragsize) + errx(1, + "Input and output configurations have different fragment sizes"); + + /* Calculate timing parameters */ + step_frames = frame_stepping(config_in.sample_rate); + frame_ns = 1000000000LL / config_in.sample_rate; + + /* Clear output buffer to prevent noise on startup */ + memset(config_out.buf, 0, bytes); + + /* Configure and start sync group */ + sync_group.mode = PCM_ENABLE_INPUT; + if (ioctl(config_in.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0) + err(1, "Failed to add input to syncgroup"); + sync_group.mode = PCM_ENABLE_OUTPUT; + if (ioctl(config_out.fd, SNDCTL_DSP_SYNCGROUP, &sync_group) < 0) + err(1, "Failed to add output to syncgroup"); + if (ioctl(config_in.fd, SNDCTL_DSP_SYNCSTART, &sync_group.id) < 0) + err(1, "Starting sync group failed"); + + /* Initialize timing and progress tracking */ + start_ns = gettime_ns(); + read_progress = update_map_progress(config_in.fd, SNDCTL_DSP_GETIPTR, + &map_pointer, &read_progress, bytes, frag_size, frame_size); + write_progress = read_progress; + next_wakeup_ns = start_ns; + + /* + * Main processing loop: + * 1. Sleep until next scheduled wakeup + * 2. Check how much new audio data is available + * 3. Copy available data from input to output buffer + * 4. Schedule next wakeup + */ + for (;;) { + sleep_until_ns(next_wakeup_ns); + read_progress = update_map_progress(config_in.fd, + SNDCTL_DSP_GETIPTR, &map_pointer, &read_progress, bytes, + frag_size, frame_size); + + /* Copy new audio data if available */ + if (read_progress > write_progress) { + int offset = write_progress % bytes; + int length = read_progress - write_progress; + + copy_ring(config_out.buf, config_in.buf, bytes, offset, + length); + write_progress = read_progress; + if (verbose) + printf("copied %d bytes at %d (abs %lld)\n", + length, offset, (long long)write_progress); + } + + /* Schedule next wakeup based on frame timing */ + next_wakeup_ns += (int64_t)step_frames * frame_ns; + if (next_wakeup_ns < gettime_ns()) + next_wakeup_ns = gettime_ns(); + } + + if (munmap(config_in.buf, bytes) != 0) + err(1, "Memory unmap failed"); + config_in.buf = NULL; + if (munmap(config_out.buf, bytes) != 0) + err(1, "Memory unmap failed"); + config_out.buf = NULL; + close(config_in.fd); + close(config_out.fd); + + return (0); +}