cross platform open source driver development framework
Loading...
Searching...
No Matches
Release history
3.0.2 - One kernel contract on every platform
2026-10-15
The kernel stands on one contract - dxd::driver, dxd::device, dxd::device::client - on WDM, KMDF, DriverKit and IOKit, so a product's kernel code is the same on every platform; buses take their devices through dx::match, the service is one model on Windows and macOS (interface/service), and every participant moves its circular through dx::circular::conversion.
Streaming and configuration
dx::circular::conversion holds the typed loops - force_write, mix_write, force_read - for one pair of sample types, which dx::stream::conversion(ext, circular) resolves once off both dx::stream::formats: every participant (engine group, kernel client, PortCls, WASAPI, ASIO, the CoreAudio client and server pin) converts at its own side, every format and width moves. New: pcm24in32hibe, dx::int32be.
stream::device::open(desc, client): a data driven stream (MIDI, CBOR) takes one client, a second open answers exclusive - its circular has exactly one writer and one reader; stream::engine::group skips a pin held exclusively and streams the rest.
dx::stream::engine::device<device_t, engine_t> (dx_stream_engine_group.h) is a bus device carrying the engine its app session runs on, on every platform, the ASIO driver's included; a pin failing in run() gives back the start shares counted before it, so no stream stays started without a session.
desc::start_stop_order names the direction a USB isochronous session stops first while the other keeps submitting (pipeline.held); halt() waits for the run out, so alternate setting 0 meets no request in flight.
desc::stream::transaction::broadcast: every data driven in reader keeps its own position, every client sees every message. transaction::timeout (msec) aborts an out transaction the device does not take.
usb::isoc::pipeline runs a clock domain's bulk transactions on the group's tick, paced by the device.
coreaudio::server::pin: the pin marked sync_reference is its circular's transport and moves its index, every other pin a client at its own sample time; the transport reader clears up to tx, so mixing clients find silence.
dx::midi::parser is the one MIDI 1.0 byte stream state - running status, system exclusive in 3 byte pieces -, encoded to UMP or USB MIDI, one per port and writer, so a system exclusive split across writes arrives whole; midi::transmit() sends under running status.
dx::usb::identifies(serial): an empty serial number or "no serial number" identifies nothing - the registry path stands in as puuid, so two such devices stay two.
Kernel
dxd::driver, dxd::device, dxd::device::client on WDM, KMDF, DriverKit and IOKit: try_launch() and try_conclude() run once, a throwing launch() is concluded before the exception leaves.
dxd::power<form> binds the OS's power management - plain WDM (S-IRP to D-IRP), dxd::portcls::adapter (IAdapterPowerManagement), KMDF (D0 entry/exit), DriverKit (SetPowerState): a device is told sleep()/wake() without code of its OS.
IOKit: dxd_iokit_class(device)/(user_client) are the product's shells over platform::driver, device and client - the kext carries the dext's code for older Macs.
pci::scatter_gather<limit> on DriverKit, IOKit, WDM and KMDF: DMA buffers as whole pages below a limit, one form on every platform. dxd::firewire::device over a platform transport, an OHCI controller beneath it. dxd::virtuel::stream::device is a kernel virtual device with a clock of its own; dxd::random() cryptographic random numbers.
dx::ioctl::listen registers one dx::os_event per client on the device's dxd::broadcast, signalled on any value's change: a client reads its mirrors back instead of polling.
Windows
kernel/wdm/dxd.inx is the inf template, preprocessed at build time: the manufacturer (DXD_VENDOR) and the hardware ids off the models, Windows 11 (22000) the minimum, DMA remapping opted into.
Every WDM and KMDF device declares surprise removal as expected - no warning on an unplug.
Every binary links the static C runtime - no redistributable to install. power::disable_c3::shared is held once per process.
macOS
dx::install::parser --install/--uninstall activate and deactivate the dext and reenumerate the devices of a replaced one, which would run on until unplugged.
coreaudio::server releases the streams of a client whose process is gone.
dx::proxy::driver matches the services whose dxd property is the product id, kext and dext alike.
dx::match (dx_match.h): dictionaries ORed, verbs ANDed, a text a regular expression; dx::match(lookup) answers off the bus or a desc before a device starts, so one that does not match is never opened. map::driver holds it, *driver= match builds the device tree anew.
dx::log::pipes names per std stream the sinks it goes to - stdio, system, file, debugger, notification -, applied through redirect::pipe, set with --log-pipe.
The test app: --<device> is one command per device as --info lists it; --unit-test and --device-configuration are device commands, the driver's --unit-test runs the selected devices in parallel (device <selector>, surprise [msec]); --midi-test in|out <pin>; parser::value<T>() reads every number.
dxd.cmake ahead of project(), then one function per kind of binary - dxd_test, dxd_asio, dxd_service, dxd_installer, dxd_coreaudio, dxd_coremidi, dxd_daemon (SERVICE), dxd_driver (KMDF <minor>, MODELS), dxd_driverkit/dxd_iokit (MODELS: an Info.plist personality per model), dxd_kext, dxd_dext, dxd_iig: a product's CMake states its targets and sources alone.
dxd_profile creates the provisioning profile over the App Store Connect API, written with -DDXD_APPSTORE_WRITE=ON (--appstore-write) alone; dxd_target_sign(... ENTITLEMENTS PROVISIONED ADHOC); dxd_target_icon makes an Icon Composer icon of an SVG (icon.xsl).
release.sh --dxd <dir> takes a framework lying elsewhere; a product without README.md gets the file with its version's heading.
etc/test/include/test.sh builds on Windows through build.ps1, a header that does not compile fails the run. CMake keeps a top level CMAKE_CONFIGURATION_TYPES and CMAKE_MSVC_RUNTIME_LIBRARY.
Breaking changes
What a product built against 3.0.1 adapts in its own code:
Kernel classes: dxd::adapter is dxd::portcls::adapter, dxd::clientdxd::device::client. A KMDF driver is dxd::kmdf::driver with create(WDFDEVICE_INIT*) - driver<device_t>, driver_creator, device_add, create_device are gone -, filter(WDFIORESREQLIST) takes prepare_hardware and the resource hooks.
Streams: pin::attach<channel_t>(channel_t** io[2], map) is attach(void** io[2], map, const
dx::stream::format&), on stream and device alike - the caller states its format, the circular converts. dx::ioctl::stream::memory(), selector(), id() are gone: the memory type is the 32 bit id, proxy::device::map/unmap(uint32_t).
Match: a bus driver's match is a dx::match; asio::dictionary, midi::services::dictionary, wasapi::dictionary are gone; add(id) returns bool, map::driver::add(id, lookup).
MIDI: dx::midi::circular is gone; ump::circular::encode(parser, src, size, port) and usb::midi's encode(parser, src, size, cable) take the parser.
Values: DX_CF_SERIALIZE and desc::value_t are gone, a dictionary goes through cf::store; dx::log is {raw, pipes}; dx::pci::dma carries no alignment and scatter_gather; windows/dx_hwid.h is interface/dx_hwid.h.
Process: dx::process::headless(own_process) is dx::process::initialize(bool headless= false) - its argument inverted.
A device runs a configuration of its own, aligned to what its bus carries, and a USB session runs through its failures; the control panel is generated from the member lists, and the build and release pipeline is shared.
Streaming and configuration
A device runs a configuration of its own - per stream the stream configuration it runs, aligned to its bus: on USB the periodic budget, interrupt endpoints included, a stream configuration per alternate setting. The default is the richest one the bus admits, on a tie the one running more streams - a device of MIDI and raw streams runs them all.
A configuration change rebinds the rings it moves; where the bus refuses it, the device keeps the configuration it runs.
The USB isochronous session runs through its failures: a failed submit costs its frame, a group charges as a unit, a refused charge rests it and retries every 250 ms. insufficient_bandwidth names a refused admission on every platform.
The session brings up the reference stream's group, whoever starts it, and holds every pipe of a started group until its halt. A stream the configuration turns off keeps its ring binding and never starts; engine::group skips it.
The operating system's removal alone removes a device; on macOS a device that answers nothing ends its streams as a removed one does.
A pipe opens its endpoint's own interface; a function of several interfaces carries an interface association descriptor, which dxd assumes.
stream::clock waits by strategy - plain, yield or spin -, one wait for dx::timer and the kernel's dxd::clock; the kernel transport's strategy and leads per direction are device settings.
A mix ring's transport reader reads a span no writer reached as silence, not as a dropout; circular::stamp() is shared by the isochronous, CoreAudio and WASAPI transports.
options.quirk.samplerate covers a device whose sampling frequency control departs from its class descriptors.
Lifecycle
A launch() that throws is concluded before the exception leaves.
A shared stream ends its session in its own conclude(); a stream whose hooks reach its own members concludes in its own destructor.
A proxy device's chained setting asks the kernel at its initialization; promoted::preference::initialize() reads under the promoted's lock.
Windows
The WASAPI client's tick walks the started clients lock free; the render transport clears what it read.
A WaveRT endpoint's ranges are the clock's bitmap, the first stream to run binds the rate.
int24 stays inside its own 3 bytes under MSVC.
USB string descriptors read whole and as UTF-8 on every path - user mode, WDK and IOKit.
install::parser --uninstall ends a software device as its creator does.
asio::driver sends no resync or reset request while a debugger is attached.
macOS
A CoreAudio host's settings are the device property STNG; nominal sample rate requests coalesce into one configuration request.
The pod client holds a stream once it launched; shared memory belongs to the side that sizes it.
The CoreMIDI server carries a port per pin.
The daemon captures the devices by vendor and product ids compiled into it.
A device's icon is the bundle's <model name>.icns.
The driver's postinstall restarts AirPlayXPCHelper once coreaudiod has loaded its plug-ins again.
GUI, values and diagnostics
The control panel is generated from the member lists (gui::model) and rendered alike by wxWidgets and JUCE.
Serialization has one sink, the key value tree; a member's kind - power2, bitmap, choice - states its presentation, visit() hands every member to a visitor.
info() prints the whole desc; a log writer never waits, a single line takes no io_mutex.
A version's build holds three decimal digits; a firmware's version is a DX_VERSION32 converted from bcdDevice; DX_VERSION_SHORT is every plist's CFBundleShortVersionString.
--hub lists every hub with its ports, open pipes and periodic bus time.
Build, signing and release
Every Windows binary is signed centrally; the key vault comes from the product's version.h with every build, --self-signed signs with the build server's own certificate.
Windows builds in parallel - build.ps1 across projects, MSVC across sources (/MP); the debug configuration defines DX_DEBUG.
dxd_plist takes its template and version.h as configure dependencies - a version bump reaches the plist.
build.sh builds and notarizes the packages where this platform's build wrote them, a CMake product's deployment target off its cache; one table of component bundles serves the packages and the test install, which runs from any product directory.
release.sh checks notarytool's credentials before it builds, notarization tries five times at most; dxd is tagged by its release's own name, a sub-release beside its minor; the Windows build is handed the directories it reads.
The build server is shared: its users enter their pid (etc/vm.sh), the last one out shuts it down.
Breaking changes
What a product built against 3.0 adapts in its own code:
Stream descriptions: transaction, buffers and clock.sync belong to the stream, not to a stream configuration. A configuration states its alternate setting (id) and its format - no wordlength; a MIDI stream's format is dx::usb::midi<>::format. A target is its endpoint address alone, without interface or setting.
Diagnostics: objects stream themselves through operator <<; the virtual info(std::ostream&) overrides are gone.
CMake: every function is dxd_<name>, without a platform suffix - dxd_plist, dxd_target_sign, dxd_embed_file, dxd_embed_ihx, dxd_embed_rbt, dxd_driver_package.
Licensing: a certificate covers the devices its DX_OID_LICENSE_MODELS names - a vendor's model, every model of a vendor or every device; an absent or empty list covers nothing. check() takes the device's vendor and model.
Proxy: proxy::usb::device is gone - reenumerate() is a dxd ioctl on the proxy device, the kernel cycles the upstream port.
3.0 - Multi-client streaming and the Windows kernel path
2026-09-26
The first stable release since 2.5: several clients share a stream on every bus, the Windows kernel driver streams over a USB pipeline of its own, and the framework verifies its licenses itself.
The Windows kernel driver
kernel/wdm carries the whole WDM audio path: a PortCls adapter publishing a WaveRT subdevice per audio pin and a DMus subdevice per MIDI pin (dxd::portcls).
It streams over a USB device of its own - raw URBs, isochronous pipes, the configuration walked by the same dx::usb::audio::configure user mode uses, a boot() hook for a product's firmware handshake.
The tick runs on a high resolution EX_TIMER, aligned to the bus frame counter; the device's rings are mapped into every proxy client, each with a cursor of its own.
Surprise removal, the hardware released in conclude() ahead of the remove, and the license gating the published subdevices.
Streaming
The streams of one device that always stream together form a group with one real time thread (dx::stream::group); the group owns every session resource, and every group of a device streams on its own thread.
Several clients share one stream: writers mix with saturation, readers keep their own positions, and the hardware clock reaches every consumer at its own I/O size.
The tick submits, writes and reads by the frame counter alone, a lead ahead of and behind the wire; completions count and carry no timing.
A tick comes from a free running clock, the dx::timer high resolution timer or the USB bus frame counter, waiting plain, yielding or spinning; transport and clients have their own strategy and leads, with per role defaults in the desc.
A ring is reset by its host at its session's start and by nobody else: a service client, a pod, a proxy client or a kernel client attaches to a running ring by anchoring a dx::stream::cursor.
A ring carries no transport of its own - the desc names it as a direction, in writes the ring, out reads it - and every participant anchors at that one index, whatever its own direction.
A session holds the streams it runs, its members taken in run() ahead of the thread and released at its end; the participants sit on a lock free list, so the tick signals every mapping without a lock.
The isochronous session's feedback ring holds two rounds at a constant lead; an asynchronous USB sink is paced by its explicit feedback endpoint (10.14 or 16.16) or by implicit feedback, on both platforms; a group brings all its pipes up as a unit, and an isochronous gap goes out as silence.
A bulk stream is data driven: its pipe keeps the platform's request ring in flight, and whatever the ring holds goes out in one request, the packet across the ring's end bridged behind it.
A session runs out in the device's start/stop order before its thread ends; the clock's discharge() drains the isochronous buffers and releases them on the thread that registered them.
The sample converters are keyed by the whole format - resolution, endian and alignment (dx::stream::sample) - in one table covering big endian, packed 24 bit and float64; a stream's latency is the transport lead in samples.
A clock's lock delay is waited out on both platforms; a sample rate change pauses the device's groups until the clock locks.
A stream's source selector is a desc entry and a promoted preference, backed by the USB selector unit.
A class compliant device's streams follow its clock domains on the richest alternate setting, a vendor specific interface with audio subclasses reads as audio, the sampling frequency is set per endpoint after the alternate setting.
The desc carries the firmware's name, version and serial and the hidden, default and editable flags; bus and plug are vocabularies of their own, mapped from IOKit, CoreAudio and the USB terminal types.
USB interfaces are addressed by number and alternate settings by value, descriptor lengths bounds checked; devices follow system sleep and wake and negotiate their settings again on wake.
EZ-USB firmware downloads from images embedded at configure time (Intel HEX, Xilinx bitstream).
Values and their stores
A promoted with a server asks it first and is born with its value otherwise; a value that could be asked with neither does not compile (dx::promoted).
The driver's log is a preference promoted on the store and serializes like a stream desc through the member list sinks of dx_serialize.h, so registry and property list hold it resolved.
Licensing
dx::license is one class for user mode and the kernel on every platform: it reads the certificates' DER, verifies the signature against the product's embedded root with its own SHA-256 and RSA, and evaluates type and constraints.
License attributes are private X.509 extensions under the vendor's IANA arc (DX_OID_LICENSE_TYPE, DX_OID_LICENSE_CONSTRAINTS): the types demo, beta and perpetual, the constraints in disjunctive normal form; a certificate without constraints binds nothing.
A license binds to the device's serial number (DSN), the machine's or an ethernet address; the platform supplies the store's candidates and those identities alone - keychain, CryptoAPI, in the kernel the registry and WMI.
The Windows user mode side
The service hosts a device tree and serves its streams over shared memory and the dxd pipe; the pipe carries a mutex, a connection answers in request order and its answer names the request's kind.
The ASIO driver runs as the service's client or straight on the kernel driver and registers itself when a device arrives; a static entry is one flat key, stale CLSID registrations are swept, several dxd ASIO drivers load side by side.
The WASAPI client - one device per adapter with a stream per endpoint, a persisted shared or exclusive mode, one clock domain per device - and the Windows MIDI Services client, with their match dictionaries.
The installer is WiX 6, a merge module and an .msi without DIFx; dx::install::parser installs the driver packages as custom actions - devices of any bus class, --restart bringing them back on the package that ranks best in the driver store, --scan reenumerating them where they stand, a device that fails failing the verb.
The proxy creates and destroys software devices (SwDeviceCreate), the desc a property of the PDO, and installs driver packages through DiInstallDriver, purging outdated ones.
Shared memory, pipes and named events serve unprivileged clients; a per application reference count keeps a client's preferences crash safe; the kernel driver and the services log to the Windows event log.
ARM64 builds the kernel driver, ARM64EC user mode alone; the kernel driver builds through kernel/wdk/CMakeLists.txt off the WDK NuGet packages.
MIDI
dx::midi speaks the Universal MIDI Packet: the ring of packets, the encode and decode of the MIDI 1.0 byte stream, the group inside the word; stream::format::midi is none, bytes or ump.
CoreMIDI carries UMP since macOS 11 and Windows MIDI Services from the start, so both buses hand whole packets at their own timestamps.
A CoreMIDI client bus with device arrival and removal, and flow control in the CoreMIDI server.
macOS
The doorman daemon takes a class compliant USB audio device from Apple's usbaudiod and does nothing else, so the driver itself stays unprivileged (dx_usb_capture.h, dx_xpc.h, dx_daemon.h); the capture request is the peer's side of it.
Who the peer may be is a code signing requirement: sec::requirement builds one from its text form - team(id) the one Apple issued to a team - and sec::code checks the running peer against it.
The CoreAudio server publishes a data source control per pin, balances start and stop per client and lists its clients with their activity; pod and server run on the multi-client mechanisms.
The client's IOProc holds its streams for the session and takes its interval off the HAL's buffer, with no promoted read on the real time thread; GetZeroTimeStamp never blocks and is quantized to cycle boundaries.
The CoreAudio driver publishes hidden devices, the plugin lists streamless ones, and a device carries an icon.
The IOUSBLib version is read off the loaded bundle; shared object names carry the product id.
Lifecycle
launch() and conclude() elect a single caller atomically; a concluding device ends every group's session, the start counts staying with their holders.
A driver's notification source lives between its launch() and conclude(); COM apartments belong to the device or bus driver that needs them.
dx::process::headless() suppresses crash dialogs, an uncaught exception is printed before terminate.
GUI and diagnostics
A wxWidgets backend stands beside JUCE; the control panel loads and saves documents with a recent file list and shows device icons, client activity and the hidden, default and editable switches.
The streaming monitor's cells show the tick's wakeup, the processing span, overloads and the leads' reserve.
Terminal output is coloured, log lines and thread names spell the class_name; dx::log has a header shared with the kernel and a level set by name persists; redirect is a distributor whose sinks join and leave in any order.
Tooling and documentation
The test harness has a nested command line, the streaming monitor with loopback, pattern-test and generator, unit test rows per bus and a driver level run with surprise removal; a device is selected by the place it holds in the tree as --info lists it.
impulse-test measures an analog loop's round trip off a band limited impulse, its level calibrated to target.
The test app has an asynchronous surprise switch for --unit-test, a preference concurrency test, load scenarios with lead and safety sweeps, --safety-offset, --strategy, --source, WASAPI's --mode, a mix channel pattern test, --bypass-service and --service-status; the include test links and runs with --macOS.
A product with a root CMakeLists.txt builds through CMake into bin/<target>/<config>, with helpers for plists, embedded files, signing, driver packages and NuGet; a driver package stages its symbols, verifies its inf and stamps DriverVer.
etc/sign.ps1 is one signature path for every case - the key vault's certificate, one a CA issued to the machine, or the machine's own, which a release configuration takes only where the caller says so; attestation submits every architecture's driver packages in one cab.
etc/bootstrap.ps1 and etc/bootstrap.sh prepare a machine for building and signing, with --server the Windows build server.
etc/release.sh performs a stable release end to end - stamp, build every platform, notarize, publish, merge, tag and bump the sub version - and rehearses it with --dryrun; it verifies before it publishes, keeps the replaced set under superseded/, names a release by its number alone and starts and stops the build server's virtual machine.
A release clones the history's commits and tags without their content, writes the README.md a repository named in it lacks, and with -l moves a submodule's pointer to the tip of its branch first; notarization takes an Apple ID or a team ID.
The headers carry their own documentation, and the Doxygen run is warning free.
Breaking changes
What a product built against 2.5 adapts in its own code:
Headers sit in per area subdirectories - interface/{stream,usb,test,virtual,crypto}, macOS/{cf,io,sec,coreaudio,coremidi,proxy,usb}, windows/{asio,crypt,eventlog,midi,proxy,service,usb,wasapi}; the search paths come from interface/CMakeLists.txt or dx.xcconfig.
dx::stream::engine is dx::stream::engine::group<group_t, pin_t, io_t, circular_t> (dx_stream_engine_group.h), the bus device's group template: pins are built and released by attach(device)/detach(device), process, notify, overload and sample rate are hooks on the group.
A stream starts and stops through stream::control's promoted counter (++start, --start, started) and its run_hook, halt_hook and flush_hook; the virtual start()/stop() are gone.
dx::asio<device_t, pin_t, driver_t> is dx::asio::driver<device_t, driver_t> (asio/dx_asio_driver.h); the bus device owns the engine, the header exports DllGetClassObject/DllCanUnloadNow itself.
A dx::promoted is constructed with a server or an initial value - it has no default constructor.
launch()/conclude() are protected, reached through try_launch()/try_conclude(), launched is atomic; exception() overrides are const.
dx::driver<device_id_t, preference_t> and dx::device<id_t, preference_t> carry the store, stream::device takes group_t and no preference_t; a bus driver constructs as (match, product_id[, preference], log), its settings under [product_id][bus].
usb::stream::device<desc_t, preference_t, stream_t, group_t> takes a stream_t template over circular_t and the stream base; usb::device::speed is usb::speed.
A stream desc takes the new layout: bus is dx::stream::bus::bus, a pin's plugdx::stream::plug::plug, clock.defaults.safety_offset is [role][direction] with sync.cycle.hw_min_ticks folded into its transport side and sync.strategy[role] beside it.
The GUI is gui/{juce,wxwidgets}/dx_gui.h, dx_gui_device.h and dx_gui_cpl.h, started by DX_GUI_START_APPLICATION(dx::gui::cpl<driver_t>); dx_cpl.h/.cpp are gone.
A test app derives from test::driver<super_device_t, device_t, driver_t>; engine and streaming tests are device commands, the parser and select live in dx_parser.h.
removed() runs ahead of the device's conclude() and releases its streams, arrived() after its launch().
Proxy drivers take their matching ids as std::set<dx::guid>; dx::guid converts to no const char* on Windows.
KMDF bus drivers derive from dxd::kmdf::device (kernel/wdk/kmdf/dxd_device.h), dxd::device is the plain WDM device; interface/windows/dx.rc serves both binary kinds, kernel/wdk/dxd.rc is gone.
DIFx is gone: the merge module etc/dxd_module carries the binaries, the WiX 6 .msi installs the driver packages through the product's install executable (dx::install::parser).
The Windows build is etc/build.ps1; etc/build.cmd is gone.
CMake configurations are spelled debug and release, other spellings are refused.
2.5 - Device settings as observable values
2025-08-26
Sample rate, clock source, I/O size, safety offsets, sync cycle, sync reference, desc and client list are dx::promoted values a preference, an ioctl or a CoreAudio property serves, notifying their listeners on every bus; sync reference and clock source selection reach every bus, and the ring buffer stands in a header of its own.
Values and stores
dx::promoted<value_t> (dx_value.h) is a value with a server, listeners and an initialized/transient state: an assignment goes to the server, which commits it back, and the listeners are told outside the lock.
promoted::preference binds a promoted to a backing store - a default, align(value, current) against the current value, the conversion between the value and its stored form - and listens to the store, so an external change reaches the value; promoted::chain puts a server ahead of an existing one.
coreaudio::promoted::property<value_t, coreaudio_value_t, settable> is a CoreAudio property as a promoted, notified by the HAL.
The Windows registry is hierarchical: nested [], iteration over keys and values, binary values, a default on a read error, change notification and schedule_erase; a Windows stream::device takes its preference from driver.preference[id].
Streaming and configuration
A sync reference is any stream whose config sets clock.sync.reference; desc::align()/sync_reference() map between stream and ordinal, CoreAudio and virtual device streams qualify.
clock.defaults.iosize.align() aligns the I/O size to the hardware interval, in whole ticks with iosize.lock.
The sample rate changes while the selected clock source is not externally locked alone.
stream::desc has a value_t - a dictionary on macOS - and operator ==; ioctl::stream::desc gets and sets a proxy device's desc, proxy and virtual devices publish desc and clients as promoted values.
A USB stream pipe carries a transaction whose size differs from the ring's.
The USB stream device sets the sample rate over the UAC1 endpoint or the UAC2 clock source SET_CUR and reads it back with GET_CUR.
Lifecycle
A non-persistent virtual device erases its preference when it concludes.
The proxy driver tells arrival and removal only when the device list changed.
A driver states its connection_id: the CoreAudio plugin object, or the kernel extension's bundle identifier.
macOS
The CoreAudio client device publishes the clock source selector, I/O size, sample rate, clients and per direction latency as promoted HAL properties; its IOProc ticks the hardware clock with the buffer size it is given.
The CoreAudio server submits a sample rate change only when the value differs, its clock source selector reports its current item and notifies changes, and it announces the plugin list on initialize.
DriverKit has dxd::memory; the proxy client maps the clock monitor.
Windows
ASIO reports the device's I/O size as preferred and the hardware interval as granularity where the I/O size is locked, and sends kAsioBufferSizeChange and kAsioResyncRequest where the host takes them, kAsioResetRequest otherwise.
The Windows projects build ARM64 and ARM64EC with the standard __cplusplus; etc/build.cmd builds Win32, x64, ARM64 and ARM64EC with Visual Studio 2022.
MIDI
The CoreMIDI server pin starts and halts through the stream layer.
GUI and diagnostics
The control panel follows the promoted sample rate, I/O size and desc, lists a device's clients by process name, offers min, default, max and the sync cycle's I/O size, and creates persistent virtual devices with a sync reference.
The test app sets and reads the clock source and the sample rate (--clocksource, --samplerate).
Breaking changes
What a product built against 2.4 adapts in its own code:
stream::device's virtual samplerate(), clocksource(), iosize(), safety_offset(direction), sync_cycle_ticks() and sync_reference() are promoted<>::preference members - read by conversion, set by assignment; a derived device customizes one through its server (promoted::chain).
sync_reference is a promoted<desc_t::stream*>; its preference holds the ordinal among the streams flagged clock.sync.reference, so a stored value is chosen again.
stream::stream::samplerate() and stream::pin::samplerate() are gone: stream.samplerate refers to the device's promoted value; stream::latency is a member.
device::iosize(iosize, event&) is hw_clock(event&), the I/O size set through the promoted iosize.
device::monitor(), monitor<>() and shared_monitor are one monitor member converting to bool and to clock::monitor*, held with ++/-- or dx::reference.
stream::device's desc type is desc_t, which every driver::add(id, desc) takes.
exception(const exception&, bool) overrides take a non-const exception.
coreaudio::driver::plugin is connection_id; coreaudio::device::clients is a promoted dx::device::client_map (dx_device.h); coreaudio::pod::device's desc is the promoted member desc.
The CoreAudio client device's and the CoreMIDI server pin's stop(bool) overrides are halt(), driven by the stream layer's start and stop; stream::pin::signal() and wait() are gone.
DX_BCD(x) converts a decimal number to BCD; DX_DEC is the token paste 0x##x.
dx::registry constructors take (key, rights, root); event<kernel>(HKEY) is gone, the registry notifies itself.
cf::key_value_tree::operator bool, cf::reference::operator CFTypeID and the CoreAudio server preference's operator bool are explicit.
2.4 - One device tree for every bus
2025-08-07
The 2.1 development line's device tree harmonization: kernel proxy, USB, virtual and CoreAudio client devices stand on one dx::driver/dx::device base and share one stream::device/stream/pin/control model and one streaming engine; on it the ASIO driver, the CoreAudio client with its virtual devices, the USB Audio Class parser, the split CoreFoundation, SystemConfiguration and IOKit wrappers, the JUCE control panel and the DriverKit and ARM64 builds.
Streaming and configuration
dx::usb is one platform independent implementation over a thin macOS and Windows layer; WinUSB streams isochronous pipes.
The USB stream pipe buffers its isochronous transactions in the ring, carries implicit feedback - micro frames included - on both platforms, pumps data driven under a watchdog and applies flow control on receive.
A USB device's hardware clock drives the engine, over isochronous as over interrupt and bulk streams.
The USB Audio Class 1.0 and 2.0 descriptors are parsed whole - control units, terminals, streaming and MIDI descriptors - and a device configures itself from them.
The stream desc carries the clock settings (source, external lock, S/PDIF professional mode), a sync reference, the sample rate aligned to the closest supported one and a start/stop order.
The streaming engine converts float32, float64 and integer formats through templated converters, takes different in and out channel counts, streams both directions and runs devices without a hardware clock.
A pin reports its latency to CoreAudio and ASIO; the safety offsets follow the clock interval.
Device tree and drivers
Kernel, USB, virtual and CoreAudio client devices share one device tree: dx::device, object::info() and the puid/puuid identities.
A virtual stream device is created from a desc, persisted in the preference store, and created or removed through the CoreAudio plugin's custom properties.
The CoreAudio client driver follows the plugin list, matches server plugins and mirrors a plugin's devices with clock, sample rate, I/O size and clients; a pod device follows a device's desc.
A driver handles sleep and wake on macOS and Windows; ASIO stops and restarts its stream around them.
An exception is handled along pipe, device and driver, carries a stack trace and its thread's name and copies its nested chain in depth.
Values and stores
cf::key_value_tree gives the CFPreferences, SystemConfiguration, CoreAudio server and registry stores one hierarchical key value idiom with range based iteration; sc::preference follows external updates.
The driver's log level lives in the preference store.
Windows
dx::asio is the ASIO driver on the streaming engine - latencies, control panel, reset requests and device changes.
WinUSB serves several clients through separate USB interfaces; a Windows pipe keeps up to 64 transactions outstanding.
Shared memory and shared events, real time priorities with MMCSS and a high resolution timestamp.
The kernel driver targets KMDF 1.33; DIFx installs an ASIO only package without the kernel driver.
macOS
A DriverKit kernel platform and project stand beside IOKit, with the CoreAudio plugin's DriverKit plist and entitlements.
The CoreAudio server plugin publishes gain, mute, solo and pan per channel and main, emulated from encoders, the clock source and S/PDIF selectors, float formats and the client list; it serializes its devices against IOService notifications and a removal during playback.
Logging goes to unified logging, real time threads join the audio workgroup, threads carry names.
MIDI
The CoreMIDI server streams MIDI out on its own thread, decodes USB MIDI and buffers MIDI in.
GUI and diagnostics
gui/juce is a JUCE control panel: clock, sample rate, I/O size and clients, following driver arrival and removal and the system's dark or light mode.
Build, signing and release
build.sh and release.sh notarize through notarytool, sign with a selectable identity under the hardened runtime, build app bundle installers embedding DriverKit provisioning profiles and retry their network steps.
The Windows projects build ARM64 and ARM64EC, a CMake project builds the macOS interface, the deployment target is macOS 10.14; the CMake project carries the license.
The documentation runs on Doxygen 1.10.
Testing
The test app has a hierarchical parameter parser, USB, MIDI and virtual test drivers, a streaming monitor with host loopback, generators and per channel pattern tests, and unit tests for buffers, surprise removal, sample rate alignment and preferences.
etc/test/include compiles every interface header on its own, on macOS and Windows.
Breaking changes
What a product built against 2.1 adapts in its own code:
The CoreFoundation wrappers are the namespace cf, one header per type (cf_*.h, cf.h), dx_mach.h is gone; domain preferences are sc::preference (sc_preference.h); the IOKit wrappers are io_*.h, dx_iokit.h is gone.
Every streaming entity is a stream::control<stream::object<device_t>>: stream::device<super_device_t, desc_t,
preference_t, stream_t> holds stream::streams, stream::pin<device_t, circular_t> takes no desc_t and waits on its stream's data[in/out] events in place of signal()/wait(); start and stop are reference counted.