/** * @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 #include #include #include /** * @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; /**< Whether this voice is sounding. Cleared by the mixer once gate and release are both spent, so it goes quiet without being told to. */ akgl_AudioWaveform waveform; /**< Oscillator shape. Persists across notes; set once with akgl_audio_waveform(). */ float32_t hz; /**< Frequency of the current note. 0.0 when the voice has never sounded. */ /** * @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; /** @brief Frames to rise from silence to full level. 0 starts at full level. */ uint32_t attack_frames; /** @brief Frames to fall from full level to `sustain`. 0 drops to it at once. */ uint32_t decay_frames; /** @brief Frames to fall to silence after the gate closes. 0 cuts off at once. */ 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. * * The device is resumed immediately rather than left paused, which is SDL's * default -- a caller who set up a voice and heard nothing would have no error * to explain it. * * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKGL_ERR_SDL If no playback device can be opened -- none present, none * permitted, or SDL's audio subsystem never initialized -- or if the * device cannot be resumed. The message carries `SDL_GetError()`. */ 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, and safe to call twice. It also puts the * voice table back to its defaults, so a subsequent akgl_audio_init() starts * from a known state rather than from whatever was left sounding. * * @return `NULL`. There is no failure path -- SDL's stream teardown reports * nothing. */ 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, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @param hz Frequency in hertz. Must be greater than 0. There is no upper * bound check, so a frequency above half the sample rate aliases * rather than being refused. * @param ms Gate length in milliseconds. Must be non-zero -- a zero-length * tone is refused rather than treated as "stop", because * akgl_audio_stop() already means that. Rounded down to a whole * number of frames, so a duration under ~0.023 ms rounds to * nothing. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p hz is not * positive, or if @p ms is 0. Each message says which. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms); /** * @brief Silence one voice immediately, skipping its release. * * A hard cut, not a note-off: the release stage is not run, so the sound stops * on the next sample. Stopping a voice that is already silent is a no-op. * * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table. */ 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, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @param waveform One of the ::akgl_AudioWaveform shapes. The setting persists * across notes until changed. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p waveform is * not one of the five defined shapes. */ 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, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @param attack Milliseconds to rise from silence to full level. 0 starts at * full level. * @param decay Milliseconds to fall from full level to @p sustain. 0 drops to * it at once. * @param sustain Held level while the gate is open, 0.0 to 1.0 inclusive. 0.0 is * legal and means the note is audible only through its attack and * decay -- a plucked sound. * @param release Milliseconds to fall to silence once the gate closes. 0 cuts * off at once. An attack plus decay longer than the note's gate * is not an error: the release simply starts partway up the ramp. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p sustain is * outside 0.0 to 1.0. The three durations are unbounded. */ 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. * * Applied after the voices are summed and before the mix is clamped, so it * takes effect on notes already sounding. akgl_audio_shutdown() puts it back * to 1.0. * * @param level Master level, 0.0 to 1.0 inclusive. 0.0 is silence. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p level is outside 0.0 to 1.0. The message * reports it. */ 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, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @param active Receives `true` while the voice is sounding, including during * its release. Required -- the return value is the error context. * The flag is only cleared by the mixer, so a voice whose time is * up still reads active until samples are next generated; with no * device open and nobody calling akgl_audio_mix(), it never * changes. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table. * @throws AKERR_NULLPOINTER If @p active is `NULL`. */ 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 Receives @p frames samples. Required, and must have room for all * of them -- the count is trusted, not checked against anything. * Overwritten, not accumulated into. * @param frames How many samples to generate. 0 is a no-op, not an error. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p dest is `NULL`. * @throws AKERR_OUTOFBOUNDS If @p frames is negative. * * @warning This mutates the voice table -- it advances each voice's frame * counter and clears `active` on voices that have finished. Calling it * while a device opened by akgl_audio_init() is running means two * threads advancing the same voices, and this path does not take the * stream lock. Use one or the other, not both. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames); #endif // _AKGL_AUDIO_H_