Windows audio, media, and device integration

Trace Wine audio, media and device interfaces across host services.

13 min readChapter 165 of 168Intermediate → advanced
In this chapter

Before you start#

Review ALSA and audio timing, PipeWire audio, Wine handles, and Wine windowing. This chapter reads Wine 10.0 commit b073859675060c9211fcbccfd90e4e87520dc2c2. The audio values are synthetic; no audio device is opened and no sound or recording is produced.

Learning goals#

Trace an audio format and render buffer through a Windows-facing client and Wine's Pulse driver. Compute buffer duration and explain padding, stream lifetime, errors and clock drift. Identify where media negotiation and device support require additional evidence.

Source walkthrough#

A rendered audio buffer crosses interfaces
  1. Windows client

    Negotiates a format and obtains frames.

  2. Wine audio driver

    Tracks stream buffers and timing.

  3. Host audio server or ALSA

    Accepts playback under its own buffering rules.

  4. Device

    Consumes samples according to its clock.

Conceptual model. A source-informed model, not a recorded audio stream.

A Windows application can discover audio endpoints through MMDevice interfaces and create an IAudioClient for one endpoint. It negotiates a format, share mode and buffer parameters, then uses a render or capture service. An endpoint is a logical device in the Windows-facing model, not automatically a particular Linux hardware card. A stream needs an actual host route and permission. A selected default endpoint can change or disappear while a client holds an interface, so object identity, device availability and stream state are separate facts.

The first exact excerpt comes from Wine 10.0 dlls/winepulse.drv/pulse.c, pulse_get_render_buffer. Its file notice grants LGPL-2.1-or-later; surrounding checks and allocation are omitted:

c
    if (stream->held_bytes / pa_frame_size(&stream->ss) + params->frames > stream->bufsize_frames)
    {
        pulse_unlock();
        params->result = AUDCLNT_E_BUFFER_TOO_LARGE;
        return STATUS_SUCCESS;
    }

The expression converts held bytes into frames, adds the requested frames and compares the result with the configured buffer capacity. If the request would exceed capacity, the driver unlocks, sets the Windows-facing result to a buffer-too-large error and returns from its Unix-call function. STATUS_SUCCESS here is the transport-level function return; params->result carries the audio operation's failure. Treating the outer return alone as successful audio acquisition would miss the error. The earlier path also checks stream validity and whether another buffer is already locked.

The second exact excerpt is from the same pinned file, pulse_release_render_buffer, with the same LGPL notice. Surrounding size validation and wrap handling are omitted:

c
    stream->held_bytes += written_bytes;
    stream->pa_held_bytes += written_bytes;

The first statement adds successfully released bytes to the stream's held data; the second updates the data tracked for the Pulse side. The subsequent code bounds the Pulse accounting against its real buffer size, advances clock_written, clears the lock and calls pulse_write. Releasing a buffer is a state transition and makes the supplied frames available to this path; it is not proof the speaker has played them. A request to release more frames than were locked takes an invalid-size path. Ownership and byte counts must survive the interval between obtaining the memory and releasing it.

Frames, formats, and timing#

A sample is one value for one channel at one instant; a frame groups one sample for every channel at that instant. At 48,000 frames per second, a stereo stream still has 48,000 frames per second but 96,000 individual channel samples per second. For interleaved signed 16-bit stereo, each frame occupies four bytes. The sample format, channel count, sample rate, channel layout and block alignment must agree; a byte count by itself cannot establish duration without knowing the format.

A PCM format header in 18 bytes

Select a field to connect its position, bytes, and meaning.

18 bytes in file order. Offsets start at zero.

Selected: wFormatTag. WAVE_FORMAT_PCM (1), stored little-endian.

Read the byte table
OffsetFieldHex bytesInterpretation
0wFormatTag01 00WAVE_FORMAT_PCM (1), stored little-endian
2nChannels02 00Two channels
4nSamplesPerSec80 bb 00 0048000 samples per second (0x0000BB80)
8nAvgBytesPerSec00 ee 02 00192000 bytes per second (48000 x 4)
12nBlockAlign04 00Four bytes per frame: two channels of 16-bit samples
14wBitsPerSample10 0016 bits per sample
16cbSize00 00No extra bytes follow for plain PCM
Source map. Source map of the packed WAVEFORMATEX layout in Wine 10.0 include/mmreg.h; the byte values are an example 48 kHz stereo header, not captured data.

An application asks for a supported format or negotiates an alternative according to the interface and mode. Shared mode normally cooperates with an audio engine and its format and scheduling policy; exclusive mode asks for a different contract and can fail when unsupported or unavailable. Wine's Pulse and ALSA implementations and host server configuration influence actual behavior. Do not assume that every Windows exclusive-mode property maps exactly to a host server or that a given host device supports every rate and channel map. The Pulse format path explicitly rejects unsupported tags, invalid sample formats and invalid channel maps in this version.

Padding describes frames already queued or held in the relevant audio client's model. It is a snapshot, not a fixed latency guarantee. If a buffer holds 480 frames at 48 kHz, its nominal duration is 10 ms. If a second downstream buffer also contains 480 frames, a simple illustrative sum is 20 ms before additional scheduling, transport, hardware and conversion delays. This is not a measurement and does not prove that the two buffers are always simultaneously full. Some stages overlap, and a real stream may have different current occupancy at each stage.

A render client normally obtains a region, writes valid frames and releases the number written with appropriate flags. Event-driven clients wait for a notification and then query available capacity; a notification is not a promise that an arbitrary amount of space remains after other work or a device change. The application needs to respect buffer order and size. A zero-frame operation, an out-of-order call, a too-large request and an invalidated device have distinct results in the inspected driver. A caller should keep the interface and buffer valid through release and avoid writing through the pointer afterward.

Worked two-buffer timeline#

Assume a synthetic shared playback stream uses 48 kHz stereo, 16-bit interleaved samples. Its Wine-facing buffer capacity is 480 frames, or 1,920 bytes. At the start of an observation, 240 frames are held, leaving 240 frames of nominal capacity. The application asks for 300 frames. In the quoted check, 240 plus 300 exceeds 480, so the audio result is AUDCLNT_E_BUFFER_TOO_LARGE; the lock is released and no acquired region should be treated as valid. The outer Unix-call status is not the Windows-facing audio result.

Change the request to 120 frames and assume acquisition succeeds. It occupies 480 bytes. The application writes 120 complete stereo frames and releases 120. The release path accounts for 480 written bytes, clears its locked state and tries to push data toward Pulse. Immediately after release, the accounting is not evidence that the physical device has consumed the frames. If the host server holds another 240 frames, those frames contribute a separate nominal five milliseconds under this example's rate. The host server can consume data concurrently, so a real observation requires timestamps and per-stage measurements rather than addition of two capacities.

Now assume the device clock actually consumes 48,004 frames per second while the producer supplies exactly 48,000. The difference is four frames per second; after one minute the accumulated difference is about 240 frames if no resampling or feedback corrects it. This is a deliberately simplified drift calculation. It explains why a real pipeline may need rate adaptation or buffer control; it does not predict a particular Wine underrun. An ALSA xrun, a Pulse underflow and a Windows-facing audio error have related timing causes but are not identical APIs or automatically identical observations.

Driver threads and lifetime#

Wine's Pulse driver uses PulseAudio client interfaces and shared stream state. Its lock protects relevant driver data while callbacks, the audio machinery and client operations can interact. The stream contains format and channel information, buffer sizes, offsets, held-byte counts and a locked-buffer state. An application interface reference and a driver stream handle are different ownership layers; closing one cannot be reasoned about solely from the numerical pointer or handle. A callback must not use a freed stream after teardown, and a client must handle device invalidation rather than assuming a previously returned endpoint remains operational forever.

The host may use PulseAudio itself or a compatible server such as PipeWire's PulseAudio compatibility service. That is a deployment possibility, not a claim that Wine 10.0 has a native PipeWire audio driver. The source files actually inspected are winepulse.drv and winealsa.drv. The ALSA path interacts with ALSA interfaces and its own driver state; it is not the Pulse path with different names. A selected Wine driver, host library, audio server, device and session policy all influence behavior. Installing one library or changing a default device is not part of this exercise.

A lock around a driver's accounting is not a guarantee that the physical audio clock is synchronized to an application's clock. It serializes access to shared memory while held; scheduling, server buffering and hardware consumption happen over time outside the caller's control. A device can be invalidated between a capacity query and a later request. The correct error path returns the audio result and releases any lock or temporary resource acquired on that branch. For wraparound, the driver can use a temporary buffer before copying into the circular storage. The application should not infer contiguous physical device memory from a contiguous pointer returned by the API.

Audio sessions group streams and policy such as volume and routing within the Windows-facing model. Their lifetime and identity are not necessarily the same as a Pulse server's stream object or a PipeWire node. Muting a Windows session, muting a host node and unplugging a device are different events. A practical diagnostic records the client mode, negotiated format, requested frame count, padding, returned result, driver selection and whether the host endpoint remains available. It should not capture microphone samples or other people's audio to establish those facts.

Media and devices: surveyed boundaries#

Wine's winegstreamer source is an entry point for media integration. Media Foundation and other Windows media interfaces can construct pipelines whose source, decoder, transform and sink must agree on formats and available components. Negotiation failure can come from an unsupported media type, unavailable plugin, missing codec or incompatible source/sink state. A container format, a codec and a decoded pixel or sample format are separate concepts. Merely creating a Windows-facing media object does not prove a particular file can be decoded or rendered.

This media discussion is a survey, not a full worked analysis of Wine's media pipeline. GStreamer plugin and codec versions are separate from the Wine commit and are not pinned as a tested combination here. Proprietary codecs and redistributables have their own licensing and availability constraints. A safe source investigation can inspect negotiation branches and supplied test data without downloading unfamiliar media, opening a camera or recording a microphone. Actual hardware and plugin validation requires a separate authorized environment and documented versions.

Wine's bus and HID support similarly translates selected device interactions through userspace and host interfaces. A Windows controller interface is not the Linux device node itself; the host still applies device permissions, session policy and kernel driver support. A Windows .sys driver is not a Linux kernel module and cannot simply be loaded into the host kernel as one. Some Windows drivers and proprietary devices are unsupported or require separate implementations. This paragraph is an overview, not evidence that a particular game controller, camera, USB device or driver works. No device enumeration or scan is required for the source-only work below.

Format failure and recovery#

Consider a client that requests a companded audio format with an unsupported bit depth. In the inspected Pulse format selection, the A-law and mu-law branches require eight bits per sample and accept only one or two channels; a different bit depth or channel count returns AUDCLNT_E_UNSUPPORTED_FORMAT. That specific branch is stronger evidence than the general statement that negotiation can fail. It does not imply all other formats accept the same channel counts, nor that the host device is absent. The application can inspect supported alternatives through its documented interface and choose a format it knows how to produce; silently relabeling existing samples as another format would change their interpretation and produce incorrect audio.

In Wine 10.0 dlls/winepulse.drv/pulse.c, the format cases for WAVE_FORMAT_ALAW and WAVE_FORMAT_MULAW, LGPL-2.1-or-later (the surrounding switch is omitted):

c
    case WAVE_FORMAT_ALAW:
    case WAVE_FORMAT_MULAW:
        if (fmt->wBitsPerSample != 8) {
            FIXME("Unsupported bpp %u for LAW\n", fmt->wBitsPerSample);
            return AUDCLNT_E_UNSUPPORTED_FORMAT;
        }
        if (fmt->nChannels != 1 && fmt->nChannels != 2) {
            FIXME("Unsupported channels %u for LAW\n", fmt->nChannels);
            return AUDCLNT_E_UNSUPPORTED_FORMAT;
        }
        stream->ss.format = fmt->wFormatTag == WAVE_FORMAT_MULAW ? PA_SAMPLE_ULAW : PA_SAMPLE_ALAW;
        pa_channel_map_init_auto(&stream->map, fmt->nChannels, PA_CHANNEL_MAP_ALSA);
        break;

The two companded tags share one case. It requires an 8-bit sample size and one or two channels, and any other value returns AUDCLNT_E_UNSUPPORTED_FORMAT after a FIXME message. Only when both checks pass does it select the matching Pulse sample format and build a channel map.

A useful failure record separates the requested format from any closest supported format, the selected mode and the actual return value. A retry must use a correctly initialized stream with resources from the failed attempt released according to the API. If the endpoint vanished instead, the client needs to reacquire or select an endpoint according to its policy rather than retrying forever against an invalidated stream. Those alternatives require different evidence and can have different user-visible effects. This source-only example establishes a conditional error branch; it does not claim that a real audio server accepted a retry.

Companded format cases side by side

Synthetic teaching data, not a device capture.

8-bit mu-law, 2 channels
Passes both checks; the sample format is set to PA_SAMPLE_ULAW.
16-bit mu-law, 2 channels
Fails the bit-depth check and returns AUDCLNT_E_UNSUPPORTED_FORMAT.
8-bit A-law, 6 channels
Fails the channel check and returns AUDCLNT_E_UNSUPPORTED_FORMAT.
Other format tag
Takes a different switch branch, which this chapter does not analyze.
Synthetic teaching data. Synthetic cases checked against the Wine 10.0 Pulse format branch; no audio device or server was used, and choosing a supported format remains the client’s job.

Practice#

Run this arithmetic-only model with Python 3. It opens no audio service or device:

sh
python3 - <<'PY'
rate, channels, bytes_per_sample = 48000, 2, 2
capacity, held, requested = 480, 240, 300
print('bytes per frame:', channels * bytes_per_sample)
print('buffer milliseconds:', 1000 * capacity / rate)
print('request fits:', held + requested <= capacity)
print('four-frame-per-second drift in one minute:', 4 * 60)
PY

The expected values are four bytes per frame, ten milliseconds, false and 240 frames. Change the request to 120 for a fitting arithmetic case. This program is not an audio test and does not model a concurrent consumer, resampler or hardware clock. If Python is unavailable, calculate the same values by hand.

For a source-only alternative, use the supplied 240-held/300-requested case and write the outer status, audio result, lock action and reason no buffer can be used. Then trace the 120-frame success assumption through the two quoted counters. Add a row for an invalidated device and explain why it cannot be inferred from a capacity calculation. Deliver the table without opening audio devices or changing host routing.

Knowledge check#

  1. Why can the Unix-call return be success while the audio request has failed?
  2. How many bytes and milliseconds are 480 frames of 48 kHz, 16-bit stereo?
  3. What does releasing a render buffer establish, and what does it not establish?
  4. Why is a Pulse-compatible PipeWire server not proof of a native Wine PipeWire driver?
  5. Why is a Windows .sys file not automatically a Linux driver?

Answers#

  1. The inspected function returns a transport-level STATUS_SUCCESS while writing the Windows-facing failure to params->result. Both layers must be interpreted.
  2. Each frame is four bytes, so 480 frames occupy 1,920 bytes and represent ten milliseconds at the stated rate. Actual end-to-end latency includes other changing buffers and delays.
  3. The driver accounts for the written frames, clears the locked state and attempts to feed the host path. It does not establish that the physical device has already played them.
  4. Wine can speak the Pulse protocol to a compatible server. The source-backed Wine driver in this example is winepulse.drv; server implementation and client driver are different components.
  5. Windows and Linux have different kernel interfaces, binary formats and driver models. Wine's userspace translation and host device permissions do not turn an arbitrary Windows kernel driver into a Linux module.

Checkpoint#

Submit the format and buffer calculation, error-path table and drift explanation. Pass when frames differ from samples, the two return layers are distinguished, and the Pulse client is not described as a native PipeWire driver. Revisit the quoted checks and retry if release is equated with physical playback. This is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.