joeylib2/docs/ApiAudio.adoc

579 lines
19 KiB
Text

== Audio
Declared in `<joey/audio.h>`.
There are three layers, and on some platforms they share physical hardware:
* *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
voices plus noise at register-write cost, with no CPU mixing. The recommended
music path.
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and
the streaming forms.
[WARNING]
====
On the Amiga the tone voices and the noise *are* the hardware the module
player drives: Paula's four channels. While a `.MOD` plays, `jlAudioVoice` and
`jlAudioNoise` writes fight the player's register writes and lose. On the
plain ST (not the STe build) digital sound -- a module, an effect, or a
stream -- plays through the sound chip's channel A, so tone voice 0 is silent
while it plays and sounds again when it stops; voices 1 and 2 play on. Choose
the module layer or the voice and tracker layer per scene, never both at once.
====
`jlInit` starts audio and `jlShutdown` stops it; a program that wants none
sets `jlConfigT.noAudio`, and every audio call is then silent. Sound that does
not start is not fatal: `jlInit` still succeeds, modules and sound effects are
silent, `jlAudioTickRegister` refuses, and the log says so. The tone and noise
voices, and so `jlMusicPlay`, still play where they drive a sound chip of
their own: on the Amiga, the ST, DOS, and the X68000.
=== Constants
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JL_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM.
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80,
and 0x00 is forbidden (it is the DOC stop byte).
|===
[TIP]
====
Prefer *slot 4* for game sound effects when music is playing. The IIgs plays
tone voices 0..2 on sound effect slots 0..2 and the noise on slot 3: an effect
and a voice on the same slot cut each other off, and a voice still being held
takes its slot back at the next `jlAudioFrameTick`. On the Amiga effects, the
module, the voices, and the noise share Paula's four channels, so an effect can
take a channel a voice is using. On the other ports the slots are independent
of the voice layer.
====
=== jlAudioFrameTick
[source,c]
----
void jlAudioFrameTick(void);
----
Hooks the engine into your game loop. Every port does its sound work here: DOS,
the ST, the X68000, DVX, and the hosted ports mix or refill their sound buffers
in it, so without it modules and effects stop or repeat; the IIgs and Amiga
feed each streaming sound effect its next chunk, notice a module's end, and
(on the IIgs) keep held notes sounding; and on every port it advances
`jlMusicPlay`.
Call it once per frame, unconditionally.
*Parameters:* none. *Returns:* nothing.
=== jlAudioPlayMod
[source,c]
----
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
----
Begins module playback. If a module is already playing it is replaced.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on every port
but the IIgs (an `.amod`, with its end marker, plays on the Amiga too), `.ntp`
on the IIgs. The asset pipeline produces the right form.
|`length` |`uint32_t` |Blob size in bytes.
|`loop` |`bool` |`true` plays forever.
|===
*Returns:* nothing.
[NOTE]
====
`loop = false` plays the song once and stops it. The IIgs's replayer has a
play-once mode; the ports that mix modules in software stop where the song
comes back to a row it has played (its end, or a jump back), at an `F00`, or
at the Amiga's `E8F` stop marker; and the Amiga adds that marker as it copies
the module to chip RAM, sending the song's jumps back to it
(<<_music_protracker_modules>>). A module with no room for one, all 128 song
positions or all 128 patterns in use, loops there anyway, and so does one
whose jump back sits in a pattern it also plays where that jump goes
forward.
====
=== jlAudioStopMod
[source,c]
----
void jlAudioStopMod(void);
----
Stops the current module and resets the playhead, so the next `jlAudioPlayMod`
starts from the top.
*Parameters:* none. *Returns:* nothing.
=== jlAudioIsPlayingMod
[source,c]
----
bool jlAudioIsPlayingMod(void);
----
*Parameters:* none.
*Returns:* `true` while a module is producing output. `false` after a stop,
once a song played with `loop = false` has ended, before the first play, and on
platforms where audio init failed.
=== jlAudioPlaySfx
[source,c]
----
void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample,
uint32_t length, uint16_t rateHz);
----
Triggers a one-shot digital sound effect. If the slot is already playing, the
new sample replaces it.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`slot` |`uint8_t` |Slot 0..`JL_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|`length` |`uint32_t` |Sample count in bytes.
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
re-pitches to its own output rate.
|===
*Returns:* nothing.
Most ports read the sample where it is while it plays, so keep it valid and
unchanged until it has finished or the slot has been stopped. The IIgs also
remembers the last sample each slot played by its address, length, and rate,
and plays its own copy when they match: changed bytes at the same address,
length, and rate are not heard there.
[NOTE]
====
The full -128..127 range is accepted everywhere, but the IIgs plays -128 as
-127: its DOC treats the corresponding byte as a wave-end marker and would halt
the voice. One level in 256, inaudible in practice.
====
=== jlAudioStreamFillT
[source,c]
----
typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
----
Stream refill callback. Write up to `count` signed 8-bit samples to `dst` and
return how many you wrote. Returning 0 signals end of stream and the slot
deactivates.
[WARNING]
====
On every port it is called in program time, from the call that starts the
stream and then from `jlAudioFrameTick` (on DVX, also while the desktop runs).
It must not block, and it should be quick: it runs inside those calls.
====
=== jlAudioPlaySfxStream
[source,c]
----
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill,
void *ctx, uint16_t rateHz);
----
Plays a streaming sound effect, pulling samples from your callback as the
slot's prefetch buffer drains. Sound effects can therefore be arbitrarily long
with no caller-side allocation.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`slot` |`uint8_t` |Slot 0..4. Replaces whatever is playing there.
|`fill` |`jlAudioStreamFillT` |Refill callback.
|`ctx` |`void *` |Opaque pointer handed back to the callback.
|`rateHz` |`uint16_t` |Rate your callback produces; the engine resamples with
linear interpolation.
|===
*Returns:* nothing.
[IMPORTANT]
====
Every port feeds streams from `jlAudioFrameTick`, so *you must be calling
`jlAudioFrameTick` every frame* or the stream stops. On the IIgs and Amiga the
engine plays one chunk at a time (7 KB on the IIgs, 4 KB on the Amiga) and
fires the next from it, so a chunk seam on those ports can carry up to one
frame of jitter.
====
=== jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw
[source,c]
----
uint8_t jlAudioSfxRawFormat(void);
void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
void *ctx, uint16_t rateHz);
----
`jlAudioSfxRawFormat` returns this port's raw stream format, either
`JL_SFX_RAW_SIGNED8` or `JL_SFX_RAW_UNSIGNED8_NOZERO`.
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
delivers bytes *already in that format*, so the port skips its per-byte
conversion of every chunk. On the IIgs that conversion costs about 7
microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw
format is signed PCM, so there this is simply the plain call (DVX still flips
each byte for its sound library).
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
*Parameters:* as `jlAudioPlaySfxStream`. *Returns:* nothing.
=== jlAudioSetPan
[source,c]
----
typedef enum {
JL_AUDIO_PAN_LEFT = 0, JL_AUDIO_PAN_CENTER, JL_AUDIO_PAN_RIGHT, JL_AUDIO_PAN_COUNT
} jlAudioPanE;
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
----
Places a sound effect slot in the stereo field. Slots start centred, and the
setting sticks to the *slot*, surviving retriggers -- so set it once for a
sound that should come from one side.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`slot` |`uint8_t` |Slot 0..4.
|`pan` |`jlAudioPanE` |Placement. Out-of-range values are ignored.
|===
*Returns:* nothing.
What each port can actually do:
[cols="1,4", options="header"]
|===
|Platform |Behaviour
|IIgs |All three. The DOC pans per oscillator, so a centred sound plays on two
oscillators, one per side. A stock IIgs sums to mono, so this shows up only on
a stereo card or an emulator.
|Amiga |Left and right by choosing the Paula channel side; a Paula channel plays
on one side only. A centred sound plays on a free channel each side when both
sides have one, so it reaches both ears; otherwise on the one channel
PTPlayer picks.
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
all audio at once.
|DOS, ST, STe |Ignored. Output is mono.
|DVX |All three, through DVX's stereo mixer (on a stereo card).
|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo.
|Win16 |Ignored. Output is mono.
|===
=== jlAudioStopSfx
[source,c]
----
void jlAudioStopSfx(uint8_t slot);
----
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`slot` |`uint8_t` |Slot to silence. A no-op if already idle.
|===
*Returns:* nothing.
=== jlAudioTone
[source,c]
----
void jlAudioTone(uint16_t freqHz);
----
A single tone that plays until the next call: `jlAudioVoice(0, freqHz, 0)`,
tone voice 0 at full volume, on every port. Designed for AGI-era music where
the engine programs a divisor and lets the chip play, so CPU cost during
playback is zero. A program that mixes it with `jlAudioVoice` or `jlMusicPlay`
shares voice 0 with them. On DOS without an OPL2 (AdLib) chip, voice 0, and so
this tone, is the PC speaker.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|===
*Returns:* nothing. Silent wherever voice 0 is (see `jlAudioVoice`).
=== jlAudioVoice
[source,c]
----
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
----
Three-voice PSG-style polyphonic tone generator.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`voice` |`uint8_t` |Voice 0, 1, or 2.
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice.
|`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest,
15 silent, 2 dB per step as near as the chip comes (the ST's YM2149 has
sixteen levels about 3.5 dB apart, so a step there is within 1.6 dB).
|===
*Returns:* nothing.
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2 (with no OPL2,
voice 0 is the PC speaker and voices 1 and 2 are silent); the ST uses YM2149
channels A, B, and C (on the plain ST voice 0 is silent while digital sound
plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample;
the IIgs uses Ensoniq DOC oscillators playing a resident square on sound
effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each,
so its tones are sine waves; DVX plays square tones of its sound library,
which puts them on the FM chip when there is a free voice and mixes them
otherwise; the hosted ports mix square waves in software. The OPL2's waves
are not square either.
[IMPORTANT]
====
A note sounds until the next call for that voice. The IIgs plays each note as
a sample a fraction of a second long and restarts the notes being held from
`jlAudioFrameTick`, so call that every frame, as every program should; a frame
much longer than 80 ms can leave a gap in a held note there.
====
=== jlAudioNoise
[source,c]
----
void jlAudioNoise(uint8_t pitch, uint8_t atten);
----
Noise and percussion generator -- the fourth logical voice of the tracker
layer.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`pitch` |`uint8_t` |0..31 in YM2149 noise-period style: 0 is the brightest
hiss (hats), 31 the lowest rumble (toms).
|`atten` |`uint8_t` |As `jlAudioVoice`. 15 or above silences.
|===
*Returns:* nothing.
The noise runs at 125000 / `pitch` Hz (`pitch` 0 as 1), the YM2149's rate, on
every port as far as its chip goes. Mapping: the ST uses the YM2149 noise
generator mixed onto channel C; the X68000 the YM2151's noise, at the same
rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose
fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone
of its sound library; and the hosted ports mix the YM2149's own noise in
software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2
rhythm-mode snare, leaving the three tone channels untouched: a tuned drum,
not free-running noise, whose pitch spreads over the snare's range rather than
following the YM2149's rate.
[WARNING]
====
On the ST, noise shares channel C's volume register with tone voice 2. Author
noise hits and voice-2 notes so they do not sound simultaneously; `songbake`
warns when they overlap.
====
=== jlMusicPlay
[source,c]
----
bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
----
Plays a JYM1 event stream baked by `songbake` through the tone and noise layer.
One authored song plays on every port at register-write cost. Because there
is no CPU mixing, this is fully viable on the ST and cheap on DOS, which
`.MOD` playback is not.
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so
tempo holds across 50, 60, and 70 Hz ports.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`data` |`const uint8_t *` |JYM1 stream. *Read in place* -- it must stay valid
until `jlMusicStop` or song end.
|`length` |`uint32_t` |Stream size in bytes.
|`loop` |`bool` |`true` restarts at the authored loop point, or the top if the
song has none. `false` silences the voices at song end.
|===
*Returns:* `true` on success; `false` on a malformed stream, in which case
nothing plays. With `jlConfigT.noAudio` the song runs silently, its time and
its end as usual.
=== jlMusicStop
[source,c]
----
void jlMusicStop(void);
----
Stops tracker playback and silences tones and noise. The playhead is discarded;
use `jlMusicPause` to keep it. Safe when nothing is playing.
*Parameters:* none. *Returns:* nothing.
=== jlMusicIsPlaying
[source,c]
----
bool jlMusicIsPlaying(void);
----
*Parameters:* none.
*Returns:* `true` while a stream is loaded and active. Paused counts as active;
stopped or finished does not.
=== jlMusicPause / jlMusicResume
[source,c]
----
void jlMusicPause(void);
void jlMusicResume(void);
----
Pauses in place -- voices go silent, the playhead keeps its position -- and
picks up where it left off. The current row restarts, and wall-clock time spent
paused is not fast-forwarded through. Both are safe no-ops when there is no
active song.
*Parameters:* none. *Returns:* nothing.
=== jlMusicSetAtten
[source,c]
----
void jlMusicSetAtten(uint8_t atten);
----
Global music attenuation, added to every authored voice and noise attenuation.
Applies immediately to sounding voices and to everything played afterwards.
Step it over successive frames for fades.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`atten` |`uint8_t` |0 plays as authored, 15 is silent.
|===
*Returns:* nothing.
[NOTE]
====
`jlMusicPlay` does *not* reset this, so a fade-out carries into the next song
unless you clear it.
====
=== jlAudioTickRegister
[source,c]
----
typedef void (*jlAudioTickFnT)(void);
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
----
Registers a periodic callback for sound schedulers needing better than
frame-rate resolution. PSG-era music drivers use this to write register changes
at exact note boundaries: frame-rate scheduling batches several notes into one
frame, so the chip only hears the last write and short notes vanish.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`fn` |`jlAudioTickFnT` |Callback, which may be fired from an interrupt. Pass
`NULL` to uninstall.
|`hz` |`uint16_t` |Firings per second.
|===
*Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or
did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or
above the mixer's rate on the hosted ports). Uninstalling always returns
`true`.
[WARNING]
====
`fn` may run in an interrupt: it does on DOS, the Amiga, the ST, and the
X68000. Keep it to memory reads and direct hardware register writes -- no
allocation, no `printf`, nothing blocking -- and share data with it through
`jlAudioCriticalEnter` and `jlAudioCriticalExit`.
====
On every port the calls average exactly `hz` a second; where they fall
differs. DOS, the Amiga, and the hosted ports place each one at its own time.
The ST and X68000 deliver them from the system's own timer, 200 and 100 times a
second, so the calls due between two of its ticks come together. The IIgs and
DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls
come together at it.
On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick
counter advancing at the canonical 18.2 Hz through an accumulator so `time`
stays correct. `jlMillisElapsed` carries on from where it was when the tick
starts or stops; it never jumps back.
On the Amiga a `timer.device` request on the E-clock raises a software
interrupt at each tick. On the ST the tick rides TOS's 200 Hz Timer C
interrupt, and on the X68000 Human68k's 100 Hz Timer C interrupt.
On the IIgs `fn` runs in program time, at `jlAudioFrameTick`, as many times as
the display's beats since the last frame have made due, so the average is
exact whatever the program's frame rate.
On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's
sound library, which `jlAudioVoice` and the rest go through there, may not be
called from an interrupt. `fn` may therefore do anything, but what it does
lands on the frame, not between frames.
On the hosted ports the sound is mixed in program time, a few frames ahead,
and `fn` is called by the mixer between exactly the two samples where its tick
falls: in program time, so it may do anything, yet a note it starts begins at
the right place in the sound. `hz` may be anything up to the mixer's rate,
44100 (22050 on Win16).
=== jlAudioCriticalEnter / jlAudioCriticalExit
[source,c]
----
void jlAudioCriticalEnter(void);
void jlAudioCriticalExit(void);
----
Critical section against the tick interrupt. If you mutate state that the
registered tick callback reads, wrap the mutation in these so the interrupt
cannot observe a half-written value. On DOS, the Amiga, the ST, and the
X68000 a section holds every interrupt off, so keep it short. Sections nest:
only the outermost pair does anything, and its Exit puts back the interrupt
state its Enter found. On the IIgs, DVX, and the hosted ports, whose
callbacks run in program time, they do nothing, and need not.
*Parameters:* none. *Returns:* nothing.