diff --git a/sc2/ChangeLog b/sc2/ChangeLog index 83c557643..bf720ca0f 100644 --- a/sc2/ChangeLog +++ b/sc2/ChangeLog @@ -1,4 +1,6 @@ Changes towards version 0.7: +- Allow any sound data format to be graphed by comm oscilloscope; also + auto-adjust the scope for different gain levels (bug #1064) - Alex - Game settings Quit menu now delegates to F10 quit (bug #462) - Alex - Do not pause the game in places where not relevant (bug #984) - Alex - Fixed crashes and potential weirdness when loading savegames from diff --git a/sc2/src/sc2code/libs/sound/audiocore.c b/sc2/src/sc2code/libs/sound/audiocore.c index c74d87e86..a271045b9 100644 --- a/sc2/src/sc2code/libs/sound/audiocore.c +++ b/sc2/src/sc2code/libs/sound/audiocore.c @@ -237,3 +237,31 @@ audio_BufferData (audio_Object bufobj, uint32 format, void* data, audiodrv.BufferData (bufobj, audiodrv.EnumLookup[format], data, size, freq); } + +bool +audio_GetFormatInfo (uint32 format, int *channels, int *sample_size) +{ + switch (format) + { + case audio_FORMAT_MONO8: + *channels = 1; + *sample_size = sizeof (uint8); + return true; + + case audio_FORMAT_STEREO8: + *channels = 2; + *sample_size = sizeof (uint8); + return true; + + case audio_FORMAT_MONO16: + *channels = 1; + *sample_size = sizeof (sint16); + return true; + + case audio_FORMAT_STEREO16: + *channels = 2; + *sample_size = sizeof (sint16); + return true; + } + return false; +} diff --git a/sc2/src/sc2code/libs/sound/audiocore.h b/sc2/src/sc2code/libs/sound/audiocore.h index 4a2c6352c..fcb2349bb 100644 --- a/sc2/src/sc2code/libs/sound/audiocore.h +++ b/sc2/src/sc2code/libs/sound/audiocore.h @@ -164,4 +164,6 @@ void audio_GetBufferi (audio_Object bufobj, audio_BufferProp pname, void audio_BufferData (audio_Object bufobj, uint32 format, void* data, uint32 size, uint32 freq); +bool audio_GetFormatInfo (uint32 format, int *channels, int *sample_size); + #endif /* _AUDIOCORE_H */ diff --git a/sc2/src/sc2code/libs/sound/sound.h b/sc2/src/sc2code/libs/sound/sound.h index b03440a3d..f91307659 100644 --- a/sc2/src/sc2code/libs/sound/sound.h +++ b/sc2/src/sc2code/libs/sound/sound.h @@ -75,11 +75,11 @@ typedef struct tfb_soundsource sint32 start_time; void *positional_object; - // for oscilloscope + // Cyclic waveform buffer for oscilloscope void *sbuffer; - uint32 sbuf_start; + uint32 sbuf_start; // cyclic buffer tail (confusing, eh?) uint32 sbuf_size; - uint32 sbuf_offset; + uint32 sbuf_offset; // cyclic buffer head uint32 sbuf_lasttime; // keep track for paused tracks uint32 pause_time; diff --git a/sc2/src/sc2code/libs/sound/stream.c b/sc2/src/sc2code/libs/sound/stream.c index a2b96600f..0a3d0feb0 100644 --- a/sc2/src/sc2code/libs/sound/stream.c +++ b/sc2/src/sc2code/libs/sound/stream.c @@ -449,3 +449,175 @@ StreamDecoderTaskFunc (void *data) return 0; } +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) +{ + 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; + + + if (speechVolumeScale != 0.0f) + { // Use speech waveform when speech is enabled + source_num = SPEECH_SOURCE; + // 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 if (musicVolumeScale != 0.0f) + { // Use music waveform when speech is disabled + source_num = MUSIC_SOURCE; + // Step is picked experimentally. Using step of 4 samples at 11025Hz. + // It looks better this way. + step = 4; + } + else + { + return 0; + } + + source = &soundSource[source_num]; + LockMutex (source->stream_mutex); + 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; + + assert (audio_GetFormatInfo (decoder->format, &channels, &sample_size)); + 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_offset + 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 == 8) + { // 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; +} + diff --git a/sc2/src/sc2code/libs/sound/stream.h b/sc2/src/sc2code/libs/sound/stream.h index 1bac490ae..aac8f2841 100644 --- a/sc2/src/sc2code/libs/sound/stream.h +++ b/sc2/src/sc2code/libs/sound/stream.h @@ -30,4 +30,6 @@ TFB_SoundTag* FindTaggedBuffer (TFB_SoundSample* sample, audio_Object buffer); void TFB_ClearBufferTag (TFB_SoundTag* ptag); void TFB_TagBuffer (TFB_SoundSample* sample, audio_Object buffer, void* data); +int GraphForegroundStream (uint8 *data, sint32 width, sint32 height); + #endif diff --git a/sc2/src/sc2code/libs/sound/trackplayer.c b/sc2/src/sc2code/libs/sound/trackplayer.c index 953bb039e..a8ac56e89 100644 --- a/sc2/src/sc2code/libs/sound/trackplayer.c +++ b/sc2/src/sc2code/libs/sound/trackplayer.c @@ -19,9 +19,7 @@ #include "libs/sound/trackint.h" #include "libs/log.h" #include "comm.h" -#include "sis.h" #include "options.h" -#include #include #include @@ -821,179 +819,6 @@ FastForward_Page () return TRUE; } -// processes sound data to oscilloscope -int -GetSoundData (void *data) -{ - // XXX: These two variants are begging to be merged - if (speechVolumeScale != 0.0f) - { - // speech is enabled - - LockMutex (soundSource[SPEECH_SOURCE].stream_mutex); - if (soundSource[SPEECH_SOURCE].sample && soundSource[SPEECH_SOURCE].sample->decoder && - PlayingStream (SPEECH_SOURCE) && soundSource[SPEECH_SOURCE].sbuffer && - soundSource[SPEECH_SOURCE].sbuf_size > 0) - { - float played_time = (GetTimeCounter () - soundSource[SPEECH_SOURCE].sbuf_lasttime) / - (float)ONE_SECOND; - long delta = (int) (played_time * (float)soundSource[SPEECH_SOURCE]. - sample->decoder->frequency * 2.0f); - unsigned long pos; - int i; - int step; - UBYTE *scopedata = (UBYTE *) data; - UBYTE *sbuffer = soundSource[SPEECH_SOURCE].sbuffer; - - assert (soundSource[SPEECH_SOURCE].sample->decoder->frequency >= 11025); - assert (soundSource[SPEECH_SOURCE].sample->decoder->format == audio_FORMAT_MONO16); - - // Using step of 1 sample at 11025Hz, because the human speech - // is mostly in the low frequencies - step = soundSource[SPEECH_SOURCE].sample->decoder->frequency * 2 / 11025; - step = (step + 1) & ~1; - - if (delta < 0) - { - log_add (log_Debug, "GetSoundData(): something's messed" - " with timing, delta %ld", delta); - delta = 0; - } - else if (delta > (int)(soundSource[SPEECH_SOURCE].sbuf_size * 2)) - { -#if 0 - log_add (log_Debug, "GetSoundData(): something's messed" - " with timing, delta %d", delta); -#endif - delta = 0; - } -#if 0 - log_add (log_Debug, "played_data %d total_decoded %d delta %d", - played_data, soundSource[SPEECH_SOURCE].total_decoded, - delta); -#endif - pos = soundSource[SPEECH_SOURCE].sbuf_offset + delta; - if (pos % 2 == 1) - pos++; - - // pos is an unsigned data type; this assertion cannot fail! - // assert (pos >= 0); - - for (i = 0; i < RADAR_WIDTH - 2; ++i) - { - SDWORD s; - - for (;;) - { - if (pos >= soundSource[SPEECH_SOURCE].sbuf_size) - pos = pos - soundSource[SPEECH_SOURCE].sbuf_size; - else - break; - } - - s = *(SWORD*) (&sbuffer[pos]); - s = (s / 1360) + (RADAR_HEIGHT >> 1); - if (s < 1) - s = 1; - else if (s > RADAR_HEIGHT - 2) - s = RADAR_HEIGHT - 2; - scopedata[i] = (UBYTE) s; - - pos += step; - } - - UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex); - return 1; - } - UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex); - } - else if (musicVolumeScale != 0.0f) - { - // speech is disabled but music is not so process it instead - - LockMutex (soundSource[MUSIC_SOURCE].stream_mutex); - if (soundSource[MUSIC_SOURCE].sample && soundSource[MUSIC_SOURCE].sample->decoder && - PlayingStream (MUSIC_SOURCE) && soundSource[MUSIC_SOURCE].sbuffer && - soundSource[MUSIC_SOURCE].sbuf_size > 0) - { - float played_time = (GetTimeCounter () - soundSource[MUSIC_SOURCE].sbuf_lasttime) / - (float)ONE_SECOND; - int delta = (int) (played_time * (float)soundSource[MUSIC_SOURCE]. - sample->decoder->frequency * 4.0f); - unsigned long pos; - int i, step; - UBYTE *scopedata = (UBYTE *) data; - UBYTE *sbuffer = soundSource[MUSIC_SOURCE].sbuffer; - - assert (soundSource[MUSIC_SOURCE].sample->decoder->frequency >= 11025); - assert (soundSource[MUSIC_SOURCE].sample->decoder->format == audio_FORMAT_STEREO16); - - // Using step of 4 samples at 11025Hz for the music - step = soundSource[MUSIC_SOURCE].sample->decoder->frequency / 11025 * 16; - if (step % 2 == 1) - step++; - - if (delta < 0) - { -#if 0 - log_add (log_Debug, "GetSoundData(): something's messed" - " with timing, delta %d", delta); -#endif - delta = 0; - } - else if (delta > (int)(soundSource[MUSIC_SOURCE].sbuf_size * 2)) - { -#if 0 - log_add (log_Debug, "GetSoundData(): something's messed" - " with timing, delta %d", delta); -#endif - delta = 0; - } -#if 0 - log_add (log_Debug, "played_data %d total_decoded %d delta %d", - played_data, soundSource[MUSIC_SOURCE].total_decoded, - delta); -#endif - pos = soundSource[MUSIC_SOURCE].sbuf_offset + delta; - if (pos % 2 == 1) - pos++; - - // pos is an unsigned type; this assertion cannot fail! - // assert (pos >= 0); - - for (i = 0; i < RADAR_WIDTH - 2; ++i) - { - SDWORD s; - - for (;;) - { - if (pos >= soundSource[MUSIC_SOURCE].sbuf_size) - pos = pos - soundSource[MUSIC_SOURCE].sbuf_size; - else - break; - } - - s = (*(SWORD*)(&sbuffer[pos])) + (*(SWORD*)(&sbuffer[pos + 2])); - - s = (s / 1800) + (RADAR_HEIGHT >> 1); - if (s < 1) - s = 1; - else if (s > RADAR_HEIGHT - 2) - s = RADAR_HEIGHT - 2; - scopedata[i] = (UBYTE) s; - - pos += step; - } - - UnlockMutex (soundSource[MUSIC_SOURCE].stream_mutex); - return 1; - } - UnlockMutex (soundSource[MUSIC_SOURCE].stream_mutex); - } - - return 0; -} - // tells current position of streaming speech int GetSoundInfo (int max_len) diff --git a/sc2/src/sc2code/libs/sound/trackplayer.h b/sc2/src/sc2code/libs/sound/trackplayer.h index 02ee34bfb..81b32fa38 100644 --- a/sc2/src/sc2code/libs/sound/trackplayer.h +++ b/sc2/src/sc2code/libs/sound/trackplayer.h @@ -37,7 +37,6 @@ void FastReverse_Page(void); void StopTrack(void); void SpliceTrack(UNICODE *filespec, UNICODE *textspec, UNICODE *TimeStamp, TFB_TrackCB cb); void SpliceMultiTrack (UNICODE *TrackNames[], UNICODE *TrackText); -int GetSoundData (void *data); int GetSoundInfo (int max_len); #endif diff --git a/sc2/src/sc2code/oscill.c b/sc2/src/sc2code/oscill.c index 21d237c83..872ae54f9 100644 --- a/sc2/src/sc2code/oscill.c +++ b/sc2/src/sc2code/oscill.c @@ -21,6 +21,7 @@ #include "sis.h" #include "libs/graphics/gfx_common.h" #include "libs/graphics/drawable.h" +#include "libs/sound/sound.h" #include "libs/sound/trackplayer.h" @@ -92,15 +93,15 @@ Oscilloscope (DWORD grab_data) return; TFB_DrawImage_Image (scope_bg, 0, 0, 0, NULL, scope_surf); - if (GetSoundData (scope_data)) + if (GraphForegroundStream (scope_data, RADAR_WIDTH - 2, RADAR_HEIGHT - 2)) { int i, r, g, b; TFB_DrawCanvas_GetPixel (scope_bg->NormalImg, scope_bg->extent.width / 2, scope_bg->extent.height / 2, &r, &g, &b); for (i = 0; i < RADAR_WIDTH - 3; ++i) - TFB_DrawImage_Line (i + 1, scope_data[i], i + 2, - scope_data[i + 1], r, g, b, scope_surf); + TFB_DrawImage_Line (i + 1, scope_data[i] + 1, i + 2, + scope_data[i + 1] + 1, r, g, b, scope_surf); } TFB_DrawImage_Image (scope_surf, 0, 0, 0, NULL, scope_frame->image);