Parallel Overhead 1.2.0 for fredygames.com (MIT, same licence as the game). Changes in the browser build (everything is under #ifdef __EMSCRIPTEN__, the desktop build is unchanged): - Escape does not end the game (it would leave a dead canvas) and the window starts at 720x720; the page scales the canvas. - The OpenGL ES context is requested as 3.0, which SDL maps to WebGL 2 (the shaders are ES 2.0 and run as they are); ES 2.0 would give WebGL 1, which does not repeat textures whose size is not a power of two. - The browser opens a single audio device, so the sound effects are converted to the music format and mixed into the music callback, and the music is silenced instead of pausing the device. Apply with: patch -p1 < parallel-overhead-fredygames.patch --- a/src/audio.c +++ b/src/audio.c @@ -24,6 +24,14 @@ char priority_sound_played; int music_place; unsigned char current_music; + +#ifdef __EMSCRIPTEN__ + //The browser opens a single audio device: the sound effects are converted to the music format and + //mixed into the music callback, and the music is silenced instead of pausing the device. + char music_paused; + struct audio *sfx; + Uint32 sfx_place; +#endif } AudioData; @@ -45,20 +53,76 @@ return audio; } +#ifdef __EMSCRIPTEN__ +/** + * Convert a loaded sound to the given format, in place. + */ +void convert_audio(struct audio *audio, SDL_AudioSpec target) +{ + SDL_AudioCVT cvt; + if (SDL_BuildAudioCVT(&cvt, audio->spec.format, audio->spec.channels, audio->spec.freq, + target.format, target.channels, target.freq) < 0) + { + report_SDL_error("convert audio"); + return; + } + cvt.len = audio->length; + cvt.buf = malloc(cvt.len * cvt.len_mult); + memcpy(cvt.buf, audio->buffer, audio->length); + SDL_ConvertAudio(&cvt); + SDL_FreeWAV(audio->buffer); + audio->buffer = cvt.buf; + audio->length = cvt.len_cvt; + audio->spec = target; +} + +//Mix the sound effect that is playing, if any, into the stream. +void mix_sfx(unsigned char *stream, int len) +{ + if (!AudioData.sfx || AudioData.sfx_place >= AudioData.sfx->length) + { + AudioData.sfx = NULL; + return; + } + Uint32 n = AudioData.sfx->length - AudioData.sfx_place; + if (n > (Uint32) len) + { + n = len; + } + SDL_MixAudioFormat(stream, AudioData.sfx->buffer + AudioData.sfx_place, AudioData.music[0].spec.format, n, SDL_MIX_MAXVOLUME); + AudioData.sfx_place += n; +} +#endif + //SDL_AudioSpec.callback function void loop_music(void *userdata, unsigned char *stream, int len) { +#ifdef __EMSCRIPTEN__ + if (AudioData.music_paused) + { + memset(stream, 0, len); + mix_sfx(stream, len); + return; + } + memset(stream, 0, len); +#endif unsigned char cur = AudioData.current_music; if (AudioData.music_place + len < AudioData.music[cur].length) { memmove(stream, AudioData.music[cur].buffer + AudioData.music_place, len); AudioData.music_place += len; +#ifdef __EMSCRIPTEN__ + mix_sfx(stream, len); +#endif return; } memmove(stream, AudioData.music[cur].buffer + AudioData.music_place, AudioData.music[cur].length - AudioData.music_place); AudioData.music_place = 0; AudioData.current_music = (cur + 1) % 2; +#ifdef __EMSCRIPTEN__ + mix_sfx(stream, len); +#endif } /** @@ -81,6 +145,20 @@ SDL_AudioSpec music_spec = AudioData.music[0].spec; music_spec.callback = loop_music; +#ifdef __EMSCRIPTEN__ + convert_audio(&AudioData.move, AudioData.music[0].spec); + convert_audio(&AudioData.death, AudioData.music[0].spec); + AudioData.music_paused = 1; + //No ALLOW_ANY_CHANGE: SDL converts to the rate of the browser's audio context. + AudioData.music_device = SDL_OpenAudioDevice(NULL, 0, &music_spec, &obt, 0); + if (!AudioData.music_device) + { + report_SDL_error("open audio device (music)"); + } + SDL_PauseAudioDevice(AudioData.music_device, 0); + return; +#endif + AudioData.music_device = SDL_OpenAudioDevice(NULL, 0, &music_spec, &obt, SDL_AUDIO_ALLOW_ANY_CHANGE); if (!AudioData.music_device) { @@ -119,6 +197,10 @@ */ void pause_music(char pause) { +#ifdef __EMSCRIPTEN__ + AudioData.music_paused = pause; + return; +#endif SDL_PauseAudioDevice(AudioData.music_device, pause); } @@ -127,7 +209,11 @@ */ void reset_music() { +#ifdef __EMSCRIPTEN__ + AudioData.music_paused = 1; +#else SDL_PauseAudioDevice(AudioData.music_device, 1); +#endif AudioData.current_music = 0; AudioData.music_place = 0; } @@ -139,6 +225,16 @@ return; } +#ifdef __EMSCRIPTEN__ + if (AudioData.priority_sound_played && AudioData.sfx && AudioData.sfx_place < AudioData.sfx->length) + { + return; + } + AudioData.sfx = &AudioData.move; + AudioData.sfx_place = 0; + AudioData.priority_sound_played = 0; + return; +#endif if (AudioData.priority_sound_played && SDL_GetQueuedAudioSize(AudioData.sound_device)) { return; @@ -158,6 +254,13 @@ return; } +#ifdef __EMSCRIPTEN__ + AudioData.sfx = &AudioData.death; + AudioData.sfx_place = 0; + AudioData.priority_sound_played = 1; + return; +#endif + SDL_ClearQueuedAudio(AudioData.sound_device); if (SDL_QueueAudio(AudioData.sound_device, AudioData.death.buffer, AudioData.death.length)) { --- a/src/init_sdl.c +++ b/src/init_sdl.c @@ -107,7 +107,13 @@ SDL_Quit(); return RET_SDL_ERR; } +#ifdef __EMSCRIPTEN__ + //SDL maps OpenGL ES 3.0 to a WebGL 2 context (ES 2.0 would give WebGL 1, which has no repeat + //wrapping for textures whose size is not a power of two). The ES 2.0 shaders run unchanged. + if (SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3)) +#else if (SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2)) +#endif { report_SDL_error("SDL_GL_SetAttribute"); SDL_Quit(); --- a/src/run_game.c +++ b/src/run_game.c @@ -126,7 +126,12 @@ case SDLK_BACKSPACE: return 1; case SDLK_ESCAPE: +#ifdef __EMSCRIPTEN__ + //Closing the game would leave a dead canvas in the page. + break; +#else return 0; +#endif } break; case SDL_MOUSEBUTTONDOWN: --- a/src/settings.c +++ b/src/settings.c @@ -6,7 +6,12 @@ struct settings Settings = { .paths = {.data_dir_len = 0}, +#ifdef __EMSCRIPTEN__ + //The page scales the canvas to fit its frame. + .window = {SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 720, 720}, +#else .window = {SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 480, 480}, +#endif .tunnel = { .length = 200, .sectors = 10, .section_length = 1.0,