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_
|