There was no audio API at all: SDL3_mixer was vendored and AKGL_REGISTRY_MUSIC was declared, but nothing opened a mixer, and a mixer plays recordings anyway. BASIC 7.0's SOUND, PLAY, ENVELOPE and VOL describe notes, so this adds a tone generator -- three voices, five waveforms, an ADSR envelope each, mixed to one float stream and fed to an SDL_AudioStream. The voice table works whether or not a device is open. akgl_audio_init connects it to one; a host that owns its own audio pipeline can call akgl_audio_mix instead. That is also what makes the tests deterministic: a device pulls samples on SDL's audio thread whenever it likes, so tests/audio.c mixes by hand and only opens a device in its last test. Phase is computed from the frame counter rather than accumulated, because a float increment of hz/44100 added 44100 times a second walks a held note off pitch. A voice that has never been configured defaults to a square wave at full level, since a zeroed voice would have a sustain of 0.0 and make no sound with no error to explain why. Voices summing past full scale are clamped rather than scaled, so one voice plays at the level it was asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
205 lines
8.3 KiB
C
205 lines
8.3 KiB
C
/**
|
|
* @file audio.h
|
|
* @brief Declares the public audio API.
|
|
*
|
|
* A small tone generator: a fixed set of voices, each with a waveform, a
|
|
* frequency, a gate length and an ADSR envelope, mixed to one stream of float
|
|
* samples. This is what a synthesised-voice vocabulary needs -- SOUND, PLAY,
|
|
* ENVELOPE and VOL all describe a note rather than a recording -- and it is
|
|
* deliberately separate from the SDL3_mixer side of the library, which loads
|
|
* and plays audio *assets*. Nothing here reads a file.
|
|
*
|
|
* The voice table exists whether or not an audio device is open.
|
|
* akgl_audio_init() connects it to one; without that a caller can still set up
|
|
* voices and pull samples itself with akgl_audio_mix(), which is how the test
|
|
* suite exercises the synthesis without depending on a sound card's timing.
|
|
* What a caller cannot do is set up voices and expect to hear them with no
|
|
* device open.
|
|
*/
|
|
|
|
#ifndef _AKGL_AUDIO_H_
|
|
#define _AKGL_AUDIO_H_
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/types.h>
|
|
|
|
/**
|
|
* @brief Voices that can sound at once.
|
|
*
|
|
* Three, because the machine this vocabulary comes from had three and its
|
|
* music is written for three. A voice is addressed here by a zero-based index;
|
|
* a language whose own voices are numbered from one maps them itself.
|
|
*/
|
|
#define AKGL_AUDIO_MAX_VOICES 3
|
|
|
|
/** @brief Sample rate of the generated stream, in frames per second. */
|
|
#define AKGL_AUDIO_SAMPLE_RATE 44100
|
|
|
|
/** @brief Frames the device callback generates per pass through the mixer. */
|
|
#define AKGL_AUDIO_MIX_FRAMES 512
|
|
|
|
/**
|
|
* @brief Shape of one voice's oscillator.
|
|
*
|
|
* The trailing comment on each is the waveform number the C128 SOUND statement
|
|
* uses for the same shape, for a caller translating one to the other.
|
|
*/
|
|
typedef enum {
|
|
AKGL_AUDIO_WAVE_TRIANGLE = 0, /** SOUND waveform 0. Soft, flute-like. */
|
|
AKGL_AUDIO_WAVE_SAWTOOTH = 1, /** SOUND waveform 1. Bright, brassy. */
|
|
AKGL_AUDIO_WAVE_SQUARE = 2, /** SOUND waveform 2. Hollow, reedy. The default. */
|
|
AKGL_AUDIO_WAVE_NOISE = 3, /** SOUND waveform 3. Unpitched; percussion. */
|
|
AKGL_AUDIO_WAVE_SINE = 4 /** No SOUND equivalent. A pure tone. */
|
|
} akgl_AudioWaveform;
|
|
|
|
/** @brief Holds one voice's oscillator, envelope, and how far through it is. */
|
|
typedef struct {
|
|
bool active;
|
|
akgl_AudioWaveform waveform;
|
|
float32_t hz;
|
|
/**
|
|
* @brief Position through one cycle, 0.0 to 1.0.
|
|
*
|
|
* Derived from `elapsed_frames` and `hz` each sample rather than
|
|
* accumulated, so a long note does not drift off pitch. Reading it is
|
|
* meaningful; writing it is not.
|
|
*/
|
|
float32_t phase;
|
|
/** @brief Frames the gate stays open, before the release begins. */
|
|
uint32_t duration_frames;
|
|
/** @brief Frames generated since the tone started, gate and release. */
|
|
uint32_t elapsed_frames;
|
|
uint32_t attack_frames;
|
|
uint32_t decay_frames;
|
|
uint32_t release_frames;
|
|
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
|
|
float32_t sustain;
|
|
} akgl_AudioVoice;
|
|
|
|
/** @brief The process-wide voice table. */
|
|
extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
|
|
|
/**
|
|
* @brief Open an audio device and start pulling samples from the voice table.
|
|
*
|
|
* Requires SDL's audio subsystem to be initialized. Repeating the call is a
|
|
* no-op, so a program that cannot order its initialization precisely may call
|
|
* it more than once. The voice table is reset only on the first call, so this
|
|
* does not silence a voice that is already sounding.
|
|
*
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void);
|
|
|
|
/**
|
|
* @brief Close the audio device and silence every voice.
|
|
*
|
|
* Safe to call when no device is open.
|
|
*
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void);
|
|
|
|
/**
|
|
* @brief Sound a note on one voice for a fixed time.
|
|
*
|
|
* The voice's envelope and waveform are whatever akgl_audio_envelope() and
|
|
* akgl_audio_waveform() last set them to. @p ms is the length of the gate: the
|
|
* voice's release runs *after* it, so a voice with a release stays audible
|
|
* slightly longer than @p ms. Sounding a voice that is already sounding
|
|
* restarts it from the beginning of its envelope.
|
|
*
|
|
* @param voice Zero-based voice index.
|
|
* @param hz Frequency in hertz.
|
|
* @param ms Gate length in milliseconds.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
|
|
|
|
/**
|
|
* @brief Silence one voice immediately, skipping its release.
|
|
* @param voice Zero-based voice index.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice);
|
|
|
|
/**
|
|
* @brief Choose the oscillator shape one voice will use.
|
|
*
|
|
* Takes effect on the next akgl_audio_tone(); it does not reshape a note that
|
|
* is already sounding.
|
|
*
|
|
* @param voice Zero-based voice index.
|
|
* @param waveform Oscillator shape to use.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform);
|
|
|
|
/**
|
|
* @brief Set one voice's ADSR envelope.
|
|
*
|
|
* @p attack, @p decay and @p release are milliseconds; @p sustain is the level
|
|
* the envelope decays to and holds while the gate is open, from 0.0 to 1.0. A
|
|
* zero-length stage is skipped rather than divided by.
|
|
*
|
|
* Takes effect on the next akgl_audio_tone().
|
|
*
|
|
* @param voice Zero-based voice index.
|
|
* @param attack Milliseconds to rise from silence to full level.
|
|
* @param decay Milliseconds to fall from full level to @p sustain.
|
|
* @param sustain Held level while the gate is open, 0.0 to 1.0.
|
|
* @param release Milliseconds to fall to silence once the gate closes.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release);
|
|
|
|
/**
|
|
* @brief Set the level every voice is scaled by.
|
|
* @param level Master level, 0.0 to 1.0.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level);
|
|
|
|
/**
|
|
* @brief Report whether a voice is still sounding.
|
|
*
|
|
* A voice goes quiet on its own when its gate and release have both elapsed, so
|
|
* this is how a caller waits out a note without keeping its own clock.
|
|
*
|
|
* @param voice Zero-based voice index.
|
|
* @param active Output destination set to `true` while the voice is sounding.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active);
|
|
|
|
/**
|
|
* @brief Generate the next @p frames mono samples from the voice table.
|
|
*
|
|
* The device callback installed by akgl_audio_init() is a loop around this. A
|
|
* host that owns its own audio pipeline can call it directly instead and never
|
|
* open a device here at all. Samples are single-precision, one channel, in the
|
|
* range -1.0 to 1.0, and every active voice is advanced by @p frames.
|
|
*
|
|
* @param dest Output destination populated with @p frames samples.
|
|
* @param frames Number of samples to generate.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
|
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
|
|
|
|
#endif // _AKGL_AUDIO_H_
|