twn_audio.c: .wav support and scratch channels
This commit is contained in:
		
							
								
								
									
										171
									
								
								src/twn_audio.c
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								src/twn_audio.c
									
									
									
									
									
								
							| @@ -7,6 +7,7 @@ | |||||||
| #include <SDL2/SDL.h> | #include <SDL2/SDL.h> | ||||||
| #include <stb_ds.h> | #include <stb_ds.h> | ||||||
| #include <physfs.h> | #include <physfs.h> | ||||||
|  | #include <physfsrwops.h> | ||||||
|  |  | ||||||
| #define STB_VORBIS_NO_PUSHDATA_API | #define STB_VORBIS_NO_PUSHDATA_API | ||||||
| #define STB_VORBIS_HEADER_ONLY | #define STB_VORBIS_HEADER_ONLY | ||||||
| @@ -17,6 +18,7 @@ | |||||||
|  |  | ||||||
| static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = { | static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = { | ||||||
|     ".ogg", /* AUDIO_FILE_TYPE_OGG */ |     ".ogg", /* AUDIO_FILE_TYPE_OGG */ | ||||||
|  |     ".wav", /* AUDIO_FILE_TYPE_WAV */ | ||||||
|     ".xm",  /* AUDIO_FILE_TYPE_XM */ |     ".xm",  /* AUDIO_FILE_TYPE_XM */ | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -73,6 +75,7 @@ static AudioFileType infer_audio_file_type(const char *path) { | |||||||
|  |  | ||||||
| /* TODO: error propagation and clearing of resources on partial success? */ | /* TODO: error propagation and clearing of resources on partial success? */ | ||||||
| /*       or should we expect things to simply fail? */ | /*       or should we expect things to simply fail? */ | ||||||
|  | /* TODO: reuse often used decoded/decompressed data */ | ||||||
| static union AudioContext init_audio_context(const char *path, AudioFileType type) { | static union AudioContext init_audio_context(const char *path, AudioFileType type) { | ||||||
|     switch (type) { |     switch (type) { | ||||||
|     case AUDIO_FILE_TYPE_OGG: { |     case AUDIO_FILE_TYPE_OGG: { | ||||||
| @@ -102,6 +105,25 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /* TODO: transform to destination format immediately? */ | ||||||
|  |     case AUDIO_FILE_TYPE_WAV: { | ||||||
|  |         SDL_AudioSpec spec; | ||||||
|  |         uint8_t *data; | ||||||
|  |         uint32_t len; | ||||||
|  |         if (!SDL_LoadWAV_RW(PHYSFSRWOPS_openRead(path), 1, &spec, &data, &len)) { | ||||||
|  |             CRY_SDL("Cannot load .wav file:"); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return (union AudioContext) { | ||||||
|  |             .wav = { | ||||||
|  |                 .position = 0, | ||||||
|  |                 .samples = data, | ||||||
|  |                 .spec = spec | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     case AUDIO_FILE_TYPE_XM: { |     case AUDIO_FILE_TYPE_XM: { | ||||||
|         unsigned char *data; |         unsigned char *data; | ||||||
|         int64_t len = get_audio_data(path, &data); |         int64_t len = get_audio_data(path, &data); | ||||||
| @@ -138,6 +160,18 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | static void free_audio_channel(AudioChannel channel) { | ||||||
|  |     switch (channel.file_type) { | ||||||
|  |     case AUDIO_FILE_TYPE_WAV: { | ||||||
|  |         SDL_free(channel.context.wav.samples); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| static void repeat_audio(AudioChannel *channel) { | static void repeat_audio(AudioChannel *channel) { | ||||||
|     switch (channel->file_type) { |     switch (channel->file_type) { | ||||||
|     case AUDIO_FILE_TYPE_OGG: { |     case AUDIO_FILE_TYPE_OGG: { | ||||||
| @@ -145,6 +179,11 @@ static void repeat_audio(AudioChannel *channel) { | |||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     case AUDIO_FILE_TYPE_WAV: { | ||||||
|  |         channel->context.wav.position = 0; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     case AUDIO_FILE_TYPE_XM: { |     case AUDIO_FILE_TYPE_XM: { | ||||||
|         xm_restart(channel->context.xm.handle); |         xm_restart(channel->context.xm.handle); | ||||||
|         break; |         break; | ||||||
| @@ -165,29 +204,48 @@ void audio_play(const char *path, | |||||||
|                 float volume, |                 float volume, | ||||||
|                 float panning) |                 float panning) | ||||||
| { | { | ||||||
|     AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel); |     if (channel) { | ||||||
|  |         AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel); | ||||||
|  |  | ||||||
|     /* create a channel if it doesn't exist */ |         /* create a channel if it doesn't exist */ | ||||||
|     if (!pair) { |         if (!pair) { | ||||||
|         AudioFileType file_type = infer_audio_file_type(path); |             AudioFileType const file_type = infer_audio_file_type(path); | ||||||
|  |             AudioChannel new_channel = { | ||||||
|  |                 .file_type = file_type, | ||||||
|  |                 .context = init_audio_context(path, file_type), | ||||||
|  |                 .path = path, | ||||||
|  |                 .name = channel, | ||||||
|  |                 .repeat = repeat, | ||||||
|  |                 .volume = volume, | ||||||
|  |                 .panning = panning, | ||||||
|  |             }; | ||||||
|  |             shput(ctx.audio_channels, channel, new_channel); | ||||||
|  |             pair = shgetp_null(ctx.audio_channels, channel); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* TODO: destroy and create new context when channel is reused for different file */ | ||||||
|  |  | ||||||
|  |         /* works for both restarts and new audio */ | ||||||
|  |         if (strcmp(pair->value.path, path) == 0) | ||||||
|  |             repeat_audio(&pair->value); | ||||||
|  |     } else { | ||||||
|  |         /* audio without channel plays without repeat and ability to change parameters over its course, nor stop it */ | ||||||
|  |         AudioFileType const file_type = infer_audio_file_type(path); | ||||||
|         AudioChannel new_channel = { |         AudioChannel new_channel = { | ||||||
|             .file_type = file_type, |             .file_type = file_type, | ||||||
|             .context = init_audio_context(path, file_type), |             .context = init_audio_context(path, file_type), | ||||||
|             .path = path, |             .path = path, | ||||||
|             .name = channel, |             .name = channel, | ||||||
|             .repeat = repeat, |             .repeat = false, | ||||||
|             .volume = volume, |             .volume = volume, | ||||||
|             .panning = panning, |             .panning = panning, | ||||||
|         }; |         }; | ||||||
|         shput(ctx.audio_channels, channel, new_channel); |  | ||||||
|         pair = shgetp_null(ctx.audio_channels, channel); |         if (repeat) | ||||||
|  |             log_warn("Cannot repeat audio played on unnamed scratch channel (for %s)", path); | ||||||
|  |  | ||||||
|  |         arrpush(ctx.unnamed_audio_channels, new_channel); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* TODO: destroy and create new context when channel is reused for different file */ |  | ||||||
|  |  | ||||||
|     /* works for both restarts and new audio */ |  | ||||||
|     if (strcmp(pair->value.path, path) == 0) |  | ||||||
|         repeat_audio(&pair->value); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -243,12 +301,12 @@ static void audio_mixin_streams(const AudioChannel *channel, | |||||||
|  |  | ||||||
|  |  | ||||||
| /* remember: sample is data for all channels where frame is a part of it */ | /* remember: sample is data for all channels where frame is a part of it */ | ||||||
| static void audio_sample_and_mixin_channel(const AudioChannel *channel, | static void audio_sample_and_mixin_channel(AudioChannel *channel, | ||||||
|                                            uint8_t *stream, |                                            uint8_t *stream, | ||||||
|                                            int len) |                                            int len) | ||||||
| { | { | ||||||
|     static uint8_t buffer[16384]; |     static uint8_t buffer[16384]; | ||||||
|     const int float_buffer_frames = sizeof (buffer) / sizeof (float); |     const int float_buffer_frames = sizeof (buffer) / sizeof (float) / 2; | ||||||
|     const int stream_frames = len / (int)(sizeof (float)); |     const int stream_frames = len / (int)(sizeof (float)); | ||||||
|  |  | ||||||
|     switch (channel->file_type) { |     switch (channel->file_type) { | ||||||
| @@ -270,9 +328,11 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, | |||||||
|                     /* seek to start and try sampling some more */ |                     /* seek to start and try sampling some more */ | ||||||
|                     stb_vorbis_seek_start(channel->context.vorbis.handle); |                     stb_vorbis_seek_start(channel->context.vorbis.handle); | ||||||
|                     continue; |                     continue; | ||||||
|                 } else |                 } else { | ||||||
|                     /* leave silence */ |                     /* leave silence */ | ||||||
|  |                     channel->finished = true; | ||||||
|                     break; |                     break; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             /* panning and mixing */ |             /* panning and mixing */ | ||||||
| @@ -286,6 +346,66 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, | |||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     case AUDIO_FILE_TYPE_WAV: { | ||||||
|  |         /* feed stream for needed conversions */ | ||||||
|  |         for (int i = 0; i < stream_frames; ) { | ||||||
|  |             const int n_frames = (stream_frames - i) > float_buffer_frames ? | ||||||
|  |                 float_buffer_frames : stream_frames - i; | ||||||
|  |  | ||||||
|  |             int const limit = MIN(n_frames, channel->context.wav.spec.samples); | ||||||
|  |  | ||||||
|  |             switch (channel->context.wav.spec.format) { | ||||||
|  |             case AUDIO_U16: { | ||||||
|  |                 if (channel->context.wav.spec.channels == 1) { | ||||||
|  |                     for (int x = 0; x < limit; ++x) { | ||||||
|  |                         ((float *)buffer)[x * 2 + 0] = (float)((uint16_t *)channel->context.wav.samples)[x] / (float)UINT16_MAX; | ||||||
|  |                         ((float *)buffer)[x * 2 + 1] = (float)((uint16_t *)channel->context.wav.samples)[x] / (float)UINT16_MAX; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case AUDIO_S16: { | ||||||
|  |                 if (channel->context.wav.spec.channels == 1) { | ||||||
|  |                     for (int x = 0; x < limit; ++x) { | ||||||
|  |                         if ((float)((int16_t *)channel->context.wav.samples)[x] < 0) { | ||||||
|  |                             ((float *)buffer)[x * 2 + 0] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MIN; | ||||||
|  |                             ((float *)buffer)[x * 2 + 1] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MIN; | ||||||
|  |                         } else { | ||||||
|  |                             ((float *)buffer)[x * 2 + 0] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MAX; | ||||||
|  |                             ((float *)buffer)[x * 2 + 1] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MAX; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             default: | ||||||
|  |                 log_warn("Unsupported .wav PCM format (%x), producing silence", channel->context.wav.spec.format); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             /* panning and mixing */ | ||||||
|  |             audio_mixin_streams(channel, &stream[i * sizeof(float)], buffer, limit * 2); | ||||||
|  |  | ||||||
|  |             channel->context.wav.position += limit; | ||||||
|  |  | ||||||
|  |             if (channel->context.wav.position == channel->context.wav.spec.samples) { | ||||||
|  |                 if (channel->repeat) | ||||||
|  |                     channel->context.wav.position = 0; | ||||||
|  |                 else { | ||||||
|  |                     /* leave silence */ | ||||||
|  |                     channel->finished = true; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             i += limit * 2; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     case AUDIO_FILE_TYPE_XM: { |     case AUDIO_FILE_TYPE_XM: { | ||||||
|         for (int i = 0; i < stream_frames; ) { |         for (int i = 0; i < stream_frames; ) { | ||||||
|             const int n_frames = (stream_frames - i) > float_buffer_frames ? |             const int n_frames = (stream_frames - i) > float_buffer_frames ? | ||||||
| @@ -301,9 +421,11 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, | |||||||
|                     /* seek to start and try sampling some more */ |                     /* seek to start and try sampling some more */ | ||||||
|                     xm_restart(channel->context.xm.handle); |                     xm_restart(channel->context.xm.handle); | ||||||
|                     continue; |                     continue; | ||||||
|                 } else |                 } else { | ||||||
|  |                     channel->finished = true; | ||||||
|                     /* leave silence */ |                     /* leave silence */ | ||||||
|                     break; |                     break; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             /* panning and mixing */ |             /* panning and mixing */ | ||||||
| @@ -346,8 +468,23 @@ void audio_callback(void *userdata, uint8_t *stream, int len) { | |||||||
|         sanity_check_channel(&ctx.audio_channels[i].value); |         sanity_check_channel(&ctx.audio_channels[i].value); | ||||||
|         audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len); |         audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     for (int i = 0; i < arrlen(ctx.unnamed_audio_channels); ++i) { | ||||||
|  |         sanity_check_channel(&ctx.unnamed_audio_channels[i]); | ||||||
|  |         audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i], stream, len); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /* ditch finished unnamed */ | ||||||
|  |     int i = 0; | ||||||
|  |     while (i < arrlen(ctx.unnamed_audio_channels)) { | ||||||
|  |         if (ctx.unnamed_audio_channels[i].finished) { | ||||||
|  |             free_audio_channel(ctx.unnamed_audio_channels[i]); | ||||||
|  |             arrdelswap(ctx.unnamed_audio_channels, i); | ||||||
|  |         } else i++; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| TWN_API void audio_play_args(PlayAudioArgs args) { | TWN_API void audio_play_args(PlayAudioArgs args) { | ||||||
|     const char *channel = m_or(args, channel, NULL); |     const char *channel = m_or(args, channel, NULL); | ||||||
|     const bool repeat = m_or(args, repeat, false); |     const bool repeat = m_or(args, repeat, false); | ||||||
|   | |||||||
| @@ -13,9 +13,12 @@ | |||||||
|  |  | ||||||
| #define AUDIO_FREQUENCY 48000 | #define AUDIO_FREQUENCY 48000 | ||||||
|  |  | ||||||
|  | /* TODO: specify which PCM formats are usable with WAV */ | ||||||
|  | /* TODO: specify limitations of libxm */ | ||||||
|  | /* TODO: specify limitations of stb_vorbis */ | ||||||
| typedef enum AudioFileType { | typedef enum AudioFileType { | ||||||
|     AUDIO_FILE_TYPE_OGG, |     AUDIO_FILE_TYPE_OGG, | ||||||
|  |     AUDIO_FILE_TYPE_WAV, | ||||||
|     AUDIO_FILE_TYPE_XM, |     AUDIO_FILE_TYPE_XM, | ||||||
|     AUDIO_FILE_TYPE_COUNT, |     AUDIO_FILE_TYPE_COUNT, | ||||||
|     AUDIO_FILE_TYPE_UNKNOWN, |     AUDIO_FILE_TYPE_UNKNOWN, | ||||||
| @@ -30,6 +33,12 @@ union AudioContext { | |||||||
|         uint8_t channel_count; |         uint8_t channel_count; | ||||||
|     } vorbis; |     } vorbis; | ||||||
|  |  | ||||||
|  |     struct { | ||||||
|  |         void *samples; | ||||||
|  |         SDL_AudioSpec spec; | ||||||
|  |         size_t position; | ||||||
|  |     } wav; | ||||||
|  |  | ||||||
|     struct { |     struct { | ||||||
|         xm_context_t *handle; |         xm_context_t *handle; | ||||||
|     } xm; |     } xm; | ||||||
| @@ -44,6 +53,7 @@ typedef struct AudioChannel { | |||||||
|     bool repeat; |     bool repeat; | ||||||
|     float volume; |     float volume; | ||||||
|     float panning; |     float panning; | ||||||
|  |     bool finished; | ||||||
| } AudioChannel; | } AudioChannel; | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -53,6 +53,7 @@ typedef struct EngineContext { | |||||||
|  |  | ||||||
|     /* audio */ |     /* audio */ | ||||||
|     AudioChannelItem *audio_channels; |     AudioChannelItem *audio_channels; | ||||||
|  |     AudioChannel *unnamed_audio_channels; | ||||||
|     SDL_AudioDeviceID audio_device; |     SDL_AudioDeviceID audio_device; | ||||||
|     int audio_stream_frequency; |     int audio_stream_frequency; | ||||||
|     SDL_AudioFormat audio_stream_format; |     SDL_AudioFormat audio_stream_format; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user