Skip to content

Commit 6793c12

Browse files
committed
stash: web audio api
1 parent 84701a2 commit 6793c12

8 files changed

Lines changed: 389 additions & 35 deletions

File tree

CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ endif()
885885
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
886886
option(PLAYER_JS_BUILD_SHELL
887887
"Build the Player executable as a shell file (.html) instead of a standalone javascript file (.js)" OFF)
888+
option(PLAYER_JS_AUDIO_WORKLETS "Use the newer Web Audio API instead of the built-in support from SDL2" OFF)
888889
set(PLAYER_JS_GAME_URL "games/" CACHE STRING "Game URL/directory where the web player searches for games")
889890
set(PLAYER_JS_OUTPUT_NAME "easyrpg-player" CACHE STRING "Output name of the js, html and wasm files")
890891
set_property(SOURCE src/async_handler.cpp APPEND PROPERTY
@@ -1260,6 +1261,9 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N
12601261
# Do not confuse this with our own -DUSE_SDL
12611262
# Prevent Emscripten from pulling in bundled SDL headers that break the build
12621263
target_compile_options(${PROJECT_NAME} PUBLIC "-sUSE_SDL=0")
1264+
if(PLAYER_JS_AUDIO_WORKLETS)
1265+
target_compile_options(${PROJECT_NAME} PUBLIC "-pthread")
1266+
endif()
12631267

12641268
set(PLAYER_JS_PREJS "${CMAKE_CURRENT_SOURCE_DIR}/resources/emscripten/emscripten-pre.js")
12651269
set(PLAYER_JS_POSTJS "${CMAKE_CURRENT_SOURCE_DIR}/resources/emscripten/emscripten-post.js")
@@ -1279,15 +1283,26 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N
12791283
set_property(TARGET ${EXE_NAME} APPEND_STRING PROPERTY LINK_FLAGS " --shell-file ${PLAYER_JS_SHELL}")
12801284
endif()
12811285

1286+
if(PLAYER_JS_AUDIO_WORKLETS)
1287+
set_property(TARGET ${EXE_NAME} APPEND_STRING PROPERTY LINK_FLAGS
1288+
" -sINITIAL_MEMORY=67108864 -sAUDIO_WORKLET -sWASM_WORKERS -sUSE_PTHREADS -sPTHREAD_POOL_SIZE=1 -sPTHREAD_POOL_SIZE_STRICT=2")
1289+
endif()
1290+
12821291
target_compile_options(${PROJECT_NAME} PUBLIC -fno-exceptions)
12831292

12841293
target_sources(${PROJECT_NAME} PRIVATE
12851294
src/platform/emscripten/clock.h
12861295
src/platform/emscripten/interface.cpp
12871296
src/platform/emscripten/interface.h)
1297+
if(PLAYER_JS_AUDIO_WORKLETS)
1298+
target_sources(${PROJECT_NAME} PRIVATE
1299+
src/platform/emscripten/audio.h
1300+
src/platform/emscripten/audio.cpp)
1301+
endif()
12881302

12891303
target_link_libraries(${EXE_NAME} "idbfs.js")
12901304
target_link_libraries(${EXE_NAME} "websocket.js")
1305+
target_link_libraries(${EXE_NAME} "wasmfs_fetch.js")
12911306
set_property(TARGET ${EXE_NAME} APPEND_STRING PROPERTY LINK_FLAGS " -s EXPORTED_RUNTIME_METHODS='[\"ccall\",\"cwrap\",\"intArrayFromString\",\"ALLOC_NORMAL\",\"allocate\",\"getValue\",\"FS\"]'")
12921307
set_target_properties(${EXE_NAME} PROPERTIES OUTPUT_NAME "${PLAYER_JS_OUTPUT_NAME}")
12931308
endif()

resources/emscripten/emscripten-pre.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const isMainThread = !!globalThis?.window;
2+
13
// Note: The `Module` context is already initialized as an
24
// empty object by emscripten even before the pre script
35
Object.assign(Module, {
@@ -13,6 +15,7 @@ Object.assign(Module, {
1315
},
1416

1517
canvas: (() => {
18+
if (!isMainThread) return undefined;
1619
const canvas = document.getElementById('canvas');
1720

1821
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
@@ -28,6 +31,7 @@ Object.assign(Module, {
2831
})(),
2932

3033
setStatus: text => {
34+
if (!isMainThread) return;
3135
if (!Module.setStatus.last) Module.setStatus.last = {
3236
time: Date.now(),
3337
text: ''
@@ -54,7 +58,8 @@ var lang_pushed = false;
5458
* Parses the current location query to setup a specific game
5559
*/
5660
function parseArgs () {
57-
const items = window.location.search.substr(1).split("&");
61+
if (!isMainThread) return [];
62+
const items = location.search.substr(1).split("&");
5863
let result = [];
5964

6065
// Store saves in subdirectory `Save`
@@ -139,7 +144,7 @@ if (Module.wsUrl === undefined) {
139144
}
140145

141146
// Catch all errors occuring inside the window
142-
window.addEventListener('error', (event) => {
147+
globalThis.window?.addEventListener('error', (event) => {
143148
// workaround chrome bug: See https://github.com/EasyRPG/Player/issues/2806
144149
if (event.error.message.includes("side-effect in debug-evaluate") && event.defaultPrevented) {
145150
return;

src/audio_generic.cpp

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,62 @@ bool GenericAudio::PlayOnChannel(SeChannel& chan, std::unique_ptr<AudioSeCache>
301301
chan.paused = false; // Unpause channel -> Play it.
302302
return true;
303303
}
304-
305304
void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) {
305+
auto [channel_active, total_volume, samples_per_frame] = GenericAudio::Decode(buffer_length);
306+
if (channel_active) {
307+
if (total_volume > 1.0) {
308+
float threshold = 0.8;
309+
for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) {
310+
float sample = mixer_buffer[i];
311+
float sign = (sample < 0) ? -1.0 : 1.0;
312+
sample /= sign;
313+
//dynamic range compression
314+
if (sample > threshold) {
315+
sample_buffer[i] = sign * 32768.0 * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold));
316+
} else {
317+
sample_buffer[i] = sign * sample * 32768.0;
318+
}
319+
}
320+
} else {
321+
//No dynamic range compression necessary
322+
for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) {
323+
sample_buffer[i] = mixer_buffer[i] * 32768.0;
324+
}
325+
}
326+
327+
memcpy(output_buffer, sample_buffer.data(), buffer_length);
328+
} else {
329+
memset(output_buffer, '\0', buffer_length);
330+
}
331+
}
332+
333+
void GenericAudio::Decode(float* output_buffer, int buffer_length) {
334+
auto [channel_active, total_volume, samples_per_frame] = GenericAudio::Decode(buffer_length);
335+
if (channel_active) {
336+
if (total_volume > 1.0) {
337+
float threshold = 0.8;
338+
for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); ++i) {
339+
float sample = mixer_buffer[i];
340+
float sign = (sample < 0) ? -1.0 : 1.0;
341+
sample /= sign;
342+
if (sample > threshold) {
343+
output_buffer[i] = sign * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold));
344+
} else {
345+
output_buffer[i] = sign * sample;
346+
}
347+
}
348+
} else {
349+
memcpy(output_buffer, mixer_buffer.data(), buffer_length);
350+
}
351+
} else {
352+
memset(output_buffer, '\0', buffer_length);
353+
}
354+
}
355+
356+
GenericAudio::DecodeResult GenericAudio::Decode(int buffer_length) {
306357
bool channel_active = false;
307358
float total_volume = 0;
308-
int samples_per_frame = buffer_length / output_format.channels / 2;
359+
int samples_per_frame = buffer_length / output_format.channels / AudioDecoder::GetSamplesizeForFormat(output_format.format);
309360

310361
assert(buffer_length > 0);
311362

@@ -471,32 +522,7 @@ void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) {
471522
channel_active = true;
472523
}
473524
}
474-
475-
if (channel_active) {
476-
if (total_volume > 1.0) {
477-
float threshold = 0.8f;
478-
for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) {
479-
float sample = mixer_buffer[i];
480-
float sign = (sample < 0) ? -1.0 : 1.0;
481-
sample /= sign;
482-
//dynamic range compression
483-
if (sample > threshold) {
484-
sample_buffer[i] = sign * 32768.0 * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold));
485-
} else {
486-
sample_buffer[i] = sign * sample * 32768.0;
487-
}
488-
}
489-
} else {
490-
//No dynamic range compression necessary
491-
for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) {
492-
sample_buffer[i] = mixer_buffer[i] * 32768.0;
493-
}
494-
}
495-
496-
memcpy(output_buffer, sample_buffer.data(), buffer_length);
497-
} else {
498-
memset(output_buffer, '\0', buffer_length);
499-
}
525+
return {channel_active, total_volume, samples_per_frame};
500526
}
501527

502528
void GenericAudio::BgmChannel::Stop() {
@@ -505,9 +531,9 @@ void GenericAudio::BgmChannel::Stop() {
505531
midi_out_used = false;
506532
instance->midi_thread->GetMidiOut().Reset();
507533
instance->midi_thread->GetMidiOut().Pause();
508-
} else if (decoder) {
509-
decoder.reset();
510534
}
535+
// Don't reset the decoder here, another thread could be using it right now
536+
// it will be reset on the next call to Decode
511537
}
512538

513539
void GenericAudio::BgmChannel::SetPaused(bool newPaused) {

src/audio_generic.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class GenericAudio : public AudioInterface {
7070
virtual void UnlockMutex() const = 0;
7171

7272
void Decode(uint8_t* output_buffer, int buffer_length);
73+
void Decode(float* output_buffer, int buffer_length);
7374

7475
private:
7576
struct BgmChannel {
@@ -118,6 +119,12 @@ class GenericAudio : public AudioInterface {
118119
std::vector<float> mixer_buffer = {};
119120

120121
std::unique_ptr<GenericAudioMidiOut> midi_thread;
122+
typedef struct DecodeResult {
123+
bool channel_active;
124+
float total_volume;
125+
int samples_per_frame;
126+
} DecodeResult;
127+
DecodeResult Decode(int buffer_length);
121128
};
122129

123130
#endif

0 commit comments

Comments
 (0)