From 7f6002caba3f0a6749820c2772161caf55b8d267 Mon Sep 17 00:00:00 2001 From: neonloop Date: Fri, 7 May 2021 20:00:12 +0000 Subject: Initial commit (uqm-0.8.0) --- src/libs/sound/stream.c | 814 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 814 insertions(+) create mode 100644 src/libs/sound/stream.c (limited to 'src/libs/sound/stream.c') diff --git a/src/libs/sound/stream.c b/src/libs/sound/stream.c new file mode 100644 index 0000000..b7d2718 --- /dev/null +++ b/src/libs/sound/stream.c @@ -0,0 +1,814 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include +#include +#include + // for abs() +#include "sound.h" +#include "sndintrn.h" +#include "libs/tasklib.h" +#include "libs/timelib.h" +#include "libs/threadlib.h" +#include "libs/log.h" +#include "libs/memlib.h" + + +static Task decoderTask; + +static TimeCount musicFadeStartTime; +static sint32 musicFadeInterval; +static int musicFadeStartVolume; +static int musicFadeDelta; +// Mutex protects fade structures +static Mutex fade_mutex; + +static void add_scope_data (TFB_SoundSource *source, uint32 bytes); + + +void +PlayStream (TFB_SoundSample *sample, uint32 source, bool looping, bool scope, + bool rewind) +{ + uint32 i; + sint32 offset; + TFB_SoundDecoder *decoder; + + if (!sample) + return; + + StopStream (source); + if (sample->callbacks.OnStartStream && + !sample->callbacks.OnStartStream (sample)) + return; // callback failed + + if (sample->buffer_tag) + memset (sample->buffer_tag, 0, + sample->num_buffers * sizeof (sample->buffer_tag[0])); + + decoder = sample->decoder; + offset = sample->offset; + if (rewind) + SoundDecoder_Rewind (decoder); + else + offset += (sint32)(SoundDecoder_GetTime (decoder) * ONE_SECOND); + + soundSource[source].sample = sample; + decoder->looping = looping; + audio_Sourcei (soundSource[source].handle, audio_LOOPING, false); + + if (scope) + { // Prealloc the scope buffer in advance so that we do not + // realloc it a zillion times + soundSource[source].sbuf_size = sample->num_buffers * + decoder->buffer_size + PAD_SCOPE_BYTES; + soundSource[source].sbuffer = HCalloc (soundSource[source].sbuf_size); + } + + for (i = 0; i < sample->num_buffers; ++i) + { + uint32 decoded_bytes; + + decoded_bytes = SoundDecoder_Decode (decoder); +#if 0 + log_add (log_Debug, "PlayStream(): source:%d filename:%s start:%d " + "position:%d bytes:%d\n", + source, decoder->filename, decoder->start_sample, + decoder->pos, decoded_bytes); +#endif + if (decoded_bytes == 0) + break; + + audio_BufferData (sample->buffer[i], decoder->format, + decoder->buffer, decoded_bytes, decoder->frequency); + audio_SourceQueueBuffers (soundSource[source].handle, 1, + &sample->buffer[i]); + if (sample->callbacks.OnQueueBuffer) + sample->callbacks.OnQueueBuffer (sample, sample->buffer[i]); + + if (scope) + add_scope_data (&soundSource[source], decoded_bytes); + + if (decoder->error != SOUNDDECODER_OK) + { + if (decoder->error != SOUNDDECODER_EOF || + !sample->callbacks.OnEndChunk || + !sample->callbacks.OnEndChunk (sample, sample->buffer[i])) + { // Decoder probably run out of data before we could fill + // all buffers, and OnEndChunk() did not set a new one + break; + } + else + { // OnEndChunk() probably set a new decoder, get it + decoder = sample->decoder; + } + } + } + + soundSource[source].sbuf_lasttime = GetTimeCounter (); + // Adjust the start time so it looks like the stream has been playing + // from the very beginning + soundSource[source].start_time = GetTimeCounter () - offset; + soundSource[source].pause_time = 0; + soundSource[source].stream_should_be_playing = TRUE; + audio_SourcePlay (soundSource[source].handle); +} + +void +StopStream (uint32 source) +{ + StopSource (source); + + soundSource[source].stream_should_be_playing = FALSE; + soundSource[source].sample = NULL; + + if (soundSource[source].sbuffer) + { + void *sbuffer = soundSource[source].sbuffer; + soundSource[source].sbuffer = NULL; + HFree (sbuffer); + } + soundSource[source].sbuf_size = 0; + soundSource[source].sbuf_head = 0; + soundSource[source].sbuf_tail = 0; + soundSource[source].pause_time = 0; +} + +void +PauseStream (uint32 source) +{ + soundSource[source].stream_should_be_playing = FALSE; + if (!soundSource[source].pause_time) + soundSource[source].pause_time = GetTimeCounter (); + audio_SourcePause (soundSource[source].handle); +} + +void +ResumeStream (uint32 source) +{ + if (soundSource[source].pause_time) + { // Adjust the start time so it looks like the stream has + // been playing all this time non-stop + soundSource[source].start_time += GetTimeCounter () + - soundSource[source].pause_time; + } + soundSource[source].pause_time = 0; + soundSource[source].stream_should_be_playing = TRUE; + audio_SourcePlay (soundSource[source].handle); +} + +void +SeekStream (uint32 source, uint32 pos) +{ + TFB_SoundSample* sample = soundSource[source].sample; + bool looping; + bool scope; + + if (!sample) + return; + looping = sample->decoder->looping; + scope = soundSource[source].sbuffer != NULL; + + StopSource (source); + SoundDecoder_Seek (sample->decoder, pos); + PlayStream (sample, source, looping, scope, false); +} + +BOOLEAN +PlayingStream (uint32 source) +{ + return soundSource[source].stream_should_be_playing; +} + + +TFB_SoundSample * +TFB_CreateSoundSample (TFB_SoundDecoder *decoder, uint32 num_buffers, + const TFB_SoundCallbacks *pcbs /* can be NULL */) +{ + TFB_SoundSample *sample; + + sample = HCalloc (sizeof (*sample)); + sample->decoder = decoder; + sample->num_buffers = num_buffers; + sample->buffer = HCalloc (sizeof (audio_Object) * num_buffers); + audio_GenBuffers (num_buffers, sample->buffer); + if (pcbs) + sample->callbacks = *pcbs; + + return sample; +} + +// Deletes all TFB_SoundSample data structures, except decoder +void +TFB_DestroySoundSample (TFB_SoundSample *sample) +{ + if (sample->buffer) + { + audio_DeleteBuffers (sample->num_buffers, sample->buffer); + HFree (sample->buffer); + } + HFree (sample->buffer_tag); + HFree (sample); +} + +void +TFB_SetSoundSampleData (TFB_SoundSample *sample, void* data) +{ + sample->data = data; +} + +void* +TFB_GetSoundSampleData (TFB_SoundSample *sample) +{ + return sample->data; +} + +void +TFB_SetSoundSampleCallbacks (TFB_SoundSample *sample, + const TFB_SoundCallbacks *pcbs /* can be NULL */) +{ + if (pcbs) + sample->callbacks = *pcbs; + else + memset (&sample->callbacks, 0, sizeof (sample->callbacks)); +} + +TFB_SoundDecoder* +TFB_GetSoundSampleDecoder (TFB_SoundSample *sample) +{ + return sample->decoder; +} + +TFB_SoundTag* +TFB_FindTaggedBuffer (TFB_SoundSample *sample, audio_Object buffer) +{ + uint32 buf_num; + + if (!sample->buffer_tag) + return NULL; // do not have any tags + + for (buf_num = 0; + buf_num < sample->num_buffers && + (!sample->buffer_tag[buf_num].in_use || + sample->buffer_tag[buf_num].buf_name != buffer + ); + buf_num++) + ; + + return buf_num < sample->num_buffers ? + &sample->buffer_tag[buf_num] : NULL; +} + +bool +TFB_TagBuffer (TFB_SoundSample *sample, audio_Object buffer, intptr_t data) +{ + uint32 buf_num; + + if (!sample->buffer_tag) + sample->buffer_tag = HCalloc (sizeof (TFB_SoundTag) * + sample->num_buffers); + + for (buf_num = 0; + buf_num < sample->num_buffers && + sample->buffer_tag[buf_num].in_use && + sample->buffer_tag[buf_num].buf_name != buffer; + buf_num++) + ; + + if (buf_num >= sample->num_buffers) + return false; // no empty slot + + sample->buffer_tag[buf_num].in_use = 1; + sample->buffer_tag[buf_num].buf_name = buffer; + sample->buffer_tag[buf_num].data = data; + + return true; +} + +void +TFB_ClearBufferTag (TFB_SoundTag *ptag) +{ + ptag->in_use = 0; + ptag->buf_name = 0; +} + +static void +remove_scope_data (TFB_SoundSource *source, audio_Object buffer) +{ + audio_IntVal buf_size; + + audio_GetBufferi (buffer, audio_SIZE, &buf_size); + source->sbuf_head += buf_size; + // the buffer is cyclic + source->sbuf_head %= source->sbuf_size; + + source->sbuf_lasttime = GetTimeCounter (); +} + +static void +add_scope_data (TFB_SoundSource *source, uint32 bytes) +{ + uint8 *sbuffer = source->sbuffer; + uint8 *dec_buf = source->sample->decoder->buffer; + uint32 tail_bytes; + uint32 wrap_bytes; + + if (source->sbuf_tail + bytes > source->sbuf_size) + { // does not fit at the tail, have to split it up + tail_bytes = source->sbuf_size - source->sbuf_tail; + wrap_bytes = bytes - tail_bytes; + } + else + { // all fits at the tail + tail_bytes = bytes; + wrap_bytes = 0; + } + + if (tail_bytes) + { + memcpy (sbuffer + source->sbuf_tail, dec_buf, tail_bytes); + source->sbuf_tail += tail_bytes; + } + + if (wrap_bytes) + { + memcpy (sbuffer, dec_buf + tail_bytes, wrap_bytes); + source->sbuf_tail = wrap_bytes; + } +} + +static void +process_stream (TFB_SoundSource *source) +{ + TFB_SoundSample *sample = source->sample; + TFB_SoundDecoder *decoder = sample->decoder; + bool end_chunk_failed = false; + audio_IntVal processed; + audio_IntVal queued; + + audio_GetSourcei (source->handle, audio_BUFFERS_PROCESSED, &processed); + audio_GetSourcei (source->handle, audio_BUFFERS_QUEUED, &queued); + + if (processed == 0) + { // Nothing was played + audio_IntVal state; + + audio_GetSourcei (source->handle, audio_SOURCE_STATE, &state); + if (state != audio_PLAYING) + { + if (queued == 0 && decoder->error == SOUNDDECODER_EOF) + { // The stream has reached the end + log_add (log_Info, "StreamDecoderTaskFunc(): " + "finished playing %s", decoder->filename); + source->stream_should_be_playing = FALSE; + + if (sample->callbacks.OnEndStream) + sample->callbacks.OnEndStream (sample); + } + else + { + log_add (log_Warning, "StreamDecoderTaskFunc(): " + "buffer underrun playing %s", decoder->filename); + audio_SourcePlay (source->handle); + } + } + } + + // Unqueue processed buffers and replace them with new ones + for (; processed > 0; --processed) + { + uint32 error; + audio_Object buffer; + uint32 decoded_bytes; + + audio_GetError (); // clear error state + + // Get the buffer that finished playing + audio_SourceUnqueueBuffers (source->handle, 1, &buffer); + error = audio_GetError(); + if (error != audio_NO_ERROR) + { + log_add (log_Warning, "StreamDecoderTaskFunc(): " + "error after audio_SourceUnqueueBuffers: %x, file %s", + error, decoder->filename); + break; + } + + // Process a callback on a tagged buffer, if any + if (sample->callbacks.OnTaggedBuffer) + { + TFB_SoundTag* tag = TFB_FindTaggedBuffer (sample, buffer); + if (tag) + sample->callbacks.OnTaggedBuffer (sample, tag); + } + + if (source->sbuffer) + remove_scope_data (source, buffer); + + // See what state the decoder was left in last time around + if (decoder->error != SOUNDDECODER_OK) + { + if (decoder->error == SOUNDDECODER_EOF) + { + if (end_chunk_failed) + continue; // should not do it again + + if (!sample->callbacks.OnEndChunk || + !sample->callbacks.OnEndChunk (sample, source->last_q_buf)) + { // Reached the end of the current stream and we did not + // get another sample to play (relevant for Trackplayer) + end_chunk_failed = true; + continue; + } + else + { // OnEndChunk succeeded, so someone (read: Trackplayer) + // wants to keep going, probably with a new decoder. + // Get the new decoder + decoder = sample->decoder; + } + } + else + { // Decoder returned a real error, keep going +#if 0 + log_add (log_Debug, "StreamDecoderTaskFunc(): " + "decoder->error is %d for %s", decoder->error, + decoder->filename); +#endif + continue; + } + } + + // Now replace the unqueued buffer with a new one + decoded_bytes = SoundDecoder_Decode (decoder); + if (decoder->error == SOUNDDECODER_ERROR) + { + log_add (log_Warning, "StreamDecoderTaskFunc(): " + "SoundDecoder_Decode error %d, file %s", + decoder->error, decoder->filename); + source->stream_should_be_playing = FALSE; + continue; + } + + if (decoded_bytes == 0) + { // Nothing was decoded, keep going + continue; + // This loses a stream buffer, which we cannot get back + // w/o restarting the stream, but we should never get here. + } + + // And a new buffer is born + audio_BufferData (buffer, decoder->format, decoder->buffer, + decoded_bytes, decoder->frequency); + error = audio_GetError(); + if (error != audio_NO_ERROR) + { + log_add (log_Warning, "StreamDecoderTaskFunc(): " + "error after audio_BufferData: %x, file %s, decoded %d", + error, decoder->filename, decoded_bytes); + continue; + } + + // Now queue the buffer + audio_SourceQueueBuffers (source->handle, 1, &buffer); + error = audio_GetError(); + if (error != audio_NO_ERROR) + { + log_add (log_Warning, "StreamDecoderTaskFunc(): " + "error after audio_SourceQueueBuffers: %x, file %s, " + "decoded %d", error, decoder->filename, decoded_bytes); + continue; + } + + // Remember the last queued buffer so we can pass it to callbacks + source->last_q_buf = buffer; + if (sample->callbacks.OnQueueBuffer) + sample->callbacks.OnQueueBuffer (sample, buffer); + + if (source->sbuffer) + add_scope_data (source, decoded_bytes); + } +} + +static void +processMusicFade (void) +{ + TimeCount Now; + sint32 elapsed; + int newVolume; + + LockMutex (fade_mutex); + + if (!musicFadeInterval) + { // there is no fade set + UnlockMutex (fade_mutex); + return; + } + + Now = GetTimeCounter (); + elapsed = Now - musicFadeStartTime; + if (elapsed > musicFadeInterval) + elapsed = musicFadeInterval; + + newVolume = musicFadeStartVolume + (long)musicFadeDelta * elapsed + / musicFadeInterval; + SetMusicVolume (newVolume); + + if (elapsed >= musicFadeInterval) + musicFadeInterval = 0; // fade is over + + UnlockMutex (fade_mutex); +} + +static int +StreamDecoderTaskFunc (void *data) +{ + Task task = (Task)data; + int active_streams; + int i; + + while (!Task_ReadState (task, TASK_EXIT)) + { + active_streams = 0; + + processMusicFade (); + + for (i = MUSIC_SOURCE; i < NUM_SOUNDSOURCES; ++i) + { + TFB_SoundSource *source = &soundSource[i]; + + LockMutex (source->stream_mutex); + + if (!source->sample || + !source->sample->decoder || + !source->stream_should_be_playing || + source->sample->decoder->error == SOUNDDECODER_ERROR) + { + UnlockMutex (source->stream_mutex); + continue; + } + + process_stream (source); + active_streams++; + + UnlockMutex (source->stream_mutex); + } + + if (active_streams == 0) + { // Throttle down the thread when there are no active streams + HibernateThread (ONE_SECOND / 10); + } + else + TaskSwitch (); + } + + FinishTask (task); + return 0; +} + +static inline sint32 +readSoundSample (void *ptr, int sample_size) +{ + if (sample_size == sizeof (uint8)) + return (*(uint8*)ptr - 128) << 8; + else + return *(sint16*)ptr; +} + +// Graphs the current sound data for the oscilloscope. +// Includes a rudimentary automatic gain control (AGC) to properly graph +// the streams at different gain levels (based on running average). +// We use AGC because different pieces of music and speech can easily be +// at very different gain levels, because the game is moddable. +int +GraphForegroundStream (uint8 *data, sint32 width, sint32 height, + bool wantSpeech) +{ + int source_num; + TFB_SoundSource *source; + TFB_SoundDecoder *decoder; + int channels; + int sample_size; + int full_sample; + int step; + long played_time; + long delta; + uint8 *sbuffer; + unsigned long pos; + int scale; + sint32 i; + // AGC variables +#define DEF_PAGE_MAX 28000 +#define AGC_PAGE_COUNT 16 + static int page_sum = DEF_PAGE_MAX * AGC_PAGE_COUNT; + static int pages[AGC_PAGE_COUNT] = + { + DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, + DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, + DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, + DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, DEF_PAGE_MAX, + }; + static int page_head; +#define AGC_FRAME_COUNT 8 + static int frame_sum; + static int frames; + static int avg_amp = DEF_PAGE_MAX; // running amplitude (sort of) average + int target_amp; + int max_a; +#define VAD_MIN_ENERGY 100 + long energy; + + + // Prefer speech to music + source_num = SPEECH_SOURCE; + source = &soundSource[source_num]; + LockMutex (source->stream_mutex); + if (wantSpeech && (!source->sample || + !source->sample->decoder || !source->sample->decoder->is_null)) + { // Use speech waveform, since it's available + // Step is picked experimentally. Using step of 1 sample at 11025Hz, + // because human speech is mostly in the low frequencies, and it looks + // better this way. + step = 1; + } + else + { // We do not have speech -- use music waveform + UnlockMutex (source->stream_mutex); + + source_num = MUSIC_SOURCE; + source = &soundSource[source_num]; + LockMutex (source->stream_mutex); + + // Step is picked experimentally. Using step of 4 samples at 11025Hz. + // It looks better this way. + step = 4; + } + + if (!PlayingStream (source_num) || !source->sample + || !source->sample->decoder || !source->sbuffer + || source->sbuf_size == 0) + { // We don't have data to return, oh well. + UnlockMutex (source->stream_mutex); + return 0; + } + decoder = source->sample->decoder; + + if (!audio_GetFormatInfo (decoder->format, &channels, &sample_size)) + { + UnlockMutex (source->stream_mutex); + log_add (log_Debug, "GraphForegroundStream(): uknown format %u", + (unsigned)decoder->format); + return 0; + } + full_sample = channels * sample_size; + + // See how far into the buffer we should be now + played_time = GetTimeCounter () - source->sbuf_lasttime; + delta = played_time * decoder->frequency * full_sample / ONE_SECOND; + // align delta to sample start + delta = delta & ~(full_sample - 1); + + if (delta < 0) + { + log_add (log_Debug, "GraphForegroundStream(): something is messed" + " with timing, delta %ld", delta); + delta = 0; + } + else if (delta > (long)source->sbuf_size) + { // Stream decoder task has just had a heart attack, not much we can do + delta = 0; + } + + // Step is in 11025 Hz units, so we need to adjust to source frequency + step = decoder->frequency * step / 11025; + if (step == 0) + step = 1; + step *= full_sample; + + sbuffer = source->sbuffer; + pos = source->sbuf_head + delta; + + // We are not basing the scaling factor on signal energy, because we + // want it to *look* pretty instead of sounding nice and even + target_amp = (height >> 1) >> 1; + scale = avg_amp / target_amp; + + max_a = 0; + energy = 0; + for (i = 0; i < width; ++i, pos += step) + { + sint32 s; + int t; + + pos %= source->sbuf_size; + + s = readSoundSample (sbuffer + pos, sample_size); + if (channels > 1) + s += readSoundSample (sbuffer + pos + sample_size, sample_size); + + energy += (s * s) / 0x10000; + t = abs(s); + if (t > max_a) + max_a = t; + + s = (s / scale) + (height >> 1); + if (s < 0) + s = 0; + else if (s > height - 1) + s = height - 1; + + data[i] = s; + } + energy /= width; + + // Very basic VAD. We don't want to count speech pauses in the average + if (energy > VAD_MIN_ENERGY) + { + // Record the maximum amplitude (sort of) + frame_sum += max_a; + ++frames; + if (frames == AGC_FRAME_COUNT) + { // Got a full page + frame_sum /= AGC_FRAME_COUNT; + // Record the page + page_sum -= pages[page_head]; + page_sum += frame_sum; + pages[page_head] = frame_sum; + page_head = (page_head + 1) % AGC_PAGE_COUNT; + + frame_sum = 0; + frames = 0; + + avg_amp = page_sum / AGC_PAGE_COUNT; + } + } + + UnlockMutex (source->stream_mutex); + return 1; +} + +// This function is normally called on the Starcon2Main thread +bool +SetMusicStreamFade (sint32 howLong, int endVolume) +{ + bool ret = true; + + LockMutex (fade_mutex); + + if (howLong < 0) + howLong = 0; + + musicFadeStartTime = GetTimeCounter (); + musicFadeInterval = howLong; + musicFadeStartVolume = musicVolume; + musicFadeDelta = endVolume - musicFadeStartVolume; + if (!musicFadeInterval) + ret = false; // reject + + UnlockMutex (fade_mutex); + + return ret; +} + +int +InitStreamDecoder (void) +{ + fade_mutex = CreateMutex ("Stream fade mutex", SYNC_CLASS_AUDIO); + if (!fade_mutex) + return -1; + + decoderTask = AssignTask (StreamDecoderTaskFunc, 1024, + "audio stream decoder"); + if (!decoderTask) + return -1; + + return 0; +} + +void +UninitStreamDecoder (void) +{ + if (decoderTask) + { + ConcludeTask (decoderTask); + decoderTask = NULL; + } + + if (fade_mutex) + { + DestroyMutex (fade_mutex); + fade_mutex = NULL; + } +} -- cgit v1.2.3