Skip to content

AC97 Audio Driver

LSantha edited this page Oct 20, 2026 · 6 revisions

AC97 Audio Driver

Intel AC'97 audio driver in the sound subproject: PCI/NAM/NABM setup, Buffer Descriptor List DMA memory, playback and capture engines, WAV I/O, a software mixer, mixer volumes and six shell commands.

Overview

JNode's audio support originally consisted of the PC speaker driver only (org.jnode.driver.sound.speaker.pc.PCSpeakerDriver, which toggles port 0x61 and programs PIT channel 2). The AC'97 driver adds a real DMA audio path for the Intel ICH AC'97 controller found in most chipsets since the late 1990s, and emulated by both QEMU (-device AC97,audiodev=...) and VirtualBox.

The controller is a PCI device (vendor 0x8086, class 04:01). It exposes two I/O register sets through PCI BARs:

BAR Name Contents
BAR0 NAM (Native Audio Mixer) 16-bit codec registers: volumes, powerdown, sample rate, record select/gain, codec id
BAR1 NABM (Native Audio Bus Master) DMA engine register boxes (PCM out, PCM in), global control/status

The driver lives in sound/src/driver/org/jnode/driver/sound/ac97/ and ships in two plugins — org.jnode.driver.sound.ac97 (driver, descriptors in sound/descriptors/) and org.jnode.driver.sound.ac97.command (the shell commands). Both are in all/conf/default-plugin-list.xml.

There is no JNode PCM mixer / javax.sound service to integrate with; the driver is consumed directly through the DeviceAPI mechanism. See Driver-Framework and Resource-Management for the surrounding framework.

Key Components

Class / File Role
AC97Constants.java interface AC97Constants extends PCIConstants: PCI ids, NAM offsets, NABM offsets, SR/CR bits, BDL entry layout, format modes, record sources, sample rates
AC97Driver.java Driver subclass bound by the mapper; PCI sanity checks in verifyConnect; owns AC97Core; implements AC97API
AC97Core.java final class … implements AC97Constants, IRQHandler — all hardware access: resource claiming, AC-link reset, codec init, both DMA engines, positions, stall recovery
AC97API.java interface AC97API extends DeviceAPI — the 33-method consumer surface
AC97Utils.java findDevice(), playTone()
BufferDescriptorList.java Allocates and manages the BDL + sample buffers as JNode DMA memory
ToneGenerator.java Square/sine PCM generator for ac97play
PcmFormat.java Sample-format value object: channels × bits-per-sample, frame/word arithmetic
PcmQueue.java Fixed-capacity byte ring used by the mixer's voices
AC97Mixer.java Software mixer: MixerVoice per stream + a MixerTask daemon thread
MixerVolume.java 5-bit-per-channel volume value object with toRegister/fromRegister/percentToSteps
SampleRateConverter.java Streaming linear-interpolating resampler
WavFile.java RIFF/WAVE header creation and parsing, plus convertToStereo16
command/AC97PlayCommand.java ac97play [frequency] [duration]
command/AC97CaptureCommand.java ac97capture — record with live level reporting and optional WAV output
command/AC97WavCommand.java ac97wav <file> — play a PCM WAV file
command/AC97VolumeCommand.java ac97volume --mute/--unmute/--level N
command/AC97RegCommand.java ac97reg --get N/--set N --value V and a register dump
command/AC97InfoCommand.java ac97info [--buffer-size N] [--mic-gain N] — show and tune the ring

How It Works

1. Driver binding

The plugin descriptor contributes five mappers to the PCI bus extension point — one per supported device id:

<mapper id="8086:2415" name="Intel ICH (82801AA) AC'97"
        driver-class="org.jnode.driver.sound.ac97.AC97Driver"
        class="org.jnode.driver.bus.pci.PCIDeviceToDriverMapper"/>

AC97Core.SUPPORTED_DEVICE_IDS = {0x2415, 0x2425, 0x24C5, 0x24D5, 0x27DE} with SUPPORTED_VENDOR_ID = 0x8086; isSupportedDevice(vendorId, deviceId) and PCI_CLASS_AC97 = 0x0401. PCIDeviceToDriverMapper matches on vendorId:deviceId (hex, colon separated) at match level MATCH_DEVICE, queried before any class-code mapper. AbstractPCIDeviceToDriverMapper instantiates the driver through its ConfigurationElement constructor (falling back to the no-arg constructor). AC97Driver.verifyConnect throws DriverException ("only usable on PCI devices" / "Not a supported AC'97 controller") for anything else; the device manager catches this and leaves the device undriven.

2. PCI setup

  • PCIDevice.getConfig().asHeaderType0() gives the type-0 header; getBaseAddresses() returns the parsed PCIBaseAddress entries (BAR probing is done by the framework: it writes 0xFFFFFFFF and reads back the size).
  • The two I/O BARs are claimed with ResourceManager.claimIOResource(device, base, size): BAR0 → NAM, BAR1 → NABM. Both are wrapped in AccessControllerUtils.doPrivileged because claiming requires the ResourcePermission("ioports") declared by the plugin.
  • The IRQ comes from PCIHeaderType0.getInterruptLine(); the line is validated against 0..15 and claimed with rm.claimIRQ(device, line, this, true) — shared, because PCI lines are frequently shared and the handler ignores interrupts that are not its own.
  • Bus mastering is enabled through the PCI command register:
config.setCommand(config.getCommand()
    | PCIConstants.PCI_COMMAND_IO
    | PCIConstants.PCI_COMMAND_MEMORY
    | PCIConstants.PCI_COMMAND_MASTER);

Without PCI_COMMAND_MASTER the DMA engine may not drive PCI bus cycles at all.

3. Codec bring-up

  • Cold reset: GLOB_CNT (NABM + 0x2C) bit 1 is AC'97 Cold Reset#, active low. The ICH PRM sequence is: clear the bit (assert AC_RESET#), wait 10 ms, set it again (release AC_RESET#), then poll GLOB_STA bit 8 (GLOB_STA_PCR, primary codec ready) for up to a second. Extensions cleared at the same time: GLOB_CNT_ACLINK_OFF (bit 3, shutdown) and the 2/4/6-channel mask (bits 21:20) which is forced back to stereo.
  • Power: NAM 0x26 (NAM_POWERDOWN_CTRL_STAT) has two halves with opposite meanings. The control bits are the high nibble (NAM_POWER_DOWN_PR_MASK = 0x0F00: ADC, DAC, analog, reference) and power a section down when set; the low nibble (NAM_POWER_STATUS_MASK = 0x000F) is the per-section ready status the codec drives. The driver clears the control bits, keeps the external amplifier on (NAM_POWER_EAPD = 0x8000), and polls the status bits until every section reports ready, with a timeout and a register dump. On QEMU the register then reads 0x000f.
  • Register reset: writing anything to NAM 0x00 resets all mixer registers to defaults. Do this before programming volumes — most volume registers default to muted (master = 0x8000), which is the classic "the driver runs but there is no sound" bug.
  • Volumes: NAM 0x02 (master) and 0x04 (headphone) are written with NAM_VOLUME_0DB = 0x0000 (unmuted, 0 dB). NAM 0x18 (PCM out) is initialized to the separate documented NAM_PCM_OUT_DEFAULT = 0x0808 — 8 attenuation steps, about −12 dB, not 0 dB; the deliberate headroom keeps a caller's first samples from clipping. Register 0x0A is the PC-beep volume (NAM_PC_BEEP_VOLUME); 0x08 is the master tone register (NAM_MASTER_TONE).
  • Variable rate: if NAM 0x28 bit 0 (NAM_EI_VRA) is set, VRA is enabled in NAM 0x2A bit 0 and the rate is programmed into NAM 0x2C. Without VRA the codec only accepts 48 kHz.

4. Sample format — PcmFormat

PcmFormat(channels, bitsPerSample) accepts channels ∈ {2, 4, 6} and bits ∈ {16, 20}; anything else throws. STEREO_16 and STEREO_20 are the two predeclared constants.

Method Value
getSampleBytes() 4 for 20-bit (a 20-bit sample occupies a whole 32-bit word), else 2
getFrameSize() channels * getSampleBytes() — 4 bytes for STEREO_16
getWordsPerFrame() (channels * getSampleBytes()) / 2 — this is what the BDL length field counts
wordsIn(bytes) (bytes / frameSize) * wordsPerFrame, throwing IllegalArgumentException if the byte count is not a whole number of frames

The global control register encodes the same two choices: GLOB_CNT_PCM_20BIT (0x00400000) for 20-bit, GLOB_CNT_PCM_4 (0x00100000) / GLOB_CNT_PCM_6 (0x00200000) in bits 21:20, masked by GLOB_CNT_PCM_246_MASK. The codec advertises what it can do read-only in GLOB_STA: GLOB_STA_MULTICHAN_CAP (bits 21:20 → 0/1/2 = 2/4/6 channels) and GLOB_STA_SAMPLE_16_20 (ICH4+ 16 and 20 bit). readCapabilities() decodes both into maxChannels and sample16_20 and logs them, so a 6-channel codec says so in the boot log.

Only 16-bit stereo is playable end to end. setSpeakerFormat(PcmFormat) accepts STEREO_16 and rejects everything else with an UnsupportedOperationException that names the format. The mixer, WAV converter and tone generator all produce 16-bit stereo, and a 20-bit stream counts its descriptor length in 32-bit words rather than 16-bit ones, so a multi-channel request would silently play off pitch. The descriptor ring is nevertheless format aware (see below), so the geometry is correct for all four formats even though the public path pins one.

5. Buffer Descriptor List as JNode memory

The BDL is the scatter/gather table the DMA engine walks. Each of the up to 32 entries is 8 bytes:

Offset Bits Meaning
0x00 31:1 Physical address of the sample buffer (word aligned; bit 0 must be 0)
0x04 31 BD_IOC — interrupt on completion of this buffer
30 BD_BUP — buffer underrun policy (repeat last sample)
29:16 reserved
15:0 number of 16-bit words (2 words = 1 stereo frame; max 65534 — 65536 cannot be encoded as a nonzero field, and only even counts are legal)

BufferDescriptorList holds its PcmFormat, sizes every buffer from getFrameSize() (4 or 8 or 12 bytes per frame — not a hard-wired 4), converts bytes to the 16-bit words the hardware counts, and validates the requested frames-per-buffer. The capture ring is built for 16-bit stereo, which is what capture is. It claims one contiguous block with

mem = rm.claimMemoryResource(owner, null, total, ResourceManager.MEMMODE_ALLOC_DMA);
bdl   = mem.claimChildResource(entryCount * 8, BDL_ALIGN);   // 128-byte aligned
buf[i] = mem.claimChildResource(framesPerBuffer * frameSize, BUFFER_ALIGN); // 8-byte aligned

MEMMODE_ALLOC_DMA makes MemoryResourceImpl allocate from Unsafe.getMinAddress() in 64 KB steps, i.e. from the low DMA-compatible zone. AC'97's BDBAR only holds 32 bits, so any memory works in principle, but the DMA allocator guarantees a physically contiguous block with a stable address, which is what a bus-master descriptor needs. DEFAULT_ENTRY_COUNT = BDL_MAX_ENTRIES (32) and DEFAULT_FRAMES_PER_BUFFER = 1024 (4096 bytes, ~21 ms), so the default ring is 128 KB of DMA memory and getPlaybackLatencyMs() reports 682 ms.

Two checks make the ring safe rather than merely plausible:

  • validateDmaBounds(address, size) covers the whole block, not the start address. A descriptor pointer is 32 bits, so a buffer that starts below 4 GB and ends above it is only partly reachable; toInt() made such a start look fine and the controller read the tail as garbage PCM.
  • The alignment is verified — the descriptor list and every sample buffer — and refused by name with the address and the boundary. A non-power-of-two alignment is rejected as a programming error, because -1 marks every address aligned and silently disables the check.

The descriptor is written through MemoryResource.setInt, which is a plain little-endian 32-bit store — the same byte order the controller expects on x86:

descriptors.setInt(ofs, buffer.getAddress().toInt());                       // pointer
descriptors.setInt(ofs + 4, samples | (ioc ? BD_IOC : 0));                  // control + length

BufferDescriptorList.withBufferUnderrunPolicy(controlWord, set) ORs / AND-NOTs BD_BUP on the control word; setBufferUnderrunPolicy(index, set) is the read-modify-write instance.

6. Playback pipeline

The producer/consumer ring lives in AC97Core:

  • open(rate) resets the DMA register box (CR_RR, wait for the hardware to clear it), programs BDBAR, sets LVI = 0, clears SR, and selects the sample rate.
  • write(pcm, offset, length) splits the PCM data into ring-sized chunks. For each chunk: MemoryResource.setBytes copies into the next free buffer, the descriptor is filled in, LVI is advanced to that index, and CR_IOCE | CR_RPBM is written. While every descriptor is in flight ((lastValidIndex + 1) % n == currentIndex) the writer waits with playbackLock.wait(250).
  • handleInterrupt runs on the JNode IRQ-<n> thread. It reads SR, returns immediately if no interrupt bit of this engine is set (shared line), acknowledges by writing SR_LVBCI | SR_BCIS | SR_FIFOE back (write-1-to-clear), clears the latched GLOB_STA playback bit (GLOB_STA_POINT), refreshes currentIndex from CIV, calls noteControllerProgress() and wakes the writer.

Ordering matters and is cheap here: descriptor contents and sample data must be visible to the DMA controller before LVI is advanced or the run bit is set. x86 is DMA coherent, so no device-side cache flush is needed; readBarrierOut()/readBarrierIn() perform a dummy read of CIV as a store-buffer fence, and JNode's port I/O (IOResource.outPort*) is a VM-intrinsic call, which the compiler never moves across.

When CIV catches up with LVI the engine halts itself (SR_DCH); the next LVI advance (or the re-written run bit) restarts it. On QEMU this is literally what the LVI write handler does.

7. Sample-accurate position from CIV + PICB

getPositionLong() and getQueuedFramesLong() reconstruct how much audio is still buffered from two registers and one remembered array. write() records descriptorSamples[index] = samples before publishing LVI, because the hardware only exposes the current descriptor's remaining count:

public static long remainingSamples(int[] descriptorSamples, int entryCount,
                                     int currentIndex, int lastValidIndex, int picb) {
    if (currentIndex == lastValidIndex) return picb;      // only one in flight
    long samples = picb;                                  // remainder of CIV's descriptor
    int index = nextIndex(currentIndex, entryCount);
    while (index != nextIndex(lastValidIndex, entryCount)) {
        samples += descriptorSamples[index];
        index = nextIndex(index, entryCount);
    }
    return samples;
}

getPositionLong() is totalQueuedFrames - remainingFrames() clamped at 0, so it is monotonically non-decreasing and never claims more progress than the engine actually made. Both accessors are long: an int frame count holds only ~12 hours of 48 kHz stereo and then wraps negative; the int versions were removed rather than kept as truncating wrappers. The CIV/PICB pair is read until it is stable (bounded retries, last read wins), because reading them separately lets the engine advance between the two and counts a buffer twice — a queued count that jumps up instead of counting down. Index helpers: maskEngineIndex(i) = i & BDL_INDEX_MASK (0x1F), nextIndex(lvi,n) = (lvi+1) % n, inFlightCount(lvi,civ,n) = (((lvi - civ) + n) % n) + 1, isRingFull(lvi,civ,n) = nextIndex(lvi,n) == civ.

8. drain() and the underrun policy

public final void drain() throws InterruptedException, TimeoutException {
    synchronized (playbackLock) {
        if (!playing) throw new IllegalStateException("AC'97 playback is not open");
        bdl.setBufferUnderrunPolicy(lastValidIndex, true);   // set BD_BUP on the last descriptor
        while (!isDrainComplete(inNabmWord(PCM_OUT_SR))) {
            checkStalled();                                  // deliberately no recovery
            playbackLock.wait(250);
        }
    }
}

BD_BUP makes the controller repeat the last sample instead of playing garbage past the end of the stream, and then halt with SR_DCH — that halt is what makes end-of-stream detectable. The stream stays open (playing remains true); the next write() re-kicks the engine through ensureRunning(). To discard buffered audio you call close(), not drain(). drain() throws on timeout without attempting stall recovery, because recovery would reset the engine and drop exactly the audio it is waiting for.

9. DMA engine stall recovery

Both engines share STALL_TIMEOUT = 5000 ms and MAX_STALL_RECOVERIES = 3, exposed as the unit-tested predicates hasStalled(lastProgress, now) and isRecoveryExhausted(recoveries).

The budget is split in two on purpose:

  • touchProgress() refreshes only the clock — a producer that keeps queueing into a dead engine must not buy itself a fresh budget.
  • noteControllerProgress() clears the budget — called only from the IRQ handler, i.e. only when the controller itself reported progress.

When a writer or reader blocks and hasStalled is true, tryRecoverFromStall() / tryRecoverCapture() log CIV=0x.. LVI=0x.. SR=0x.., stop and reset the engine, rebuild (playback) or re-arm (capture) the ring, and let the caller continue with fresh data — buffered audio is dropped. After three consecutive recoveries without controller progress the stream fails with TimeoutException("AC'97 DMA engine stalled and did not recover"). MAX_STALL_RECOVERIES must stay <= 5 so a genuinely wedged engine fails in ~15 s rather than blocking a shell forever.

10. Capture — a mirror of the playback ring

The PCM in register box at NABM + 0x00 is the exact same register set as PCM out at NABM + 0x10, offset by PCM_OUT_BOX:

In Off Out Off
PCM_IN_BDBAR 0x00 PCM_OUT_BDBAR 0x10
PCM_IN_CIV 0x04 (RO) PCM_OUT_CIV 0x14 (RO)
PCM_IN_LVI 0x05 PCM_OUT_LVI 0x15
PCM_IN_SR 0x06 PCM_OUT_SR 0x16
PCM_IN_PICB 0x08 (RO) PCM_OUT_PICB 0x18 (RO)
PCM_IN_PIV 0x0A (RO) PCM_OUT_PIV 0x1A (RO)
PCM_IN_CR 0x0B PCM_OUT_CR 0x1B

Global status splits correspondingly: GLOB_STA_PIINT = 0x20 for capture, GLOB_STA_POINT = 0x40 for playback.

AC97Core keeps a completely separate captureLock, a separately claimed captureBdl (CAPTURE_ENTRY_COUNT = 32 × CAPTURE_FRAMES_PER_BUFFER = 512, ~11 ms each) and its own cursor/recovery counters. Playback and capture also have separate rate fields (sampleRate + captureSampleRate) under their own engine locks: the DAC and ADC have separate rate registers, so a capture at 8 kHz must not silently change a running playback rate. openCapture(rate) programs only NAM_PCM_LR_ADC_RATE, defaults to RECORD_SELECT_MIC with zero gain, arms every descriptor up front (no prebuffering — recording has no first-note gap to hide), zeroes the sample buffers so an unwritten buffer reads as silence, and starts the engine at the descriptor the controller is filling. Without that last step a second capture begins with the tail of the first: the register-box reset stops the engine but does not move its index, so the reader started at descriptor 0 while the controller was mid-ring. The last valid index is placed behind the controller's own as well, so the engine always has something left to fill instead of waiting for a reader that is waiting for it.

readCapture(byte[] pcm, int offset, int length) requires a whole number of stereo frames, blocks until the destination is completely filled, and always returns length. Each time the reader exhausts the current descriptor it re-arms it and advances captureLastHandled; if completedCaptureCount(captureCurrentIndex, captureLastHandled, entryCount) is zero it calls tryRecoverCapture() and otherwise waits 250 ms. A re-armed descriptor is followed by a check of SR_DCH and a CR_IOCE | CR_RPBM re-kick if the engine halted in the meantime — without it a recorder stops forever at a ring boundary. After every wait and before any recovery the loop re-checks the capture-open state, and recovery itself refuses to run on a closed stream; release() takes the capture lock, marks capture closed, stops the PCM-in engine and wakes a blocked reader before freeing the rings, so a close or plugin unload can never restart dead hardware or copy out of freed memory.

setRecordSource(select, gainSteps) validates and writes NAM_RECORD_SELECT (0x1C) and the gain to NAM_RECORD_GAIN only. NAM_RECORD_GAIN_MIC (0x1E) holds the microphone boost in bits 9:6, which setMicGainSteps owns, so writing the record gain there as well silently moved the boost of the next capture. The gain is bounds-checked (0..15) at the API boundary and declared with its range in the command syntax, instead of being masked with 0x0F (a gain of 16 wrapped into the neighbouring field). validateRecordSource rejects a value > 0xFFFF and any value whose low byte differs from its high byte: the register is a 7-bit mux per channel (right in bits 6:0, left in bits 14:8), so line-in must be RECORD_SELECT_LINE = 0x0404, not 0x0400. AC97CaptureCommand's Source enum (MIC = 0x0000, LINE = 0x0404) is parsed case-insensitively through a private SourceArgument extends EnumArgument<Source>.

Microphone gain is a separate 4-bit boost field in NAM_RECORD_GAIN_MIC bits 9:6 (validateMicGainSteps rejects < 0 || > 15); setMicGainSteps is a read-modify-write preserving the neighbouring bits.

All six commands take --device N because a machine can have more than one codec; an out-of-range index names the devices that do exist. The ac97capture level window is a CaptureLevels value object (rather than inline command code) that closes on the frames it was asked for, counts a full-scale negative sample as both peak and clip, and reports every 100 ms — its predecessor thresholded on frames but closed on samples, so a 100 ms window fired every 50 ms.

11. WAV — WavFile

WavFile is a static utility over a byte[] header. HEADER_SIZE = 44, MAX_HEADER_SIZE = 4096, FORMAT_TAG_PCM = 1.

createHeader(dataLength, sampleRate, channels, bitsPerSample) writes the canonical 44-byte RIFF/WAVE/fmt /data header with little-endian integers and the derived byteRate = sampleRate * channels * bits/8 and blockAlign = channels * bits/8.

parseFormat(byte[] header, int length) walks chunks from offset 12 with odd-length padding (offset = chunkData + chunkLength + (chunkLength & 1)), rejecting a fmt chunk shorter than 16 bytes, a non-1 format tag, more than 2 channels, a bit depth that is not 8 or 16, and a sample rate outside 4000..48000. One asymmetry is deliberate: if a chunk claims more data than the buffer holds, parsing stops unless the tag is data — a truncated data chunk is a legal partial file, a truncated fmt is not.

A file with metadata chunks behind fmt does not parse inside the canonical 44 header bytes, so the command re-reads into a wider buffer (MAX_HEADER_SIZE = 4096). The retry deliberately fills a fresh buffer and copies it into place rather than issuing a read at an offset: JNode's system-library file read fills from index zero even when asked to start at an offset, which would silently throw the first 44 bytes away. The rest is appended behind the bytes already read.

convertToStereo16(src, srcOffset, length, dst, dstOffset, channels, bitsPerSample) is the format funnel everything else shares: 8-bit input is unsigned with 128 as zero level ((src[ofs] - 128) << 8), mono duplicates into both channels, and an incomplete trailing frame is dropped. convertedLength(length, channels, bits) is (length / frameSize) * 4.

12. Resampling — SampleRateConverter

SampleRateConverter(fromRate, toRate) accepts 8000..96000 Hz. step = (double) fromRate / toRate; convert(src, srcOffset, srcFrames, dst, dstOffset) is linear interpolation with no filter:

p = pos + outFrames * step;
i0 = (int) p;  frac = p - i0;
left  = clamp(l0 + (int) ((l1 - l0) * frac));
right = clamp(r0 + (int) ((r1 - r0) * frac));
// at the end of the chunk, carry the fraction forward:
pos = max(0.0, pos + (outFrames * step) - srcFrames);

(int) truncates toward zero rather than rounding. When i0 + 1 >= srcFrames the last sample is held (l1 = l0), so the end of a chunk degrades to sample-and-hold. Carrying pos across chunks is what makes streaming match a single-call conversion within one frame. outputFramesFor(inputFrames) truncates the sub-frame remainder, so the converter can emit one frame fewer than the exact ratio.

AC97WavCommand creates a converter only when the rates differ: playbackRate(api, fileRate) uses the file rate if it is 48000, or if the codec is VRA-capable and fileRate is one of {48000,44100,32000,22050,16000,11025,8000}; otherwise it forces 48000 and prints ac97wav: resampling <from>Hz to <to>Hz. There is no --resample flag — resampling is automatic.

13. Software mixer — AC97Mixer + PcmQueue

AC97Mixer(api, sampleRate, maxVoices) owns the device: start() opens it and spawns the daemon MixerTask; stop() joins the task (outside the lock, so the in-flight chunk finishes) and closes. FRAMES_PER_CHUNK = 1024 (~21 ms at 48 kHz) and VOICE_QUEUE_BYTES = 48000 * PCM_FRAME_SIZE (~0.5 s).

createVoice() returns a MixerVoice or null when maxVoices is exhausted. Each voice owns a PcmQueue — a fixed-capacity byte ring (buffer, capacity, readPos, writePos, size, closed) with per-method synchronized monitors, notifyAll() signalling, and up to two System.arraycopys per wrap. write chunks by available space and releases the lock between chunks (an inner synchronized (this) inside the loop) so a blocked producer cannot freeze the mixer; waitQuietly() uses wait(100) and swallows InterruptedException on the documented grounds that a blocked producer stays blocked and the close path wakes it. close() sets closed and notifies, so a blocked write returns its partial count and a blocked read keeps draining.

MixerTask zeroes mixBuffer each iteration, pulls up to one chunk from every non-empty voice, mixes it in, and writes one full chunk when any voice was active. When nothing is queued it Thread.sleep(5) rather than spinning. running and voiceCount are volatile so the mixing thread sees a gain change and a voice appearing/disappearing; a device that never accepts a chunk is counted and the task backs off after eight consecutive failures instead of busy-looping at full speed. PcmQueue reports an interrupt to a blocked producer (it used to swallow it, leaving a shutting-down producer blocked until somebody drained the queue), so the producer returns with what it wrote.

Mixing arithmetic is static and unit-testable:

public static void mix(byte[] dst, int dstOffset, byte[] src, int srcOffset,
                       int frames, int gainSteps) {
    // gainSteps 0 = full volume; > 0 attenuates on the codec's own 1.5 dB/step scale
    scaledLeft = (left * (NAM_VOLUME_MAX_STEPS - gainSteps)) >> 5;
    accumulate(dst, offset, scaledLeft, scaledRight);   // additive, clamped, NOT wrapping
}

MixerVoice.setVolume keeps only the left steps (gainSteps = muted ? 31 : volume.getLeftSteps()) because the mixer has one gain per voice, not per channel.

There is no format conversion inside the mixer. AC97Mixer is hard-wired to 16-bit signed LE stereo (PCM_FRAME_SIZE = 4) and has no PcmFormat awareness; all conversion happens upstream in WavFile.convertToStereo16. Because AC97Core.write validates the length against speakerFormat.getFrameSize(), a non-STEREO_16 speaker format is incompatible with the mixer's fixed 4-byte frames.

14. Mixer volumes — MixerVolume

Field Constant Layout
mute (both channels) NAM_VOLUME_MUTE bit 15 (0x8000)
left channel, 5-bit NAM_VOLUME_MASK bits 13:8
right channel, 5-bit NAM_VOLUME_MASK bits 4:0
max attenuation NAM_VOLUME_MAX_STEPS 31 steps × 1.5 dB = −46.5 dB

MAX_LEVEL = 0 (0 dB) and MIN_LEVEL = 31. getLeftDb() / getRightDb() are -1.5 * steps. toRegister() is NAM_VOLUME_MUTE | (left << 8) | right; fromRegister(value) masks both fields and ignores everything else (a 6-bit codec reads back as 0 dB). percentToSteps(percent) rounds to nearest (((100 - percent) * 31 + 50) / 100) and stepsToPercent rounds half up. toString() is "muted", "N%" when both channels agree, or "left N% right M%".

AC97Core exposes get/setMasterVolume (NAM_MASTER_VOLUME 0x02), get/setPcmVolume (NAM_PCM_OUT_VOLUME 0x18) and get/setHeadphoneVolume (NAM_HEADPHONE_VOLUME 0x04) as MixerVolume values, so both channels are written in a single register write and there is never a gap between them.

ac97volume matches --output master|pcm|headphone case-insensitively and applies a level or mute to the selected output alone; with no selector it applies to master and PCM together, which preserves the old behaviour. Headphone used to be readable but not writable from the command at all.

15. Codec register access — ac97reg

validateCodecRegisterIndex(index) rejects index < 0, index > 0x7E and any odd index (the mixer registers are 16-bit wide but byte addressed). getCodecId() is (inNam(NAM_VENDOR_ID1) << 16) | (inNam(NAM_VENDOR_ID2) & 0xFFFF).

Every NAM access goes through ACC_SEMA (NABM + 0x34): waitCodecSemaphore() waits for ACC_SEMA_CAS with a bounded timeout, performs the access, then checks the read-completion status bit GLOB_STA_RCS and clears it. A set RCS means the read did not complete, so inNam returns 0xFFFF. Following Linux, a failed access is still performed and merely counted (codecErrors): registers a codec does not implement never release the semaphore, so refusing to continue would break the ones that do exist. QEMU clears the semaphore immediately and never sets RCS, which is why the failure path can only be exercised on real hardware or a fake.

ac97reg accepts 0x-prefixed hex for the register index, the value and the device selector as well as decimal (-g 0x2e and -g 46 read the same register), through a RegisterArgument that names what it expects when it rejects a value.

ac97reg with no arguments dumps twelve registers: NAM_RESET, NAM_MASTER_VOLUME, NAM_HEADPHONE_VOLUME, NAM_PCM_OUT_VOLUME, NAM_RECORD_SELECT, NAM_RECORD_GAIN, NAM_POWERDOWN_CTRL_STAT, NAM_EXTENDED_AUDIO_ID, NAM_EXTENDED_AUDIO_STATUS_CTRL, NAM_PCM_FRONT_DAC_RATE, NAM_VENDOR_ID1, NAM_VENDOR_ID2 — then the codec id.

16. Ring tuning — ac97info

setPlaybackBufferSize(framesPerBuffer) accepts 1..2048 (MAX_FRAMES_PER_BUFFER) and throws IllegalStateException while playback is open, because the ring geometry is baked into the allocated descriptors. Rebuilding allocates a new BufferDescriptorList with the entry count still pinned at 32, and on ResourceNotFreeException restores the old BDL and reports "Cannot claim a playback ring of N frames per buffer".

getPlaybackLatencyMs() is (entryCount * framesPerBuffer * 1000) / sampleRate: 682 ms at the 1024-frame default, 170 ms at 256 frames per buffer.

ac97info always reports playback open=, ring=<frames> frames per buffer, <ms>ms deep, format= rate= variable rate supported=, capture open=, buffers waiting=, microphone gain= and codec id=, plus the diagnostics codec errors= (semaphore/RCS failures), fifo errors=, last valid buffers= and capture interrupts=.

The interrupt handler counts fifoErrors, lastValidBuffers and captureInterrupts in volatile counters instead of calling log.debug. The handler runs with interrupts disabled and log4j can allocate and take unrelated locks, so the fastest path in the driver must not be the one that can stall the machine. A working playback leaves fifoErrors=0 lastValidBuffers=2 on a 600 ms tone; a 400 ms capture reports 48 capture interrupts.

17. Shell commands

All six are declared in sound/descriptors/org.jnode.driver.sound.ac97.command.xml and implemented as AbstractCommand subclasses.

Command Syntax
ac97play ac97play [--device N] [frequency] [duration] — <sequence> of optional IntegerArguments; defaults 440 Hz / 500 ms
ac97capture --duration N --rate N --gain N --source mic|line --file <path> --device N — all optional; defaults 5000 ms, 48000 Hz, gain 0, MIC
ac97wav ac97wav [--device N] <file> — one mandatory FileArgument
ac97volume --mute | --unmute | --level 0..100 [--output master|pcm|headphone] [--device N]; no arguments reports master, pcm and headphone
ac97reg --get N | --set N --value V [--device N]; decimal or 0x hex, no arguments dumps the twelve registers
ac97info --buffer-size N | --mic-gain N [--device N]

Every option lives inside an <optionSet> with both a shortName and a longName. Without them SyntaxSpecLoader rejects the syntax and the shell falls back to a generic parser — --unmute --level 100 simply did not parse before the <optionSet> wrappers were added.

ac97capture computes totalFrames = rate * durationMs / 1000, buffers the whole recording, and walks every 16-bit word accumulating sample * sample per 100 ms window (windowFrames = rate / 10) to report:

ac97capture: <frames> frames rms=<int> (silent | -12.3 dBFS) peak=<peak> clips=<clips>
ac97capture: recorded <frames> frames (<ms>ms), peak=<totalPeak>, clipped=<totalClips>

A totalPeak == 0 run reports "input is digital silence - check the record source, the gain and the host audio input" and exits 1 — the diagnostic for a wrong --source. With --file, it writes WavFile.createHeader(...) plus the PCM.

ac97capture's level accounting is in CaptureLevels, whose window closes after the frames it was asked for and which counts a full-scale negative sample as both the peak and a clip. With no --output, ac97volume changes master and PCM; --output selects one of master, PCM or headphone, so headphone is now writable as well as reported.

18. Tests

sound/src/test/org/jnode/driver/sound/ac97/AllTests.java is a @RunWith(Suite.class) aggregating 16 classes / 156 @Test methods:

Ac97ConstantsTest, BufferDescriptorListTest, AC97CoreTest, ToneGeneratorTest, WavFileTest, MixerVolumeTest, SampleRateConverterTest, PcmQueueTest, AC97MixerTest, PcmFormatTest, AC97CaptureCommandTest, AC97CommandSyntaxTest, AC97RegCommandTest, AC97VolumeCommandTest, AC97WavCommandTest, CaptureLevelsTest.

The hardware-free logic is deliberately testable: remainingSamples, hasStalled, isRecoveryExhausted, completedCaptureCount, isDrainComplete, mix, validateRecordSource, validateMicGainSteps, validateCodecRegisterIndex, validatePcmRange, validateDmaBounds, BufferDescriptorList geometry for all four formats, CaptureLevels window arithmetic and PcmFormat arithmetic are all static and host-runnable. Ac97ConstantsTest asserts the constants against their documented values rather than against themselves — the earlier self-referential assertions had locked in a wrong power-down value and a "0 dB" PCM level that was really −12 dB.

sound/build-tests.xml runs that single suite with <junit fork="on" haltonfailure="on"> and an XML formatter into sound/build/reports/junit.

Build Wiring

sound was a dormant skeleton until this driver landed; it is now a real subproject:

File Change
all/lib/jnode.xml jnode.antall gained <ant target="@{target}" dir="${root.dir}/sound" inheritall="on" inheritrefs="on"/>, between gui and textui
all/build.xml jnode-sound.jar = sound/build/classes and jnode-sound-test.jar = sound/build/testclasses properties, both added to cp-jnode / cp-test and to the <libalias> list; the sound/descriptors header-check fileset was uncommented
all/conf/default-plugin-list.xml org.jnode.driver.sound.ac97 and org.jnode.driver.sound.ac97.command
sound/build.xml compile of src/driver then src/test; there is no jar task — jnode-sound.jar is the classes directory

Note that the source root is src/driver, not src.

Gotchas & Non-Obvious Behavior

  • Volume registers default to muted. Master volume (0x8000) and PCM out (0x8808) are muted after any NAM register reset; a driver that only writes the codec reset gets silence.
  • BDL length is in samples, not bytes, and counts channels. A 1024-frame stereo buffer is 2048 samples / 4096 bytes — and PcmFormat.getWordsPerFrame() is (channels * sampleBytes) / 2, not bits / 16.
  • The BDBAR low bits are hardwired to 0. The list must be at least 8-byte aligned; 128 bytes is the common choice. Each sample buffer pointer must be word aligned so a sample never straddles a DWord.
  • Always allocate all 32 BDL entries. A shorter list is prefetched past its end (CIV/PIV are 5-bit and walk modulo 32), which silently plays JNode heap memory as PCM. The 32 × 1024 frame default ring is 128 KB of DMA memory and 682 ms of audio.
  • Prebuffer before starting the engine. Starting the DMA on the first filled descriptor turns JVM warm-up time into an audible silent gap at the start of every first stream after boot; queue a few descriptors first (PREBUFFER_DESCRIPTORS = 4).
  • Do not rewrite CR_RPBM while the engine is running. It restarts the descriptor walk from the prefetched index; advance LVI instead and only re-write the control register when SR_DCH says the engine halted.
  • A producer must not clear the stall budget. touchProgress() (clock only) and noteControllerProgress() (clock + budget, IRQ-only) are separate for exactly this reason.
  • Capture needs no prebuffer but does need a re-kick. The recorder arms all descriptors at once; the re-kick after re-arming a descriptor is what prevents a record from stopping dead at a ring boundary when the engine halted.
  • drain() is not close(). drain() marks BD_BUP, waits for SR_DCH and leaves the stream open so the next write() resumes; close() stops the engine and drops the buffered PCM.
  • Line-in is 0x0404, not 0x0400. NAM_RECORD_SELECT is a per-channel mux; setting only the right channel records the left channel as digital silence.
  • The mixer has one gain per voice, not per channel. MixerVoice.setVolume uses the left steps only.
  • The mixer saturates, it does not wrap. clampSample bounds every accumulate to [-32768, 32767], so two loud voices sum to a clipped sample rather than a sign-flipped one.
  • ACC_SEMA (NABM + 0x34) is mandatory. Every codec register access waits for the semaphore and every read checks GLOB_STA_RCS; ignoring both reads stale data on real hardware while looking fine on QEMU, which grants the semaphore immediately and never sets RCS.
  • A descriptor buffer must fit inside 4 GB, not just start there. BDBAR/buffer pointers are 32-bit; a block that starts below the boundary and ends above it must be rejected as a whole, or the controller reads its tail as garbage.
  • Verify alignment, do not assume it. The allocator happens to return 128-byte-aligned DMA blocks, but that is an implementation detail; a wrong boundary arrives as a random playback glitch. A non-power-of-two alignment silently makes the check always pass.
  • The capture reader must start where the controller is. A register-box reset stops the engine without moving its index, so a naive descriptor-0 reader replays the previous recording's tail.
  • Playback and capture rates are separate. The DAC and ADC have separate rate registers; sharing one field makes a capture change a running playback rate.
  • A stopped device fails predictably. release() sets a terminated flag before dismantling anything, and every hardware-touching method checks it; close()/closeCapture() are the exception and return quietly from a finally block.
  • The power-down register is inverted. High-nibble control bits power sections down when set; the low nibble is read-only ready status. Writing 0x000F "to power up" is a no-op.
  • PCM out starts at −12 dB, not 0 dB. NAM_PCM_OUT_DEFAULT = 0x0808 is deliberate headroom; NAM_VOLUME_0DB is 0x0000.
  • The IRQ handler runs on a dedicated high-priority thread, not on an interrupt stack, but it should still stay short: read status, acknowledge, update currentIndex, notifyAll. It services both engines from one method.
  • Cold reset on QEMU is a no-op (the emulator ignores GLOB_CNT reset writes, /* TODO: Handle WR or CR */), but GLOB_STA already reports the primary codec ready, so the driver's wait succeeds immediately. Real ICH hardware needs the full sequence.
  • AC97Core claims and releases in strict order (IRQ, NAM, NABM, BDL memory) and releases in reverse on any constructor failure, mirroring DefaultFDC's documented claim order discipline.

Validation

Validated 2026-10-10 on QEMU 10.2.1 (-accel tcg, -device AC97,audiodev=snd0, -audiodev wav,id=snd0,path=...):

00:00:54,093 INFO  [AC97Core]: AC'97 at 0,4,0 NAM=0x0000C000 NABM=0x0000C400
                            IRQ=11 BufferDescriptorList addr=0x00004000 entries=32 framesPerBuffer=1024
00:00:54,198 INFO  [AC97Core]: AC'97 codec 8384:7600 VRA=true
Check Result
Build sh build.sh cd-x86-lite OK, both plugins packaged into all/build/plugins/
plugin org.jnode.driver.sound.ac97 state active
device pci(0,4,0) started driver:org.jnode.driver.sound.ac97.AC97Driver
BDL address 0x00004000 — low DMA memory, as designed
ac97play 440 500 696 KB of PCM captured on the wav backend
Tone analysis square wave on both L and R, 444.4 Hz as programmed (measured 49.5 half-period samples on the 44100 Hz wav stream — QEMU resamples the 48 kHz voice to the backend rate)
7-note arpeggio 7 clean bursts, both channels identical, zero garbage across three ring wraps
plugin --unload org.jnode.driver.sound.ac97 clean stop, resources released, no errors

Bugs found by these runs:

  1. BufferDescriptorList.setDescriptor originally validated the buffer address with (address & BD_LENGTH_MASK) != address, which is a "below 64 KB" test, not a "below 4 GB" test. It threw IllegalStateException on the first ring wrap (buffer at 0x00010080) after one full ring had played. Now a real check on Address.toLong(), plus the same check for the BDL itself in the constructor.
  2. The BDL was allocated with only 16 entries. CIV and PIV are 5-bit index registers walked modulo 32 regardless of LVI, so the prefetch ran past the end of a 16-descriptor list into the first sample buffer. Sample PCM reinterpreted as a descriptor yields a pointer into the JNode heap (1-2 MB), which is then played as audio: sustained garbage with peaks around 24000 where the tone itself peaks at ~4500. Notes shorter than the ring played clean, which is what hid it. Fixed by always allocating the full BDL_MAX_ENTRIES (32) — exactly what Linux does (lvi = ICH_REG_LVI_MASK).
  3. ensureRunning() re-wrote CR_RPBM after every ring refill. Writing the run bit restarts the descriptor walk from the prefetched index, so this discarded buffered position every ~21 ms and caused drops/repeats. The control register is now written only when the engine is stopped: at stream start, and after SR_DCH shows it halted itself.
  4. The engine was started on the first filled descriptor. On a cold JVM the producer needs tens of milliseconds to generate the next chunk, and the controller turns that into a silent gap at the start of the first tone of every boot. Fixed with prebuffering.

Observations that are hardware/emulator behaviour, not driver bugs:

  • QEMU's wav backend records at 44100 Hz and resamples the 48 kHz AC'97 voice; a written frequency appears in the file multiplied by 44100/48000.
  • After the ring drains, the engine halts with RPBM still set, so QEMU emulates the BD_BUP policy and repeats the last sample: capture continues for a couple of seconds past the tone.
  • QEMU logs audio: Could not create a backend for voice 'ac97.pi'/'ac97.mc' with the wav backend — it only supports playback, so the capture engine has no backend to record into on QEMU and must be validated by ac97capture's own level report.
  • AC97Utils.playTone rounds the half period with rate / (2 * f), so a requested 523 Hz comes out as 533 Hz (~2% sharp). Pitch-accurate rounding needs (rate + f) / (2 * f).
  • The first activation of the PCM out voice warps the first milliseconds of a stream (the codec backend warms up its rate converter), which is audible as a chirp on the opening note. playTone therefore prepends 30 ms of digital silence and uses a 15 ms attack / 20 ms release envelope.

Hardening pass (2026-10-11 → 2026-10-14)

A review-driven series hardened the driver after the first end-to-end validation. Each change was re-validated on QEMU; the test suite grew from 108 to 156:

Area Before After
Codec access plain port I/O ACC_SEMA wait + RCS check, counted failures
Power/volumes wrong constants, self-asserted corrected semantics, value-based tests
Format 16/20-bit, 2/4/6 ch advertised public path pins 16-bit stereo; format-aware ring
Bounds start-address checks whole-block 4 GB check, alignment verification
API offset/length trusted validatePcmRange at the boundary; clean terminated failures
Positions int, two separate reads long; stable CIV/PICB snapshot
Capture shared rate, descriptor 0, both gain regs separate rates, controller-index start, record-gain-only
Release playback engine only both engines, readers woken before rings freed
Mixer unsynchronized flags, swallowed interrupts volatile flags, failure backoff, producer interrupt
Diagnostics log.debug in the IRQ handler volatile counters exposed through ac97info
Commands first device, decimal only, master+PCM --device, 0x hex, --output, ranged --gain

A 300 ms 24 kHz WAV resampled to 48 kHz reports 300 ms (the duration used to be computed from the file rate), a WAV with a metadata chunk parses through the wider read, capture windows report every 100 ms, and ac97info -b 2049 is still refused.

Related Pages

Clone this wiki locally