Skip to content

audio: play RAD1 and O2 audio at the guest's rate, plus an audio test suite - #356

Open
jamesbraid wants to merge 4 commits into
techomancer:mainfrom
jamesbraid:rad-rate
Open

jamesbraid wants to merge 4 commits into
techomancer:mainfrom
jamesbraid:rad-rate

Conversation

@jamesbraid

@jamesbraid jamesbraid commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #354.

The card ran its analog input and output at a fixed 44.1 kHz and stored the clock generator words without reading them. IRIX sets the output rate through those words for every program that plays sound, and keeps the last rate when the program exits. A 48 kHz sound, the rate IRIX boots with, played 9% slow and flat. An 11025 or 22050 Hz game played wrong, and so did every sound after it until something set 44.1 kHz again.

The decode follows the public Linux RAD1 driver:

  • 0xa0 selects generators from bit 28 and holds the divisor less one
  • 0xa4 holds the remainder and its denominator
  • each generator's word in 0xa8..0xb4 picks the 176.4 or 192 kHz master.

Generator 0 clocks the analog input and 1 the output. A rate change on a running channel takes effect from the write. The WAV capture stays 44.1 kHz, converted from the card's rate, and the host stream's resampler follows the rate. The power-on rate before IRIX programs a generator is unknown, so the card keeps 44.1 kHz until then.

sgiquake and sgidoom both set 11025 Hz. Before this change the card read their ring four times too fast and replayed stale samples: Doom's 0.51 s pistol shot came out as about 4.3 s of noise, and Quake's 0.16 s talk sound as 2.1 s. With it they play in 0.53–0.57 s and 0.21 s.

Audio suite

test/audiotest is the audio counterpart of test/gltest. A small guest program built with MIPSpro plays a 1 kHz, 2 s tone through the audio library, or writes it as an AIFF for playaiff and sfplay. The sweep covers 8 to 48 kHz, mono and stereo, 8 and 16 bit, plus a case that plays 44.1 kHz after a 22.05 kHz program. check.py measures each tone's length and pitch in the IRIS_OCTANE_AUDIO_WAV capture. clanker/devices/audio-test-suite.md has the results and how to run it.

On an Octane running IRIX 6.5.30, all 86 cases pass with this change. Without it, 72 fail: only 44.1 kHz played right.

O2 at 8 and 11 kHz

The suite reads the O2's IRIS_O2_AUDIO_DUMP too, and it found a separate O2 bug, fixed in the last commit. IRIX keeps only about 2 ms of sound queued in MACE's ring, computed from the read pointer it reads back. iris only advanced that pointer on the service thread, which backs off to a 2 ms poll when CRIME is idle. So at 8 and 11 kHz IRIX's lead fell a frame short, the ring ran dry, and a zero landed in the tone about every 33 samples, playing it up to 17% sharp. A read of an audio register now brings the rings up to date first, as serial DMA already does. All 86 cases pass on the O2 after it.

The Indy and Indigo2 aren't covered yet, because HAL2 only records on the host side.

Also

The last commit fixes an unrelated IOC3 test that failed now and then under load (host_input_arrives_in_the_receive_ring_after_the_timer). The model was right; the test assumed it ran inside the 32 ms receive timer's window. It now holds the deadline off while it reads.

The card ran its analog input and output at a fixed 44.1 kHz and stored
the clock generator words without reading them. IRIX sets the output
rate through those words for every program that plays sound, and keeps
the last rate when the program exits. A 48 kHz sound, the rate IRIX
boots with, played 9% slow and flat; an 11025 or 22050 Hz game played
wrong, and so did every sound after it until something set 44.1 kHz
again. That is techomancer#354.

The decode follows the public Linux RAD1 driver: 0xa0 selects
generators from bit 28 and holds the divisor less one, 0xa4 holds the
remainder and its denominator, and each generator's word in 0xa8..0xb4
picks the 176.4 or 192 kHz master. Generator 0 clocks the analog input
and 1 the output. A rate change on a running channel takes effect from
the write. The WAV capture stays 44.1 kHz, converted from the card's
rate, and the host stream's resampler follows the rate.

The tests hold the words IRIX 6.5.30 writes for 8 to 48 kHz. A 2 s
1 kHz tone at each of 7 rates, mono and stereo, 8 and 16 bit, now plays
for 2.0 s at 1000 Hz through playaiff and sfplay; before, only 44.1 kHz
did.
The rate fix was checked by hand with a handful of files. Add the suite
the GL tests have: test/audiotest plays a 1 kHz, 2.0 s tone in the guest
at 7 rates, mono and stereo, 8 and 16 bit, through the audio library
(audiotest, built with MIPSpro) and through playaiff and sfplay on AIFFs
that audiotest writes itself, so no audio file is committed or made on
the host. Two sticky-rate cases play 22050 Hz and then 44100 Hz.

host.sh drives a booted Octane or O2 and notes the capture's size around
each case; check.py slices IRIS_OCTANE_AUDIO_WAV, or the O2's guest-rate
IRIS_O2_AUDIO_DUMP, and passes a case when its length is within 5% of 2.0 s
and its pitch within 3% of 1000 Hz.

Measured on IRIX 6.5.30: on the Octane 86 of 86 cases pass with the rate
fix, and 72 fail on upstream b188bb9 (everything but 44.1 kHz). On the O2,
62 of 86 pass; the 24 at 8000 and 11025 Hz play at the right rate but with
extra samples in the tone (pitch 1036 to 1166 Hz). The Indy and Indigo2
are not covered: HAL2 has no guest-rate capture yet.
O2 sound at 8000 and 11025 Hz played sharp and about 4% long through
the audio library, playaiff and sfplay alike. The guest-rate dump showed
the right rate but zeros inserted mid-tone, one in about 33 at 8 kHz.

IRIX feeds an output ring by reading MACE's status register about once
a millisecond and setting the write pointer about 2 ms past the read
pointer it finds: 17 frames at 8 kHz, 16 after the block rounding. The
engine only moved the read pointer on the service thread's passes, and
since techomancer#343 those come about every 2.1 ms on a quiet machine. IRIX then
computed its lead from a pointer up to 2 ms stale, a pass wanted 16 or
17 frames where 16 or fewer were queued, and each missing frame played
as a zero. At 16 kHz and up the lead rounds to enough spare frames to
cover the pass.

A read of the audio registers now moves the rings up to now first, as
the ISA block's reads already do for the serial DMA. What the outputs
play there is kept until the service thread's pass, which dumps and
plays it in order, so a capture write that stalls that thread (up to
a second on a network filesystem) no longer holds the rings back.

The O2 audio suite now passes 86 of 86 cases, 8 kHz at 2.000 s and
1000 Hz.
host_input_arrives_in_the_receive_ring_after_the_timer failed once with
"two held in the packer". The test rig never starts the chip,
so no receive thread or timer is involved: service() starts the 32 ms
receive timer from the wall clock, and every register access first
expires any deadline already past. A test thread descheduled for over
32 ms between service() and the RX_PRODUCE read made that read post the
entry itself, so the packer was already empty when the test looked.
A 33 ms pause at that point fails the test every time.

The model is right here. The guest sees the same thing on a late read,
and the partial entry still reaches the ring. The test assumed it would
run inside the timer's window.

The test now checks that the deadline is 32 ms after the character,
holds it off while it reads the held count and the arm, then restores
it and sleeps until it passes. That also replaces the fixed 60 ms sleep.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

messed up audio in sgiquake

1 participant