== Audio Declared in ``. 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 on the machines' sound chips (DVX and the hosted ports mix them in software). 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. The mix is the same on every port, within 3 dB: a sound effect at full scale, and a module channel at full volume playing a full-scale sample, are each as loud as a tone voice at attenuation 0 (a square wave sampled at full scale peaks where the voice does), and the noise at attenuation 0 is as loud as a voice, so modules, effects, voices, and `jlMusicPlay` keep their balance from one machine to the next. The ports that mix in software put that peak at a quarter of full scale, so the three voices and the noise together, or a module's four channels, fit without clipping (the plain ST is the exception, <>). === 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. It goes silent at once on every port: the ports that mix ahead of the sound chip (DOS and the X68000) take it back out of what they have mixed, leaving the sound effects there playing on, as they start a new module, or a new sound effect, in what is mixed already rather than after it. *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. Any rate plays at its pitch: one faster than the Amiga's or the IIgs's chip skips samples there (<>). |=== *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. Any rate plays at its pitch, as for `jlAudioPlaySfx`. |=== *Returns:* nothing. [IMPORTANT] ==== Every port feeds streams from `jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or the stream stops. On the IIgs the engine plays one chunk at a time (7 KB) and fires the next from it, so a chunk seam there can carry up to one frame of jitter. The Amiga loops an 8 KB ring that `jlAudioFrameTick` tops up as it plays, with no seams; if the calls stop for longer than the ring lasts (a third of a second at 22 kHz), it plays the ring's old contents again. ==== === 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. DVX's raw format is the IIgs's too, since its sound library's 8-bit PCM is unsigned, and it skips its flip of every byte. Every other port's raw format is signed PCM, 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 { 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 centered, 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 |Behavior |IIgs |All three. The DOC pans per oscillator, so a centered 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 centered 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 |All three on an SB16, which plays stereo: the module's channels on Paula's sides, as on the Amiga, and each effect on its side. Ignored on the Sound Blasters before it, which play mono. |ST |Ignored. Output is mono. |STe build |All three: the DMA chip plays stereo, the module's channels on Paula's sides, as on the Amiga, and each effect on its side. |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 and the hosted ports mix square waves in software (DVX through a stream of its sound library's mixer; on a machine whose only sound is the PC speaker, DVX's own square tones instead). 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; and DVX and the hosted ports mix the YM2149's own noise in software (its 17-bit generator), so it sounds as on the ST (DVX with only the PC speaker: a noise tone of its sound library). DOS mixes the same generator into the Sound Blaster's stream, at about 22 kHz, so pitches 0 to 5 sound alike there; an AdLib with no Sound Blaster plays the OPL2 rhythm-mode snare instead, 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 wherever a sound chip makes it (DVX and the hosted ports mix it in software, and DOS mixes its noise into the Sound Blaster's stream), so it 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 the mixer's rate on DVX and 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, DVX, 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 delivers 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 and the hosted ports the sound is mixed in program time, a few frames ahead (on DVX by its sound library's pump, which DVX also runs while the desktop has the processor), 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 on the hosted ports (22222, or 22050 where the card refuses that, on Win16), and on DVX the sound card's mixing rate. DVX's ticks keep time on a machine with no sound card too: its null driver runs the mixer as a card would. === 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.