532 lines
15 KiB
Text
532 lines
15 KiB
Text
== Audio
|
|
|
|
Declared in `<joey/audio.h>`.
|
|
|
|
There are three layers, and on two 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 Atari ST and Amiga the tone voices *are* the hardware the module player
|
|
drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register
|
|
writes and lose. Choose the module layer or the voice and tracker layer per
|
|
scene, never both at once.
|
|
====
|
|
|
|
A failed `jlAudioInit` is not fatal: `jlInit` still succeeds and every audio
|
|
call becomes a silent no-op.
|
|
|
|
=== Constants
|
|
|
|
[cols="3,1,4", options="header"]
|
|
|===
|
|
|Name |Value |Meaning
|
|
|
|
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|
|
|`JOEY_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 maps
|
|
tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the
|
|
other ports the slots are independent of the voice layer.
|
|
====
|
|
|
|
=== jlAudioInit
|
|
|
|
[source,c]
|
|
----
|
|
bool jlAudioInit(void);
|
|
----
|
|
|
|
*Parameters:* none.
|
|
|
|
*Returns:* `true` if the platform has a working audio engine and started it;
|
|
`false` silently otherwise, with the rest of the API still callable but silent.
|
|
|
|
=== jlAudioShutdown
|
|
|
|
[source,c]
|
|
----
|
|
void jlAudioShutdown(void);
|
|
----
|
|
|
|
Tears the engine down. Safe when audio was never initialised.
|
|
|
|
*Parameters:* none. *Returns:* nothing.
|
|
|
|
=== jlAudioFrameTick
|
|
|
|
[source,c]
|
|
----
|
|
void jlAudioFrameTick(void);
|
|
----
|
|
|
|
Hooks the engine into your game loop. Most platforms drive their engines from a
|
|
hardware interrupt and ignore this, but it is required for any port that mixes
|
|
in user context, it advances tracker playback, and on IIgs and Amiga it is what
|
|
fires the next chunk of a streaming sound effect.
|
|
|
|
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 most ports,
|
|
`.amod` on Amiga, `.ntp` on 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` needs help on the Amiga: PTPlayer has no native song-end signal,
|
|
so the module must contain an `E8FF` marker at song end. The `joeymod` tool's
|
|
`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga.
|
|
====
|
|
|
|
=== 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,
|
|
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..`JOEY_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.
|
|
|
|
[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]
|
|
====
|
|
This is called from the audio engine's refill context -- the main thread on DOS
|
|
and ST, but the *audio interrupt* on Amiga and IIgs. It must not block and must
|
|
not allocate.
|
|
====
|
|
|
|
=== 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]
|
|
====
|
|
On IIgs and Amiga the engine plays one chunk at a time and fires the next from
|
|
`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or
|
|
the stream stops after its first chunk. 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 is a 65816 byte loop
|
|
over each 3.5 KB chunk, and it is enough to stall a game for a third of a
|
|
second per chunk of speech. Every other port streams signed PCM natively, so
|
|
there this is simply the plain call.
|
|
|
|
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
|
|
|
|
*Parameters:* as `jlAudioPlaySfxStream`. *Returns:* nothing.
|
|
|
|
=== jlAudioSetPan
|
|
|
|
[source,c]
|
|
----
|
|
typedef enum {
|
|
AUDIO_PAN_LEFT = 0, AUDIO_PAN_CENTER, AUDIO_PAN_RIGHT, 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 only, by choosing the Paula channel side. Paula cannot
|
|
place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own
|
|
channel choice alone.
|
|
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
|
|
all audio at once.
|
|
|DOS, ST |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);
|
|
----
|
|
|
|
Direct hardware tone generator: plays a square wave continuously until the next
|
|
call. Designed for AGI-era music where the engine programs a divisor and lets
|
|
the chip play, so CPU cost during playback is zero. Single voice; uses voice 0
|
|
on platforms with several oscillators. On DOS this programs PIT counter 2 and
|
|
the speaker gate.
|
|
|
|
[cols="1,1,4", options="header"]
|
|
|===
|
|
|Parameter |Type |Description
|
|
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|
|
|===
|
|
|
|
*Returns:* nothing. A no-op on platforms with no reachable tone generator --
|
|
fall back to `jlAudioPlaySfxStream` there if you need synthesised sound.
|
|
|
|
=== 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, about 2 dB per step.
|
|
|===
|
|
|
|
*Returns:* nothing.
|
|
|
|
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses
|
|
YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a
|
|
two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a
|
|
resident square.
|
|
|
|
Voice state is sticky -- a voice keeps its last frequency until you change it
|
|
-- and CPU cost during sustained playback is zero, because the hardware
|
|
oscillates on its own.
|
|
|
|
[IMPORTANT]
|
|
====
|
|
The IIgs is the exception to stickiness: each call is an approximately 80 ms
|
|
retriggered one-shot, so a note held longer must be re-asserted about once a
|
|
frame. It also requires `jlAudioInit`, because the DOC path rides the tracker
|
|
engine. On DOS, ST and Amiga this is direct hardware programming and works
|
|
without `jlAudioInit`.
|
|
====
|
|
|
|
=== 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.
|
|
|
|
Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses
|
|
the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the
|
|
Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident
|
|
noise sample on slot 3 with the same retriggered one-shot model as its tone
|
|
voices.
|
|
|
|
[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 all five ports 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.
|
|
|
|
[NOTE]
|
|
====
|
|
Call `jlAudioInit` first for the song to be audible on the IIgs -- see
|
|
`jlAudioVoice`.
|
|
====
|
|
|
|
=== 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, fired from interrupt context. Pass `NULL` to
|
|
uninstall.
|
|
|`hz` |`uint16_t` |Firings per second.
|
|
|===
|
|
|
|
*Returns:* `true` if installed. Ports without a suitable timer source return
|
|
`false`.
|
|
|
|
[WARNING]
|
|
====
|
|
`fn` runs in interrupt context. Keep it to memory reads and direct hardware
|
|
register writes -- no allocation, no `printf`, nothing blocking.
|
|
====
|
|
|
|
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.
|
|
|
|
=== 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 this is `cli` and `sti`; on ports
|
|
with no audio interrupt both are no-ops.
|
|
|
|
*Parameters:* none. *Returns:* nothing.
|