2023-07-30 19:18:50 +00:00
// @fixme: really shutdown audio & related threads before quitting. drwav crashes.
# if is(win32) && !is(gcc)
# include <windows.h>
# include <mmeapi.h> // midi
static HMIDIOUT midi_out_handle = 0 ;
# elif is(osx)
static AudioUnit midi_out_handle = 0 ;
# endif
static void midi_init ( ) {
# if is(win32) && !is(gcc)
if ( midiOutGetNumDevs ( ) ! = 0 ) {
midiOutOpen ( & midi_out_handle , 0 , 0 , 0 , 0 ) ;
}
# elif is(osx)
AUGraph graph ;
AUNode outputNode , mixerNode , dlsNode ;
NewAUGraph ( & graph ) ;
AudioComponentDescription output = { ' auou ' , ' ahal ' , ' appl ' , 0 , 0 } ;
AUGraphAddNode ( graph , & output , & outputNode ) ;
AUGraphOpen ( graph ) ;
AUGraphInitialize ( graph ) ;
AUGraphStart ( graph ) ;
AudioComponentDescription dls = { ' aumu ' , ' dls ' , ' appl ' , 0 , 0 } ;
AUGraphAddNode ( graph , & dls , & dlsNode ) ;
AUGraphNodeInfo ( graph , dlsNode , NULL , & midi_out_handle ) ;
AudioComponentDescription mixer = { ' aumx ' , ' smxr ' , ' appl ' , 0 , 0 } ;
AUGraphAddNode ( graph , & mixer , & mixerNode ) ;
AUGraphConnectNodeInput ( graph , mixerNode , 0 , outputNode , 0 ) ;
AUGraphConnectNodeInput ( graph , dlsNode , 0 , mixerNode , 0 ) ;
AUGraphUpdate ( graph , NULL ) ;
# endif
}
static void midi_quit ( ) {
# if is(win32) && !is(gcc)
if ( midi_out_handle ) midiOutClose ( midi_out_handle ) ;
# endif
// @fixme: osx
// https://developer.apple.com/library/archive/samplecode/PlaySoftMIDI/Listings/main_cpp.html#//apple_ref/doc/uid/DTS40008635-main_cpp-DontLinkElementID_4
}
void midi_send ( unsigned midi_msg ) {
# if is(win32) && !is(gcc)
if ( midi_out_handle ) {
midiOutShortMsg ( midi_out_handle , midi_msg ) ;
}
# elif is(osx)
if ( midi_out_handle ) {
MusicDeviceMIDIEvent ( midi_out_handle , ( midi_msg ) & 0xFF , ( midi_msg > > 8 ) & 0xFF , ( midi_msg > > 16 ) & 0xFF , 0 ) ;
}
# endif
}
// encapsulate drwav,drmp3,stbvorbis and some buffer with the sts_mixer_stream_t
enum { UNK , WAV , OGG , MP1 , MP3 } ;
typedef struct {
int type ;
union {
drwav wav ;
stb_vorbis * ogg ;
void * opaque ;
drmp3 mp3_ ;
} ;
sts_mixer_stream_t stream ; // mixer stream
union {
int32_t data [ 4096 * 2 ] ; // static sample buffer
float dataf [ 4096 * 2 ] ;
} ;
bool rewind ;
} mystream_t ;
static void downsample_to_mono_flt ( int channels , float * buffer , int samples ) {
if ( channels > 1 ) {
float * output = buffer ;
while ( samples - - > 0 ) {
float mix = 0 ;
for ( int i = 0 ; i < channels ; + + i ) mix + = * buffer + + ;
* output + + = ( float ) ( mix / channels ) ;
}
}
}
static void downsample_to_mono_s16 ( int channels , short * buffer , int samples ) {
if ( channels > 1 ) {
short * output = buffer ;
while ( samples - - > 0 ) {
float mix = 0 ;
for ( int i = 0 ; i < channels ; + + i ) mix + = * buffer + + ;
* output + + = ( short ) ( mix / channels ) ;
}
}
}
// the callback to refill the (stereo) stream data
static void refill_stream ( sts_mixer_sample_t * sample , void * userdata ) {
mystream_t * stream = ( mystream_t * ) userdata ;
switch ( stream - > type ) {
default :
break ; case WAV : {
int sl = sample - > length / 2 ; /*sample->channels*/ ;
if ( stream - > rewind ) stream - > rewind = 0 , drwav_seek_to_pcm_frame ( & stream - > wav , 0 ) ;
if ( drwav_read_pcm_frames_s16 ( & stream - > wav , sl , ( short * ) stream - > data ) < sl ) {
drwav_seek_to_pcm_frame ( & stream - > wav , 0 ) ;
}
}
break ; case MP3 : {
int sl = sample - > length / 2 ; /*sample->channels*/ ;
if ( stream - > rewind ) stream - > rewind = 0 , drmp3_seek_to_pcm_frame ( & stream - > mp3_ , 0 ) ;
if ( drmp3_read_pcm_frames_f32 ( & stream - > mp3_ , sl , stream - > dataf ) < sl ) {
drmp3_seek_to_pcm_frame ( & stream - > mp3_ , 0 ) ;
}
}
break ; case OGG : {
stb_vorbis * ogg = ( stb_vorbis * ) stream - > ogg ;
if ( stream - > rewind ) stream - > rewind = 0 , stb_vorbis_seek ( stream - > ogg , 0 ) ;
if ( stb_vorbis_get_samples_short_interleaved ( ogg , 2 , ( short * ) stream - > data , sample - > length ) = = 0 ) {
stb_vorbis_seek ( stream - > ogg , 0 ) ;
}
}
}
}
static void reset_stream ( mystream_t * stream ) {
if ( stream ) memset ( stream - > data , 0 , sizeof ( stream - > data ) ) , stream - > rewind = 1 ;
}
// load a (stereo) stream
static bool load_stream ( mystream_t * stream , const char * filename ) {
int datalen ;
char * data = vfs_load ( filename , & datalen ) ; if ( ! data ) return false ;
int error ;
int HZ = 44100 ;
stream - > type = UNK ;
if ( stream - > type = = UNK & & ( stream - > ogg = stb_vorbis_open_memory ( ( const unsigned char * ) data , datalen , & error , NULL ) ) ) {
stb_vorbis_info info = stb_vorbis_get_info ( stream - > ogg ) ;
if ( info . channels ! = 2 ) { puts ( " cannot stream ogg file. stereo required. " ) ; goto end ; } // @fixme: upsample
stream - > type = OGG ;
stream - > stream . sample . frequency = info . sample_rate ;
stream - > stream . sample . audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
}
if ( stream - > type = = UNK & & drwav_init_memory ( & stream - > wav , data , datalen , NULL ) ) {
if ( stream - > wav . channels ! = 2 ) { puts ( " cannot stream wav file. stereo required. " ) ; goto end ; } // @fixme: upsample
stream - > type = WAV ;
stream - > stream . sample . frequency = stream - > wav . sampleRate ;
stream - > stream . sample . audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
}
drmp3_config mp3_cfg = { 2 , HZ } ;
if ( stream - > type = = UNK & & ( drmp3_init_memory ( & stream - > mp3_ , data , datalen , NULL /*&mp3_cfg*/ ) ! = 0 ) ) {
stream - > type = MP3 ;
stream - > stream . sample . frequency = stream - > mp3_ . sampleRate ;
stream - > stream . sample . audio_format = STS_MIXER_SAMPLE_FORMAT_FLOAT ;
}
if ( stream - > type = = UNK ) {
return false ;
}
end : ;
stream - > stream . userdata = stream ;
stream - > stream . callback = refill_stream ;
stream - > stream . sample . length = sizeof ( stream - > data ) / sizeof ( stream - > data [ 0 ] ) ;
stream - > stream . sample . data = stream - > data ;
refill_stream ( & stream - > stream . sample , stream ) ;
return true ;
}
// load a (mono) sample
static bool load_sample ( sts_mixer_sample_t * sample , const char * filename ) {
int datalen ;
char * data = vfs_load ( filename , & datalen ) ; if ( ! data ) return false ;
int error ;
int channels = 0 ;
if ( ! channels ) for ( drwav w = { 0 } , * wav = & w ; wav & & drwav_init_memory ( wav , data , datalen , NULL ) ; wav = 0 ) {
channels = wav - > channels ;
sample - > frequency = wav - > sampleRate ;
sample - > audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
sample - > length = wav - > totalPCMFrameCount ;
sample - > data = REALLOC ( 0 , sample - > length * sizeof ( short ) * channels ) ;
drwav_read_pcm_frames_s16 ( wav , sample - > length , ( short * ) sample - > data ) ;
drwav_uninit ( wav ) ;
}
if ( ! channels ) for ( stb_vorbis * ogg = stb_vorbis_open_memory ( ( const unsigned char * ) data , datalen , & error , NULL ) ; ogg ; ogg = 0 ) {
stb_vorbis_info info = stb_vorbis_get_info ( ogg ) ;
channels = info . channels ;
sample - > frequency = info . sample_rate ;
sample - > audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
sample - > length = ( int ) stb_vorbis_stream_length_in_samples ( ogg ) ;
stb_vorbis_close ( ogg ) ;
short * buffer ;
int sample_rate ;
stb_vorbis_decode_memory ( ( const unsigned char * ) data , datalen , & channels , & sample_rate , ( short * * ) & buffer ) ;
sample - > data = buffer ;
}
drmp3_config mp3_cfg = { 2 , 44100 } ;
drmp3_uint64 mp3_fc ;
if ( ! channels ) for ( short * fbuf = drmp3_open_memory_and_read_pcm_frames_s16 ( data , datalen , & mp3_cfg , & mp3_fc , NULL ) ; fbuf ; fbuf = 0 ) {
channels = mp3_cfg . channels ;
sample - > frequency = mp3_cfg . sampleRate ;
sample - > audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
sample - > length = mp3_fc ; // / sizeof(float) / mp3_cfg.channels;
sample - > data = fbuf ;
}
if ( ! channels ) {
short * output = 0 ;
int outputSize , hz , mp1channels ;
bool ok = jo_read_mp1 ( data , datalen , & output , & outputSize , & hz , & mp1channels ) ;
if ( ok ) {
channels = mp1channels ;
sample - > frequency = hz ;
sample - > audio_format = STS_MIXER_SAMPLE_FORMAT_16 ;
sample - > length = outputSize / sizeof ( int16_t ) / channels ;
sample - > data = output ; // REALLOC(0, sample->length * sizeof(int16_t) * channels );
// memcpy( sample->data, output, outputSize );
}
}
if ( ! channels ) {
return false ;
}
if ( channels > 1 ) {
if ( sample - > audio_format = = STS_MIXER_SAMPLE_FORMAT_FLOAT ) {
downsample_to_mono_flt ( channels , sample - > data , sample - > length ) ;
sample - > data = REALLOC ( sample - > data , sample - > length * sizeof ( float ) ) ;
}
else
if ( sample - > audio_format = = STS_MIXER_SAMPLE_FORMAT_16 ) {
downsample_to_mono_s16 ( channels , sample - > data , sample - > length ) ;
sample - > data = REALLOC ( sample - > data , sample - > length * sizeof ( short ) ) ;
}
else {
puts ( " error! " ) ; // @fixme
}
}
return true ;
}
// -----------------------------------------------------------------------------
static ma_device device ;
static ma_context context ;
static sts_mixer_t mixer ;
// This is the function that's used for sending more data to the device for playback.
static ma_uint32 audio_callback ( ma_device * pDevice , void * pOutput , const void * pInput , ma_uint32 frameCount ) {
int len = frameCount ;
sts_mixer_mix_audio ( & mixer , pOutput , len / ( sizeof ( int32_t ) / 4 ) ) ;
( void ) pDevice ; ( void ) pInput ;
return len / ( sizeof ( int32_t ) / 4 ) ;
}
void audio_drop ( void ) {
ma_device_stop ( & device ) ;
ma_device_uninit ( & device ) ;
ma_context_uninit ( & context ) ;
}
int audio_init ( int flags ) {
atexit ( audio_drop ) ;
// init sts_mixer
sts_mixer_init ( & mixer , 44100 , STS_MIXER_SAMPLE_FORMAT_32 ) ;
// The prioritization of backends can be controlled by the application. You need only specify the backends
// you care about. If the context cannot be initialized for any of the specified backends ma_context_init()
// will fail.
ma_backend backends [ ] = {
# if 1
ma_backend_wasapi , // Higest priority.
ma_backend_dsound ,
ma_backend_winmm ,
ma_backend_pulseaudio ,
ma_backend_alsa ,
ma_backend_oss ,
ma_backend_jack ,
ma_backend_opensl ,
//ma_backend_webaudio,
//ma_backend_openal,
//ma_backend_sdl,
ma_backend_null // Lowest priority.
# else
// Highest priority
ma_backend_wasapi , // WASAPI | Windows Vista+
ma_backend_dsound , // DirectSound | Windows XP+
ma_backend_winmm , // WinMM | Windows XP+ (may work on older versions, but untested)
ma_backend_coreaudio , // Core Audio | macOS, iOS
ma_backend_pulseaudio , // PulseAudio | Cross Platform (disabled on Windows, BSD and Android)
ma_backend_alsa , // ALSA | Linux
ma_backend_oss , // OSS | FreeBSD
ma_backend_jack , // JACK | Cross Platform (disabled on BSD and Android)
ma_backend_opensl , // OpenSL ES | Android (API level 16+)
ma_backend_webaudio , // Web Audio | Web (via Emscripten)
ma_backend_sndio , // sndio | OpenBSD
ma_backend_audio4 , // audio(4) | NetBSD, OpenBSD
ma_backend_aaudio , // AAudio | Android 8+
ma_backend_custom , // Custom | Cross Platform
ma_backend_null , // Null | Cross Platform (not used on Web)
// Lowest priority
# endif
} ;
if ( ma_context_init ( backends , countof ( backends ) , NULL , & context ) ! = MA_SUCCESS ) {
PRINTF ( " %s \n " , " Failed to initialize audio context. " ) ;
return false ;
}
ma_device_config config = ma_device_config_init ( ma_device_type_playback ) ; // Or ma_device_type_capture or ma_device_type_duplex.
config . playback . pDeviceID = NULL ; // &myPlaybackDeviceID; // Or NULL for the default playback device.
config . playback . format = ma_format_s32 ;
config . playback . channels = 2 ;
config . sampleRate = 44100 ;
config . dataCallback = ( void * ) audio_callback ; //< @r-lyeh add void* cast
config . pUserData = NULL ;
if ( ma_device_init ( NULL , & config , & device ) ! = MA_SUCCESS ) {
printf ( " Failed to open playback device. " ) ;
ma_context_uninit ( & context ) ;
return false ;
}
( void ) flags ;
ma_device_start ( & device ) ;
return true ;
}
typedef struct audio_handle {
bool is_clip ;
bool is_stream ;
union {
sts_mixer_sample_t clip ;
mystream_t stream ;
} ;
} audio_handle ;
static array ( audio_handle * ) audio_instances ;
audio_t audio_clip ( const char * pathfile ) {
audio_handle * a = REALLOC ( 0 , sizeof ( audio_handle ) ) ;
memset ( a , 0 , sizeof ( audio_handle ) ) ;
a - > is_clip = load_sample ( & a - > clip , pathfile ) ;
array_push ( audio_instances , a ) ;
return a ;
}
audio_t audio_stream ( const char * pathfile ) {
audio_handle * a = REALLOC ( 0 , sizeof ( audio_handle ) ) ;
memset ( a , 0 , sizeof ( audio_handle ) ) ;
a - > is_stream = load_stream ( & a - > stream , pathfile ) ;
array_push ( audio_instances , a ) ;
return a ;
}
static float volume_clip = 1 , volume_stream = 1 , volume_master = 1 ;
float audio_volume_clip ( float gain ) {
if ( gain > = 0 & & gain < = 1 ) volume_clip = gain * gain ;
// patch all live clips
for ( int i = 0 , active = 0 ; i < STS_MIXER_VOICES ; + + i ) {
if ( mixer . voices [ i ] . state ! = STS_MIXER_VOICE_STOPPED ) // is_active?
if ( mixer . voices [ i ] . sample ) // is_sample?
mixer . voices [ i ] . gain = volume_clip ;
}
return sqrt ( volume_clip ) ;
}
float audio_volume_stream ( float gain ) {
if ( gain > = 0 & & gain < = 1 ) volume_stream = gain * gain ;
// patch all live streams
for ( int i = 0 , active = 0 ; i < STS_MIXER_VOICES ; + + i ) {
if ( mixer . voices [ i ] . state ! = STS_MIXER_VOICE_STOPPED ) // is_active?
if ( mixer . voices [ i ] . stream ) // is_stream?
mixer . voices [ i ] . gain = volume_stream ;
}
return sqrt ( volume_stream ) ;
}
float audio_volume_master ( float gain ) {
if ( gain > = 0 & & gain < = 1 ) volume_master = gain * gain ;
// patch global mixer
mixer . gain = volume_master ;
return sqrt ( volume_master ) ;
}
int audio_play_gain_pitch_pan ( audio_t a , int flags , float gain , float pitch , float pan ) {
if ( flags & AUDIO_IGNORE_MIXER_GAIN ) {
// do nothing, gain used as-is
} else {
// apply mixer gains on top
gain + = a - > is_clip ? volume_clip : volume_stream ;
}
if ( flags & AUDIO_SINGLE_INSTANCE ) {
audio_stop ( a ) ;
}
// gain: [0..+1], pitch: (0..N], pan: [-1..+1]
if ( a - > is_clip ) {
int voice = sts_mixer_play_sample ( & mixer , & a - > clip , gain , pitch , pan ) ;
if ( voice = = - 1 ) return 0 ; // all voices busy
}
if ( a - > is_stream ) {
int voice = sts_mixer_play_stream ( & mixer , & a - > stream . stream , gain ) ;
if ( voice = = - 1 ) return 0 ; // all voices busy
}
return 1 ;
}
int audio_play_gain_pitch ( audio_t a , int flags , float gain , float pitch ) {
return audio_play_gain_pitch_pan ( a , flags , gain , pitch , 0 ) ;
}
int audio_play_gain ( audio_t a , int flags , float gain ) {
return audio_play_gain_pitch ( a , flags , gain , 1.f ) ;
}
int audio_play ( audio_t a , int flags ) {
return audio_play_gain ( a , flags & ~ AUDIO_IGNORE_MIXER_GAIN , 0.f ) ;
}
int audio_stop ( audio_t a ) {
if ( a - > is_clip ) {
sts_mixer_stop_sample ( & mixer , & a - > clip ) ;
}
if ( a - > is_stream ) {
sts_mixer_stop_stream ( & mixer , & a - > stream . stream ) ;
reset_stream ( & a - > stream ) ;
}
return 1 ;
}
// -----------------------------------------------------------------------------
// audio queue
# ifndef AUDIO_QUEUE_BUFFERING_MS
# define AUDIO_QUEUE_BUFFERING_MS 50 // 10 // 100
# endif
# ifndef AUDIO_QUEUE_MAX
# define AUDIO_QUEUE_MAX 2048
# endif
# ifndef AUDIO_QUEUE_TIMEOUT
# define AUDIO_QUEUE_TIMEOUT ifdef(win32, THREAD_QUEUE_WAIT_INFINITE, 500)
# endif
typedef struct audio_queue_t {
int cursor ;
int avail ;
unsigned flags ;
char data [ 0 ] ;
} audio_queue_t ;
static thread_queue_t queue_mutex ;
static void audio_queue_init ( ) {
static void * audio_queues [ AUDIO_QUEUE_MAX ] = { 0 } ;
do_once thread_queue_init ( & queue_mutex , countof ( audio_queues ) , audio_queues , 0 ) ;
}
static void audio_queue_callback ( sts_mixer_sample_t * sample , void * userdata ) {
( void ) userdata ;
int sl = sample - > length / 2 ; // 2 ch
int bytes = sl * 2 * ( sample - > audio_format = = STS_MIXER_SAMPLE_FORMAT_16 ? 2 : 4 ) ;
char * dst = sample - > data ;
static audio_queue_t * aq = 0 ;
do {
while ( ! aq ) aq = ( audio_queue_t * ) thread_queue_consume ( & queue_mutex , THREAD_QUEUE_WAIT_INFINITE ) ;
int len = aq - > avail > bytes ? bytes : aq - > avail ;
memcpy ( dst , ( char * ) aq - > data + aq - > cursor , len ) ;
dst + = len ;
bytes - = len ;
aq - > cursor + = len ;
aq - > avail - = len ;
if ( aq - > avail < = 0 ) {
FREE ( aq ) ; // @fixme: mattias' original thread_queue_consume() implementation crashes here on tcc+win because of a double free on same pointer. using mcmp for now
aq = 0 ;
}
} while ( bytes > 0 ) ;
}
static int audio_queue_voice = - 1 ;
void audio_queue_clear ( ) {
do_once audio_queue_init ( ) ;
sts_mixer_stop_voice ( & mixer , audio_queue_voice ) ;
audio_queue_voice = - 1 ;
}
int audio_queue ( const void * samples , int num_samples , int flags ) {
do_once audio_queue_init ( ) ;
float gain = 1 ; // [0..1]
float pitch = 1 ; // (0..N]
float pan = 0 ; // [-1..1]
int bits = flags & AUDIO_8 ? 8 : flags & ( AUDIO_32 | AUDIO_FLOAT ) ? 32 : 16 ;
int channels = flags & AUDIO_2CH ? 2 : 1 ;
int bytes_per_sample = channels * ( bits / 8 ) ;
int bytes = num_samples * bytes_per_sample ;
static sts_mixer_stream_t q = { 0 } ;
if ( audio_queue_voice < 0 ) {
void * reuse_ptr = q . sample . data ;
q = ( ( sts_mixer_stream_t ) { 0 } ) ;
q . sample . data = reuse_ptr ;
q . callback = audio_queue_callback ;
2023-08-10 14:30:56 +00:00
q . sample . frequency = flags & AUDIO_8KHZ ? 8000 : flags & AUDIO_11KHZ ? 11025 : flags & AUDIO_44KHZ ? 44100 : flags & AUDIO_32KHZ ? 32000 : 22050 ;
2023-07-30 19:18:50 +00:00
q . sample . audio_format = flags & AUDIO_FLOAT ? STS_MIXER_SAMPLE_FORMAT_FLOAT : STS_MIXER_SAMPLE_FORMAT_16 ;
q . sample . length = q . sample . frequency / ( 1000 / AUDIO_QUEUE_BUFFERING_MS ) ; // num_samples;
int bytes = q . sample . length * 2 * ( flags & AUDIO_FLOAT ? 4 : 2 ) ;
q . sample . data = memset ( REALLOC ( q . sample . data , bytes ) , 0 , bytes ) ;
audio_queue_voice = sts_mixer_play_stream ( & mixer , & q , gain * 1.f ) ;
if ( audio_queue_voice < 0 ) return 0 ;
}
2023-08-11 19:53:24 +00:00
audio_queue_t * aq = MALLOC ( sizeof ( audio_queue_t ) + ( bytes < < ( channels = = 1 ) ) ) ; // dupe space if going to be converted from mono to stereo
2023-07-30 19:18:50 +00:00
aq - > cursor = 0 ;
aq - > avail = bytes ;
aq - > flags = flags ;
if ( ! samples ) {
memset ( aq - > data , 0 , bytes ) ;
} else {
// @todo: convert from other source formats to target format in here: add AUDIO_8, AUDIO_32
if ( channels = = 1 ) {
// mixer accepts stereo samples only; so resample mono to stereo if needed
for ( int i = 0 ; i < num_samples ; + + i ) {
memcpy ( ( char * ) aq - > data + ( i * 2 + 0 ) * bytes_per_sample , ( char * ) samples + i * bytes_per_sample , bytes_per_sample ) ;
memcpy ( ( char * ) aq - > data + ( i * 2 + 1 ) * bytes_per_sample , ( char * ) samples + i * bytes_per_sample , bytes_per_sample ) ;
}
} else {
memcpy ( aq - > data , samples , bytes ) ;
}
}
while ( ! thread_queue_produce ( & queue_mutex , aq , THREAD_QUEUE_WAIT_INFINITE ) ) { }
return audio_queue_voice ;
}