ALSA, PCM, ASoC, and audio timing

Explain ALSA PCM streams, audio buffers and system-on-chip audio routing.

16 min readChapter 66 of 168Advanced
In this chapter

Before you start#

Review interrupts and deferred work, DMA ownership, and device drivers. We read upstream v6.12; no audio device is needed. A QEMU audio device and its host backend, or an arm64 SoC audio path, each require their own supported configuration. The examples below model timing and source branches; they are not measurements of a particular sound card.

Learning goals#

Trace the ALSA PCM start sequence, explain frame and ring-buffer timing, distinguish a precondition failure from a driver trigger failure, and identify the different responsibilities of PCM core and SoC audio components.

Source walkthrough#

ALSA, PCM, ASoC, and audio timing
  1. PREPARED

    PCM runtime is ready for the start check

  2. Playback data

    Empty playback fails pre-start with EPIPE

  3. Driver trigger

    Successful pre-action reaches the trigger callback

  4. RUNNING or error

    Post action sets state; a trigger failure has a separate path

Source map. Source-based teaching scenario, not a captured execution; consult the lesson for error paths and configuration limits.

ALSA is the Linux sound subsystem. PCM means pulse-code modulation: each sample is a numeric amplitude, and a frame contains one sample for every channel at one instant. Stereo 16-bit PCM has two two-byte samples per frame. At 48,000 frames per second, 480 frames span 10 ms regardless of the number of channels; the memory consumed depends on format and channels.

A PCM substream has a struct snd_pcm_runtime holding state and parameters. In playback, userspace fills a circular buffer and the device consumes it. In capture, the device produces frames and userspace consumes them. Software and hardware pointers describe progress around the ring; advancing one without the other changes available space or data. Wraparound must be interpreted using ALSA's boundary rules, not by assuming a monotonically increasing physical address. An underrun or overrun is an XRUN: the producer and consumer did not keep the required timing relationship.

In pcm_native.c, snd_pcm_start calls snd_pcm_action with snd_pcm_action_start and the intended SNDRV_PCM_STATE_RUNNING. Its documented caller must hold the stream lock. The action table selects pre-, do-, undo-, and post-start functions. This exact v6.12 excerpt from snd_pcm_pre_start is GPL-2.0-or-later (preceding and following lines omitted):

c
	if (runtime->state != SNDRV_PCM_STATE_PREPARED)
		return -EBADFD;
	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
	    !snd_pcm_playback_data(substream))
		return -EPIPE;

The first comparison requires a prepared runtime; an inappropriate state returns -EBADFD. The next condition applies specifically to playback and rejects a start when snd_pcm_playback_data says no data is available. -EPIPE here is an audio stream error, not proof that a Unix pipe failed. Successful pre-start sets the trigger master and timestamp latch in the following lines. The snd_pcm_do_start callback then invokes substream->ops->trigger for the trigger master. If that callback returns -EPIPE, the core sets XRUN state. Post-start records timing and changes state to the requested state. Failure and linked-group handling follow the action framework, so a driver's trigger callback alone is not the entire transition.

In sound/core/pcm_native.c, v6.12, SPDX GPL-2.0-or-later (surrounding lines omitted), the start action table that snd_pcm_action uses to select its phase callbacks:

c
static const struct action_ops snd_pcm_action_start = {
	.pre_action = snd_pcm_pre_start,
	.do_action = snd_pcm_do_start,
	.undo_action = snd_pcm_undo_start,
	.post_action = snd_pcm_post_start
};

The table names four phases, but it does not show the stream-lock handling, the linked-group iteration, or how snd_pcm_action unwinds after an error. Read those in the same file before concluding what happens to other linked substreams when one start fails.

Start: precondition, trigger, post-start
Start: precondition, trigger, post-start1. Caller (stream lock held) to PCM action framework: snd_pcm_start requests RUNNING. 2. PCM action framework to Caller (stream lock held) (return): pre: -EBADFD if not PREPARED; -EPIPE if no playback data. 3. PCM action framework to Driver trigger (snd_pcm_ops): do: trigger(START) for the trigger master. 4. Driver trigger (snd_pcm_ops) to PCM action framework (return): -EPIPE from trigger sets XRUN state. 5. PCM action framework to Caller (stream lock held) (return): post: set requested state, notify timers.Caller (stream lock held)PCM action frameworkDriver trigger (snd_pcm_ops)snd_pcm_start requestsRUNNINGpre: -EBADFD if notPREPARED; -EPIPE if noplayback datado: trigger(START) forthe trigger master-EPIPE from trigger setsXRUN statepost: set requestedstate, notify timers
  1. Caller (stream lock held)PCM action framework

    snd_pcm_start requests RUNNING

  2. PCM action frameworkCaller (stream lock held)

    Return. pre: -EBADFD if not PREPARED; -EPIPE if no playback data

  3. PCM action frameworkDriver trigger (snd_pcm_ops)

    do: trigger(START) for the trigger master

  4. Driver trigger (snd_pcm_ops)PCM action framework

    Return. -EPIPE from trigger sets XRUN state

  5. PCM action frameworkCaller (stream lock held)

    Return. post: set requested state, notify timers

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Caller (stream lock held) → PCM action framework: snd_pcm_start requests RUNNING
  2. PCM action framework → Caller (stream lock held) (return): pre: -EBADFD if not PREPARED; -EPIPE if no playback data
  3. PCM action framework → Driver trigger (snd_pcm_ops): do: trigger(START) for the trigger master
  4. Driver trigger (snd_pcm_ops) → PCM action framework (return): -EPIPE from trigger sets XRUN state
  5. PCM action framework → Caller (stream lock held) (return): post: set requested state, notify timers
Source map. Simplified from snd_pcm_action and its start callbacks in pcm_native.c, v6.12. Teaching model, not a captured trace; linked-group handling and undo paths are omitted.

Worked playback timeline#

Assume a synthetic 48 kHz stereo playback stream with a 480-frame period and 1,920-frame ring. The nominal period is 10 ms and ring duration is 40 ms. Suppose userspace prepares the stream and has committed 960 frames; the device has consumed none. A successful pre-start sees PREPARED and playback data, then the driver's trigger is asked to start. After 10 ms, under this idealized constant-rate example, the device has consumed 480 frames. If the application supplies another 480 before the remaining data runs out, the queued amount returns to 960. If it supplies nothing for another 10 ms, the remaining 480 frames are consumed; further playback cannot be supplied from that queue and an underrun is possible. Exact XRUN reporting and pointer updates depend on hardware and configuration.

Change the initial condition to an empty playback queue. The source's pre-start check returns -EPIPE; the driver trigger is not reached through a successful start action. Change it instead to a non-PREPARED state: the earlier check returns -EBADFD. Neither outcome demonstrates a physical audio defect. For capture, the empty-playback check does not apply: the driver will be producing data, not consuming an initial playback queue.

The nominal arithmetic is a model, not a guarantee about interrupt period, latency, scheduling or sound fidelity. Period notifications, DMA hardware pointers, application wakeup policy, scheduling delays and hardware buffering affect measured results. A large buffer gives scheduling slack but can increase latency; a small one reduces slack. Measure end-to-end latency using a defined test setup rather than equating buffer_size / rate with measured latency.

SoC audio and ownership#

ASoC separates reusable components such as CPU digital audio interfaces, codec interfaces and platform-specific machine descriptions. A board's wiring, clocks and routes matter: finding an ALSA card does not prove the amplifier is powered or that the selected path is routed to a speaker. sound/soc/soc-core.c participates in registering and binding those components. Compare it with PCM core rather than assuming the PCM trigger is directly implemented by every codec.

A DMA ring must remain valid while the device can access it. Stop, synchronization, interrupt completion and buffer release must be ordered by the relevant driver contract. The fact that a userspace call returned is not itself proof that hardware cannot still touch a buffer. Device removal and suspend introduce additional state transitions and are driver dependent. Never change sound controls or trigger a physical device as part of the following source-only practice.

Opening, parameters, and the PCM state machine#

A card is ALSA's grouping for a sound device; a PCM device can expose playback and capture streams, and a stream may have one or more substreams. Opening a PCM device creates access to a particular stream; it does not automatically choose a sample format or start DMA. The snd_pcm_substream links stream direction, driver operations and a snd_pcm_runtime. The runtime records negotiated hardware and software parameters, pointers and stream state for that open instance. Those fields describe an active configuration, not a universal promise that all sample formats or rates are available on the hardware.

A typical userspace sequence is open, negotiate hardware parameters, prepare, transfer samples and start, then drain or drop and close. ALSA library calls and plugins may perform or combine steps, so the exact system calls and transitions depend on the application and device. Hardware parameters include rate, channel count, sample format, buffer size and period size, constrained by the driver and device. The kernel's snd_pcm_hw_params performs refinement and parameter setup; a requested combination may be rejected even if each individual value looked plausible. A successful parameter negotiation does not mean the analog route is powered or that the application has begun sending frames.

After hardware setup, prepare resets the stream for operation. The pre-start code already shown requires PREPARED and, for playback, available data. Start reaches the driver's trigger operation through the action callbacks. RUNNING means the stream is operating according to that state machine; it does not mean a listener heard a sound. PAUSED, DRAINING, XRUN, SUSPENDED and disconnected conditions need separate handling. An application can recover from many XRUN cases by preparing again and re-establishing its buffer and start conditions; recovery may introduce an audible discontinuity and is not guaranteed for every error or device removal. In particular, repeatedly retrying an unavailable device is not equivalent to handling disconnection.

struct snd_pcm_ops is the driver's operation table. Depending on the implementation, it includes open and close, hardware parameter and free callbacks, prepare, trigger and pointer operations. A callback is a contract with the PCM core: it does not run merely because a similarly named function exists somewhere in a driver. The .pointer callback reports hardware progress in frames in the expected range, not a userspace virtual address. The trigger starts, stops, pauses or resumes according to supported commands. Driver code must obey locking and execution-context constraints documented for the callbacks; an interrupt handler cannot freely sleep just because a userspace caller can.

Pointer arithmetic and a longer timeline#

The application pointer appl_ptr tracks frames committed by the application in playback, or frames consumed by it in capture. The hardware pointer hw_ptr represents progress by the device as interpreted and updated by the PCM core. These are logical ring positions with a boundary larger than the physical ring used for wrap handling. Do not subtract their raw values naively across that boundary; ALSA's helpers account for direction, wrap and state. During playback, the difference corresponds to queued data under the applicable rules; in capture it corresponds to data produced but not yet consumed.

In include/sound/pcm.h, v6.12, SPDX GPL-2.0-or-later (the comment block above the helper omitted):

c
static inline snd_pcm_uframes_t snd_pcm_playback_avail(struct snd_pcm_runtime *runtime)
{
	snd_pcm_sframes_t avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;
	if (avail < 0)
		avail += runtime->boundary;
	else if ((snd_pcm_uframes_t) avail >= runtime->boundary)
		avail -= runtime->boundary;
	return avail;
}

The helper adds buffer_size to the hardware position, subtracts the application position, and folds the result back into the boundary range. The capture helper is a separate function with a different formula, so do not reuse this expression for capture. The result is a free-space count for writes, not a prediction of when the device will consume those frames.

Pointers become free space or data
Pointers become free space or datahw_ptr: Device progress, read from the runtime status → Playback free space: Writable frames, bounded by the boundary (adds). buffer_size: Ring size in frames → Playback free space: Writable frames, bounded by the boundary (adds). appl_ptr: Application position, from the runtime control → Playback free space: Writable frames, bounded by the boundary (subtracts). hw_ptr: Device progress, read from the runtime status → Capture available: Readable frames, bounded by the boundary (adds). appl_ptr: Application position, from the runtime control → Capture available: Readable frames, bounded by the boundary (subtracts)addsaddssubtractsaddssubtractshw_ptrDevice progress, read fromthe runtime statusappl_ptrApplication position, fromthe runtime controlbuffer_sizeRing size in framesPlayback free spaceWritable frames, bounded bythe boundaryCapture availableReadable frames, bounded bythe boundary
hw_ptrDevice progress, read from the runtime status
Playback free spaceWritable frames, bounded by the boundary
buffer_sizeRing size in frames
Playback free spaceWritable frames, bounded by the boundary
appl_ptrApplication position, from the runtime control
Playback free spaceWritable frames, bounded by the boundary
hw_ptrDevice progress, read from the runtime status
Capture availableReadable frames, bounded by the boundary
appl_ptrApplication position, from the runtime control
Capture availableReadable frames, bounded by the boundary
Read the text version
  • hw_ptr: Device progress, read from the runtime status → Playback free space: Writable frames, bounded by the boundary (adds)
  • buffer_size: Ring size in frames → Playback free space: Writable frames, bounded by the boundary (adds)
  • appl_ptr: Application position, from the runtime control → Playback free space: Writable frames, bounded by the boundary (subtracts)
  • hw_ptr: Device progress, read from the runtime status → Capture available: Readable frames, bounded by the boundary (adds)
  • appl_ptr: Application position, from the runtime control → Capture available: Readable frames, bounded by the boundary (subtracts)
Source map. Simplified from snd_pcm_playback_avail and snd_pcm_capture_avail in include/sound/pcm.h, v6.12. Boundary wrap correction and the status and control structures are omitted.

For the chapter's 48 kHz, two-channel, 16-bit example, one frame is four bytes. A 480-frame period is 1,920 bytes and 10 ms; a 1,920-frame buffer is 7,680 bytes and 40 ms. These numbers are derived from the stated format. If a driver stores samples in a different physical width or imposes an alignment requirement, the memory size calculation changes; the frame-time calculation still uses the negotiated frame rate. With 960 frames initially queued, an ideal device consumes one 480-frame period in 10 ms, leaving 480. If the application is scheduled 9 ms later and commits 480 more frames before the remaining 10 ms is consumed, the queue is replenished in this simplified timeline. If it is delayed beyond the remaining data, the stream can XRUN. A period is a reporting or processing unit, not a guarantee that the scheduler will wake userspace exactly at each boundary.

The distinction is useful when diagnosing glitches. If the application takes 12 ms to process each 10 ms period indefinitely, it accumulates a 2 ms deficit per period; a larger initial queue delays but does not eliminate the eventual underrun. If average processing fits but occasional scheduling delays exceed the available slack, the distribution of delays matters more than the average. Measure wakeup and pointer progress on a matched system before assigning the cause to the driver, scheduler, storage or audio server. Recording only “buffer is 40 ms” does not establish the full end-to-end latency, which includes application queues, hardware and any external path.

Interrupts, pointer updates, and wakeups#

A driver typically reports a completed period from an interrupt or an appropriate deferred context by calling snd_pcm_period_elapsed. In pcm_lib.c, the v6.12 GPL-2.0-or-later implementation includes this exact excerpt; the guard and surrounding function are omitted:

c
	guard(pcm_stream_lock_irqsave)(substream);
	snd_pcm_period_elapsed_under_stream_lock(substream);

The guard takes the stream lock with interrupt state handling for this scope; the second line calls the locked implementation. That implementation checks runtime, verifies the stream is running, and invokes snd_pcm_update_hw_ptr0. If the stream is not running or the update reports an error, it skips the timer notification path. The function also sends the asynchronous notification shown later in source. Pointer updating and the associated state and wait-queue logic determine when blocked clients can progress; one must read the actual update path and software thresholds to explain a particular wakeup. An interrupt is a report that work progressed, not a promise that every application was immediately scheduled.

For a driver author the error counterexample is significant: a wildly incorrect hardware pointer, a missed interrupt or a trigger failure can produce very different symptoms despite all looking like an audio dropout. The core's start path has a specific -EPIPE branch, while a device disconnect or invalid parameter can return another error. Capture the exact return, stream state and negotiated geometry before concluding that an XRUN occurred. Also record whether the application uses direct hardware PCM or a userspace mixing layer, whose buffers and timing add another boundary.

ASoC components and power routing#

ASoC represents a sound card as a composed system rather than forcing every board to duplicate all codec and controller code. A component represents a reusable audio function; a digital audio interface (DAI) describes a connection endpoint and its configuration. A card and machine-specific description connect CPU-side DAIs, codec DAIs and platform components according to board wiring. DAI links can describe multiple paths, and device-tree or firmware information may participate depending on the platform. The core binds compatible components; failure to register a component, configure clocks or bind the link can prevent the card or a stream from becoming usable. See ASoC core and ASoC overview.

Dynamic Audio Power Management (DAPM) describes a graph of widgets and routes so components can be powered according to active audio paths and control settings. A running PCM stream can be silent if a route, mixer control, clock, amplifier or physical output is not configured; conversely a powered route does not prove correct sample contents. A DAPM graph is not the PCM ring, and changing controls on a real machine can affect other users. Read the graph and relevant control documentation before making board-specific changes, and keep any experiment confined to a disposable guest or dedicated audio device.

Read-only Linux observation lab#

On an authorized Linux host or disposable guest, inspect the running kernel's ALSA inventory without starting playback:

sh
uname -r
cat /proc/asound/cards 2>/dev/null || echo 'ALSA cards inventory unavailable'
cat /proc/asound/pcm 2>/dev/null || echo 'ALSA PCM inventory unavailable'
command -v aplay >/dev/null 2>&1 && aplay --version || echo 'aplay unavailable'

A card listing identifies registered cards; the PCM listing identifies available PCM device entries and directions. These files do not establish the selected rate, a working analog route, or absence of contention. Missing /proc/asound, no cards, an inaccessible device or missing aplay is an environment finding, not evidence that the v6.12 source lacks ALSA. Record the kernel and alsa-utils version; aplay is userspace and separately versioned.

For a capability probe, use only a disposable guest with a dedicated virtual audio device, not a shared host: after choosing an actual guest playback device from /proc/asound/pcm, replace CARD,DEVICE below with its numeric values. The command opens the PCM and may start a short silent stream; it can fail if the guest device is missing or busy. It changes no host audio controls intentionally, but its backend may still route silence through the host, so isolate or disable that backend when needed.

sh
timeout 3s aplay -D hw:CARD,DEVICE --dump-hw-params -t raw -f S16_LE -c 2 -r 48000 -d 1 /dev/zero

Interpret the reported parameter ranges as constraints from that opened device, not proof every combination within every printed range is valid. A failure to open due to missing device or permission yields no capability conclusion. The timeout ends a stuck probe; stop and discard the guest after the lab. Source-only alternative: use the existing PREPARED/empty and wrong-state branches: the former returns -EPIPE, the latter -EBADFD, before the successful trigger path. Deliver a table of actual card/PCM inventory or those two source results, plus the calculated 10 ms period and 40 ms ring; identify which evidence was observed.

Practice#

Run this self-contained userspace calculation on a host with Python 3. It models occupancy, not ALSA ioctls or actual device timing:

sh
python3 - <<'PY'
rate, period, capacity = 48000, 480, 1920
queued = 960
print('period ms:', 1000 * period / rate, 'capacity ms:', 1000 * capacity / rate)
for label, added, consumed in [('period 1', 480, 480), ('period 2', 0, 480), ('period 3', 0, 480), ('period 4', 0, 480)]:
    available = queued + added
    underrun = consumed > available
    queued = max(0, available - consumed)
    print(label, 'queued frames:', queued, 'underrun:', underrun)
PY

Expected output starts with 10.0 ms and 40.0 ms. Queue levels are 960, 480, 0 and 0; only the last modeled step requests more frames than available. Modify the first period's added value to 2,000: the model now exceeds the declared capacity, showing that it omits a real ring's space constraint. Add a check on paper or in a copy and explain why overflowing the ring is not a valid correction for an underrun. Deliver a timing table, both failure conditions from snd_pcm_pre_start, and one limitation of the model. No files or privileged operations are required.

Knowledge check#

  1. Why does an empty playback stream return -EPIPE before the driver's trigger callback in the worked start path?
  2. How do frames, channels, and the sample rate determine period duration?
  3. Why is successful PCM start insufficient to prove audible output on an ASoC board?
  4. What distinguishes the hardware pointer, application pointer, and period notification?

Answers#

  1. snd_pcm_pre_start checks for playback data before the do-action invokes the trigger. That error reports an unsatisfied start condition and does not identify a hardware failure.
  2. Duration is frames divided by frames per second; channels determine samples per frame and storage size. At 48 kHz, 480 frames span 10 ms for both mono and stereo.
  3. The selected route, codec, clocks, amplifier and physical wiring must also be correct; PCM state alone does not establish the complete analog output path.
  4. The hardware pointer tracks device progress, the application pointer tracks committed or consumed frames, and a period notification prompts the core to update and signal progress. A notification does not itself guarantee timely application scheduling.

Checkpoint#

Submit the four-step queue table and explain PREPARED, -EBADFD, -EPIPE, and one ASoC boundary. Ready means the arithmetic is consistent and you distinguish source behavior from the synthetic timing model. If not, repeat the empty-buffer and wrong-state branches from the excerpt. This is a readiness check, not a 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.