@adatechnology/game-audio 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/assets/sfx/CREDITOS.md +1057 -0
- package/assets/sfx/ambiente/ambience_city_day_01.ogg +0 -0
- package/assets/sfx/ambiente/ambience_city_day_02.ogg +0 -0
- package/assets/sfx/ambiente/ambience_construction_01.ogg +0 -0
- package/assets/sfx/ambiente/ambience_night_01.ogg +0 -0
- package/assets/sfx/ambiente/ambience_room_tone_01.ogg +0 -0
- package/assets/sfx/ambiente/ambience_room_tone_02.ogg +0 -0
- package/assets/sfx/ambiente/ambience_rural_day_01.ogg +0 -0
- package/assets/sfx/ambiente/ambience_rural_day_02.ogg +0 -0
- package/assets/sfx/ambiente/rain_loop_01.ogg +0 -0
- package/assets/sfx/ambiente/water_river_loop_01.ogg +0 -0
- package/assets/sfx/ambiente/water_river_loop_02.ogg +0 -0
- package/assets/sfx/ambiente/water_river_loop_03.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_01.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_02.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_03.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_04.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_05.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_06.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_07.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_08.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_09.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_10.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_11.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_12.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_13.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_14.ogg +0 -0
- package/assets/sfx/ambiente/water_splash_15.ogg +0 -0
- package/assets/sfx/armas/bolt_lift_medium_01.ogg +0 -0
- package/assets/sfx/armas/bolt_lift_medium_02.ogg +0 -0
- package/assets/sfx/armas/bolt_lift_medium_03.ogg +0 -0
- package/assets/sfx/armas/bolt_lift_medium_04.ogg +0 -0
- package/assets/sfx/armas/bolt_lift_medium_05.ogg +0 -0
- package/assets/sfx/armas/bolt_lock_medium_01.ogg +0 -0
- package/assets/sfx/armas/bolt_lock_medium_02.ogg +0 -0
- package/assets/sfx/armas/bolt_lock_medium_03.ogg +0 -0
- package/assets/sfx/armas/bolt_lock_medium_04.ogg +0 -0
- package/assets/sfx/armas/bolt_lock_medium_05.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_heavy_01.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_01.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_02.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_03.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_04.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_05.ogg +0 -0
- package/assets/sfx/armas/bolt_pull_medium_06.ogg +0 -0
- package/assets/sfx/armas/bolt_push_heavy_01.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_01.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_02.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_03.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_04.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_05.ogg +0 -0
- package/assets/sfx/armas/bolt_push_medium_06.ogg +0 -0
- package/assets/sfx/armas/casing_drop_concreto_01.ogg +0 -0
- package/assets/sfx/armas/casing_drop_concreto_02.ogg +0 -0
- package/assets/sfx/armas/casing_drop_concreto_03.ogg +0 -0
- package/assets/sfx/armas/casing_drop_metal_01.ogg +0 -0
- package/assets/sfx/armas/casing_drop_metal_02.ogg +0 -0
- package/assets/sfx/armas/casing_drop_metal_03.ogg +0 -0
- package/assets/sfx/armas/casing_drop_terra_01.ogg +0 -0
- package/assets/sfx/armas/casing_drop_terra_02.ogg +0 -0
- package/assets/sfx/armas/casing_drop_terra_03.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_light_01.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_light_02.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_light_03.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_light_04.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_medium_01.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_medium_02.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_medium_03.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_medium_04.ogg +0 -0
- package/assets/sfx/armas/charging_handle_pull_medium_05.ogg +0 -0
- package/assets/sfx/armas/charging_handle_release_medium_01.ogg +0 -0
- package/assets/sfx/armas/charging_handle_release_medium_02.ogg +0 -0
- package/assets/sfx/armas/charging_handle_release_medium_03.ogg +0 -0
- package/assets/sfx/armas/charging_handle_release_medium_04.ogg +0 -0
- package/assets/sfx/armas/charging_handle_release_medium_05.ogg +0 -0
- package/assets/sfx/armas/cylinder_close_light_01.ogg +0 -0
- package/assets/sfx/armas/cylinder_close_light_02.ogg +0 -0
- package/assets/sfx/armas/cylinder_close_light_03.ogg +0 -0
- package/assets/sfx/armas/cylinder_close_light_04.ogg +0 -0
- package/assets/sfx/armas/cylinder_close_light_05.ogg +0 -0
- package/assets/sfx/armas/cylinder_eject_light_01.ogg +0 -0
- package/assets/sfx/armas/cylinder_eject_light_02.ogg +0 -0
- package/assets/sfx/armas/cylinder_eject_light_03.ogg +0 -0
- package/assets/sfx/armas/cylinder_open_light_01.ogg +0 -0
- package/assets/sfx/armas/cylinder_open_light_02.ogg +0 -0
- package/assets/sfx/armas/cylinder_open_light_03.ogg +0 -0
- package/assets/sfx/armas/cylinder_open_light_04.ogg +0 -0
- package/assets/sfx/armas/cylinder_open_light_05.ogg +0 -0
- package/assets/sfx/armas/lever_close_medium_01.ogg +0 -0
- package/assets/sfx/armas/lever_close_medium_02.ogg +0 -0
- package/assets/sfx/armas/lever_open_medium_01.ogg +0 -0
- package/assets/sfx/armas/lever_open_medium_02.ogg +0 -0
- package/assets/sfx/armas/mag_out_heavy_01.ogg +0 -0
- package/assets/sfx/armas/mag_out_heavy_02.ogg +0 -0
- package/assets/sfx/armas/mag_out_light_01.ogg +0 -0
- package/assets/sfx/armas/mag_out_light_02.ogg +0 -0
- package/assets/sfx/armas/mag_out_light_03.ogg +0 -0
- package/assets/sfx/armas/mag_out_light_04.ogg +0 -0
- package/assets/sfx/armas/mag_out_medium_01.ogg +0 -0
- package/assets/sfx/armas/mag_out_medium_02.ogg +0 -0
- package/assets/sfx/armas/mag_out_medium_03.ogg +0 -0
- package/assets/sfx/armas/mag_seat_heavy_01.ogg +0 -0
- package/assets/sfx/armas/mag_seat_heavy_02.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_01.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_02.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_03.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_04.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_05.ogg +0 -0
- package/assets/sfx/armas/mag_seat_light_06.ogg +0 -0
- package/assets/sfx/armas/mag_seat_medium_01.ogg +0 -0
- package/assets/sfx/armas/mag_seat_medium_02.ogg +0 -0
- package/assets/sfx/armas/mag_seat_medium_03.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_close_01.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_close_02.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_far_01.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_far_02.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_far_03.ogg +0 -0
- package/assets/sfx/armas/pistol_22lr_far_04.ogg +0 -0
- package/assets/sfx/armas/pistol_45_close_01.ogg +0 -0
- package/assets/sfx/armas/pistol_45_close_02.ogg +0 -0
- package/assets/sfx/armas/pistol_45_close_03.ogg +0 -0
- package/assets/sfx/armas/pistol_45_close_04.ogg +0 -0
- package/assets/sfx/armas/pistol_45_close_05.ogg +0 -0
- package/assets/sfx/armas/pistol_45_far_01.ogg +0 -0
- package/assets/sfx/armas/pistol_45_far_02.ogg +0 -0
- package/assets/sfx/armas/pistol_45_far_03.ogg +0 -0
- package/assets/sfx/armas/pistol_45_far_04.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_01.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_02.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_03.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_04.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_05.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_close_06.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_far_01.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_far_02.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_far_03.ogg +0 -0
- package/assets/sfx/armas/pistol_9mm_far_04.ogg +0 -0
- package/assets/sfx/armas/pump_back_heavy_01.ogg +0 -0
- package/assets/sfx/armas/pump_back_heavy_02.ogg +0 -0
- package/assets/sfx/armas/pump_back_heavy_03.ogg +0 -0
- package/assets/sfx/armas/pump_back_heavy_04.ogg +0 -0
- package/assets/sfx/armas/pump_back_heavy_05.ogg +0 -0
- package/assets/sfx/armas/pump_forward_heavy_01.ogg +0 -0
- package/assets/sfx/armas/pump_forward_heavy_02.ogg +0 -0
- package/assets/sfx/armas/pump_forward_heavy_03.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_close_01.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_close_02.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_close_03.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_close_04.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_far_01.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_far_02.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_far_03.ogg +0 -0
- package/assets/sfx/armas/revolver_38_357_far_04.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_01.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_02.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_03.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_04.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_05.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_close_06.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_01.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_02.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_03.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_04.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_05.ogg +0 -0
- package/assets/sfx/armas/rifle_308_3006_far_06.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_close_01.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_close_02.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_close_03.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_close_04.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_close_05.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_far_01.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_far_02.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_far_03.ogg +0 -0
- package/assets/sfx/armas/rifle_5_56_far_04.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_01.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_02.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_03.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_04.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_05.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_06.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_07.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_08.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_09.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_10.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_11.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_close_12.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_far_01.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_far_02.ogg +0 -0
- package/assets/sfx/armas/rifle_7_62x39_far_03.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_01.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_02.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_03.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_04.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_05.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_06.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_07.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_08.ogg +0 -0
- package/assets/sfx/armas/shell_insert_heavy_09.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_01.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_02.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_03.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_04.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_05.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_close_06.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_01.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_02.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_03.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_04.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_05.ogg +0 -0
- package/assets/sfx/armas/shotgun_12_far_06.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_01.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_02.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_03.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_04.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_05.ogg +0 -0
- package/assets/sfx/armas/slide_release_light_06.ogg +0 -0
- package/assets/sfx/armas/slide_release_medium_01.ogg +0 -0
- package/assets/sfx/armas/slide_release_medium_02.ogg +0 -0
- package/assets/sfx/armas/slide_release_medium_03.ogg +0 -0
- package/assets/sfx/armas/slide_release_medium_04.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_01.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_02.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_03.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_04.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_05.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_06.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_close_07.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_01.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_02.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_03.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_04.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_05.ogg +0 -0
- package/assets/sfx/armas/sniper_338_300wm_far_06.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_01.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_02.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_03.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_04.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_05.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_06.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_07.ogg +0 -0
- package/assets/sfx/armas/speedloader_light_08.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_close_01.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_close_02.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_close_03.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_close_04.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_close_05.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_far_01.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_far_02.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_far_03.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_far_04.ogg +0 -0
- package/assets/sfx/armas/suppressed_pistol_far_05.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_close_01.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_close_02.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_close_03.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_close_04.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_far_01.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_far_02.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_far_03.ogg +0 -0
- package/assets/sfx/armas/suppressed_rifle_far_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_06.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_mixed_07.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_06.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_07.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_08.ogg +0 -0
- package/assets/sfx/explosoes-fogo/debris_rain_stone_09.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_big_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_big_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_big_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_big_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_big_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_car_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_car_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_car_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_car_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_far_close_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_far_close_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_far_close_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_far_close_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_flashbang_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_flashbang_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_grenade_06.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_06.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_07.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_08.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_09.ogg +0 -0
- package/assets/sfx/explosoes-fogo/explosion_small_bang_10.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_crackle_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_crackle_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_crackle_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_crackle_05.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_crackle_07.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_extinguish_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_extinguish_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_ignite_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_ignite_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_ignite_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_ignite_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_gas_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_gas_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_gas_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_gas_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_liquid_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_liquid_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_liquid_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_plastic_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_plastic_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_plastic_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_plastic_04.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_loop_small_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_pop_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_pop_02.ogg +0 -0
- package/assets/sfx/explosoes-fogo/fire_pop_03.ogg +0 -0
- package/assets/sfx/explosoes-fogo/flamethrower_loop_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/gas_leak_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/molotov_break_ignite_01.ogg +0 -0
- package/assets/sfx/explosoes-fogo/molotov_break_ignite_02.ogg +0 -0
- package/assets/sfx/impactos/ceramic_hit_close_01.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_01.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_02.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_03.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_04.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_05.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_06.ogg +0 -0
- package/assets/sfx/impactos/concrete_block_drag_close_07.ogg +0 -0
- package/assets/sfx/impactos/crack_close_01.ogg +0 -0
- package/assets/sfx/impactos/crack_close_02.ogg +0 -0
- package/assets/sfx/impactos/crack_close_03.ogg +0 -0
- package/assets/sfx/impactos/crack_close_04.ogg +0 -0
- package/assets/sfx/impactos/crack_close_05.ogg +0 -0
- package/assets/sfx/impactos/crack_close_06.ogg +0 -0
- package/assets/sfx/impactos/crack_close_07.ogg +0 -0
- package/assets/sfx/impactos/crack_close_08.ogg +0 -0
- package/assets/sfx/impactos/crack_close_09.ogg +0 -0
- package/assets/sfx/impactos/crack_close_10.ogg +0 -0
- package/assets/sfx/impactos/crumble_close_01.ogg +0 -0
- package/assets/sfx/impactos/crumble_close_02.ogg +0 -0
- package/assets/sfx/impactos/crumble_close_03.ogg +0 -0
- package/assets/sfx/impactos/crumble_close_04.ogg +0 -0
- package/assets/sfx/impactos/crumble_close_05.ogg +0 -0
- package/assets/sfx/impactos/flesh_fruit_splat_close_01.ogg +0 -0
- package/assets/sfx/impactos/flesh_fruit_splat_close_02.ogg +0 -0
- package/assets/sfx/impactos/flesh_fruit_splat_close_03.ogg +0 -0
- package/assets/sfx/impactos/furniture_push_wood_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_heavy_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_heavy_close_02.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_heavy_close_03.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_heavy_close_04.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_heavy_close_05.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_light_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_light_close_02.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_light_close_03.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_light_close_04.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_light_close_05.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_medium_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_medium_close_02.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_medium_close_03.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_medium_close_04.ogg +0 -0
- package/assets/sfx/impactos/glass_blunt_medium_close_05.ogg +0 -0
- package/assets/sfx/impactos/glass_bottle_break_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_debris_fall_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_debris_fall_close_02.ogg +0 -0
- package/assets/sfx/impactos/glass_debris_fall_close_03.ogg +0 -0
- package/assets/sfx/impactos/glass_debris_fall_close_04.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_01.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_02.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_03.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_04.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_05.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_06.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_07.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_08.ogg +0 -0
- package/assets/sfx/impactos/glass_shatter_close_09.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_21.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_22.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_23.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_24.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_25.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_26.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_27.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_28.ogg +0 -0
- package/assets/sfx/impactos/gravel_hit_close_29.ogg +0 -0
- package/assets/sfx/impactos/impact_generic_light_close_01.ogg +0 -0
- package/assets/sfx/impactos/impact_generic_light_close_02.ogg +0 -0
- package/assets/sfx/impactos/impact_generic_light_close_03.ogg +0 -0
- package/assets/sfx/impactos/impact_generic_light_close_04.ogg +0 -0
- package/assets/sfx/impactos/impact_generic_light_close_05.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_01.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_02.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_03.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_04.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_05.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_06.ogg +0 -0
- package/assets/sfx/impactos/masonry_blunt_close_07.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_01.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_02.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_03.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_04.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_05.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_21.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_22.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_23.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_24.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_25.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_26.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_27.ogg +0 -0
- package/assets/sfx/impactos/masonry_bullet_close_28.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_heavy_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_heavy_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_heavy_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_heavy_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_heavy_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_light_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_light_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_light_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_light_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_light_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_medium_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_medium_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_medium_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_medium_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_blunt_medium_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_06.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_21.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_22.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_23.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_24.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_25.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_26.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_27.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_28.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_29.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_30.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_31.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_32.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_33.ogg +0 -0
- package/assets/sfx/impactos/metal_bullet_close_34.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_06.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_07.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_08.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_09.ogg +0 -0
- package/assets/sfx/impactos/metal_fall_close_10.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_01.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_02.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_03.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_04.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_05.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_06.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_07.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_08.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_09.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_10.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_11.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_12.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_13.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_14.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_15.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_16.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_17.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_18.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_19.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_20.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_21.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_22.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_23.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_24.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_25.ogg +0 -0
- package/assets/sfx/impactos/metal_thin_close_26.ogg +0 -0
- package/assets/sfx/impactos/paper_tear_close_01.ogg +0 -0
- package/assets/sfx/impactos/paper_tear_close_02.ogg +0 -0
- package/assets/sfx/impactos/plastic_hit_close_01.ogg +0 -0
- package/assets/sfx/impactos/plastic_hit_close_02.ogg +0 -0
- package/assets/sfx/impactos/plastic_hit_close_03.ogg +0 -0
- package/assets/sfx/impactos/plastic_hit_close_04.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_21.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_22.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_23.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_24.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_25.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_26.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_27.ogg +0 -0
- package/assets/sfx/impactos/ricochet_metal_close_28.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_01.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_02.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_03.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_04.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_05.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_06.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_07.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_08.ogg +0 -0
- package/assets/sfx/impactos/rubble_falling_close_09.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_01.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_02.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_03.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_04.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_05.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_06.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_07.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_08.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_09.ogg +0 -0
- package/assets/sfx/impactos/soft_bullet_close_10.ogg +0 -0
- package/assets/sfx/impactos/soil_bullet_close_21.ogg +0 -0
- package/assets/sfx/impactos/soil_bullet_close_22.ogg +0 -0
- package/assets/sfx/impactos/soil_bullet_close_23.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_heavy_close_01.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_heavy_close_02.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_heavy_close_03.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_heavy_close_04.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_heavy_close_05.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_light_close_01.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_light_close_02.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_light_close_03.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_light_close_04.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_light_close_05.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_medium_close_01.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_medium_close_02.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_medium_close_03.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_medium_close_04.ogg +0 -0
- package/assets/sfx/impactos/wood_blunt_medium_close_05.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_01.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_02.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_03.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_04.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_05.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_06.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_07.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_08.ogg +0 -0
- package/assets/sfx/impactos/wood_fall_close_09.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_01.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_02.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_03.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_04.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_05.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_06.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_07.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_08.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_09.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_10.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_11.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_12.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_13.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_14.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_21.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_22.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_23.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_24.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_25.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_26.ogg +0 -0
- package/assets/sfx/impactos/wood_hit_close_27.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_01.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_02.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_03.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_04.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_05.ogg +0 -0
- package/assets/sfx/itens-portas/backpack_open_shut_06.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_01.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_02.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_03.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_04.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_05.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_06.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_07.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_08.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_09.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_10.ogg +0 -0
- package/assets/sfx/itens-portas/book_open_shut_11.ogg +0 -0
- package/assets/sfx/itens-portas/door_creak_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_creak_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_creak_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_creak_06.ogg +0 -0
- package/assets/sfx/itens-portas/door_creak_07.ogg +0 -0
- package/assets/sfx/itens-portas/door_glass_sliding_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_glass_sliding_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_glass_sliding_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_glass_sliding_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_knock_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_05.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_06.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_07.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_08.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_09.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_10.ogg +0 -0
- package/assets/sfx/itens-portas/door_lock_unlock_11.ogg +0 -0
- package/assets/sfx/itens-portas/door_locked_rattle_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_locked_rattle_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_locked_rattle_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_05.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_06.ogg +0 -0
- package/assets/sfx/itens-portas/door_metal_open_shut_07.ogg +0 -0
- package/assets/sfx/itens-portas/door_slam_wood_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_slam_wood_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_slam_wood_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_slam_wood_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_open_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_open_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_open_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_open_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_01.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_02.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_03.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_04.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_05.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_06.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_07.ogg +0 -0
- package/assets/sfx/itens-portas/door_wood_shut_08.ogg +0 -0
- package/assets/sfx/itens-portas/drawer_open_01.ogg +0 -0
- package/assets/sfx/itens-portas/drawer_open_02.ogg +0 -0
- package/assets/sfx/itens-portas/drawer_shut_01.ogg +0 -0
- package/assets/sfx/itens-portas/drawer_shut_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_bag_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_bag_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_glass_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_metal_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_metal_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_metal_03.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_metal_04.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_metal_05.ogg +0 -0
- package/assets/sfx/itens-portas/item_drop_paper_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_coins_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_coins_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_generic_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_generic_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_generic_03.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_generic_04.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_leather_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_leather_02.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_leather_03.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_leather_04.ogg +0 -0
- package/assets/sfx/itens-portas/item_pickup_leather_05.ogg +0 -0
- package/assets/sfx/itens-portas/item_setdown_01.ogg +0 -0
- package/assets/sfx/itens-portas/item_setdown_02.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_01.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_02.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_03.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_04.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_05.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_06.ogg +0 -0
- package/assets/sfx/itens-portas/keys_jingle_07.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_01.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_02.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_03.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_04.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_05.ogg +0 -0
- package/assets/sfx/itens-portas/lever_pull_06.ogg +0 -0
- package/assets/sfx/itens-portas/metal_latch_01.ogg +0 -0
- package/assets/sfx/itens-portas/metal_latch_02.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_01.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_02.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_03.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_04.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_05.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_06.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_07.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_08.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_09.ogg +0 -0
- package/assets/sfx/itens-portas/plates_hit_10.ogg +0 -0
- package/assets/sfx/itens-portas/plates_slide_01.ogg +0 -0
- package/assets/sfx/itens-portas/plates_slide_02.ogg +0 -0
- package/assets/sfx/itens-portas/plates_slide_03.ogg +0 -0
- package/assets/sfx/itens-portas/plates_slide_04.ogg +0 -0
- package/assets/sfx/itens-portas/saw_wood_01.ogg +0 -0
- package/assets/sfx/itens-portas/switch_click_01.ogg +0 -0
- package/assets/sfx/itens-portas/switch_click_02.ogg +0 -0
- package/assets/sfx/itens-portas/switch_click_03.ogg +0 -0
- package/assets/sfx/itens-portas/switch_click_04.ogg +0 -0
- package/assets/sfx/itens-portas/toilet_flush_01.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_01.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_02.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_03.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_04.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_05.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_06.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_07.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_08.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_09.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_10.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_11.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_12.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_13.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_14.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_15.ogg +0 -0
- package/assets/sfx/itens-portas/tools_handling_16.ogg +0 -0
- package/assets/sfx/itens-portas/water_drip_01.ogg +0 -0
- package/assets/sfx/itens-portas/water_drip_02.ogg +0 -0
- package/assets/sfx/itens-portas/water_drip_03.ogg +0 -0
- package/assets/sfx/itens-portas/water_drip_04.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_01.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_02.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_03.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_04.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_05.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_06.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_07.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_08.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_09.ogg +0 -0
- package/assets/sfx/itens-portas/water_faucet_10.ogg +0 -0
- package/assets/sfx/manifest.json +12093 -0
- package/assets/sfx/personagem/axe_chop_01.ogg +0 -0
- package/assets/sfx/personagem/axe_chop_02.ogg +0 -0
- package/assets/sfx/personagem/blunt_hit_metal_01.ogg +0 -0
- package/assets/sfx/personagem/blunt_hit_metal_02.ogg +0 -0
- package/assets/sfx/personagem/blunt_hit_metal_03.ogg +0 -0
- package/assets/sfx/personagem/blunt_hit_metal_04.ogg +0 -0
- package/assets/sfx/personagem/blunt_hit_metal_05.ogg +0 -0
- package/assets/sfx/personagem/body_fall_01.ogg +0 -0
- package/assets/sfx/personagem/body_fall_02.ogg +0 -0
- package/assets/sfx/personagem/body_fall_03.ogg +0 -0
- package/assets/sfx/personagem/body_fall_04.ogg +0 -0
- package/assets/sfx/personagem/body_fall_05.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_01.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_02.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_03.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_04.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_05.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_06.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_07.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_08.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_09.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_10.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_11.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_12.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_13.ogg +0 -0
- package/assets/sfx/personagem/cloth_rustle_14.ogg +0 -0
- package/assets/sfx/personagem/drink_gulp_01.ogg +0 -0
- package/assets/sfx/personagem/drink_gulp_02.ogg +0 -0
- package/assets/sfx/personagem/drink_gulp_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_carpet_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_carpet_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_carpet_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_carpet_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_carpet_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_concrete_walk_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_dirt_walk_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_06.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_07.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_08.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_09.ogg +0 -0
- package/assets/sfx/personagem/footstep_generic_alt_10.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt2_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt2_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt2_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt2_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_grass_walk_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_06.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_07.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_08.ogg +0 -0
- package/assets/sfx/personagem/footstep_hard_alt_09.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_metal_walk_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_snow_walk_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_alt_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_alt_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_alt_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_alt_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_alt_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_run_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_run_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_run_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_run_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_run_05.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_walk_01.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_walk_02.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_walk_03.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_walk_04.ogg +0 -0
- package/assets/sfx/personagem/footstep_wood_walk_05.ogg +0 -0
- package/assets/sfx/personagem/frying_pan_hit_01.ogg +0 -0
- package/assets/sfx/personagem/frying_pan_hit_02.ogg +0 -0
- package/assets/sfx/personagem/frying_pan_hit_03.ogg +0 -0
- package/assets/sfx/personagem/grab_01.ogg +0 -0
- package/assets/sfx/personagem/kick_hit_01.ogg +0 -0
- package/assets/sfx/personagem/kick_hit_02.ogg +0 -0
- package/assets/sfx/personagem/kick_hit_03.ogg +0 -0
- package/assets/sfx/personagem/kick_hit_04.ogg +0 -0
- package/assets/sfx/personagem/kick_hit_05.ogg +0 -0
- package/assets/sfx/personagem/knife_draw_01.ogg +0 -0
- package/assets/sfx/personagem/knife_draw_02.ogg +0 -0
- package/assets/sfx/personagem/knife_draw_03.ogg +0 -0
- package/assets/sfx/personagem/knife_scrape_01.ogg +0 -0
- package/assets/sfx/personagem/knife_slash_01.ogg +0 -0
- package/assets/sfx/personagem/knife_slash_02.ogg +0 -0
- package/assets/sfx/personagem/land_concrete_01.ogg +0 -0
- package/assets/sfx/personagem/land_concrete_02.ogg +0 -0
- package/assets/sfx/personagem/land_dirt_01.ogg +0 -0
- package/assets/sfx/personagem/land_dirt_02.ogg +0 -0
- package/assets/sfx/personagem/land_grass_01.ogg +0 -0
- package/assets/sfx/personagem/land_grass_02.ogg +0 -0
- package/assets/sfx/personagem/land_metal_01.ogg +0 -0
- package/assets/sfx/personagem/land_metal_02.ogg +0 -0
- package/assets/sfx/personagem/land_snow_01.ogg +0 -0
- package/assets/sfx/personagem/land_snow_02.ogg +0 -0
- package/assets/sfx/personagem/land_wood_01.ogg +0 -0
- package/assets/sfx/personagem/land_wood_02.ogg +0 -0
- package/assets/sfx/personagem/player_breath_run_01.ogg +0 -0
- package/assets/sfx/personagem/player_breath_run_02.ogg +0 -0
- package/assets/sfx/personagem/player_breath_run_03.ogg +0 -0
- package/assets/sfx/personagem/player_breath_run_04.ogg +0 -0
- package/assets/sfx/personagem/player_breath_run_05.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_01.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_02.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_03.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_04.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_05.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_06.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_07.ogg +0 -0
- package/assets/sfx/personagem/player_death_female_08.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_01.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_02.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_03.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_04.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_05.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_06.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_07.ogg +0 -0
- package/assets/sfx/personagem/player_death_male_08.ogg +0 -0
- package/assets/sfx/personagem/player_effort_01.ogg +0 -0
- package/assets/sfx/personagem/player_effort_02.ogg +0 -0
- package/assets/sfx/personagem/player_effort_03.ogg +0 -0
- package/assets/sfx/personagem/player_effort_04.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_01.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_02.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_03.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_04.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_05.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_female_06.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_01.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_02.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_03.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_04.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_05.ogg +0 -0
- package/assets/sfx/personagem/player_hurt_male_06.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_01.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_02.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_03.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_04.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_05.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_06.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_07.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_08.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_09.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_alt_10.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_01.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_02.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_03.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_04.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_05.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_body_06.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_face_01.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_face_02.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_face_03.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_face_04.ogg +0 -0
- package/assets/sfx/personagem/punch_hit_face_05.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_01.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_02.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_03.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_04.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_05.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_06.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_07.ogg +0 -0
- package/assets/sfx/personagem/punch_swing_08.ogg +0 -0
- package/assets/sfx/personagem/scream_01.ogg +0 -0
- package/assets/sfx/personagem/scream_02.ogg +0 -0
- package/assets/sfx/personagem/scream_03.ogg +0 -0
- package/assets/sfx/personagem/scream_04.ogg +0 -0
- package/assets/sfx/personagem/slap_01.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_01.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_02.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_03.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_04.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_05.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_06.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_07.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_08.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_09.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_10.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_11.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_12.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_13.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_14.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_15.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_16.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_17.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_18.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_19.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_20.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_21.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_22.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_23.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_24.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_25.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_26.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_27.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_28.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_29.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_30.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_31.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_32.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_33.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_34.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_35.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_36.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_37.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_38.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_39.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_40.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_41.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_42.ogg +0 -0
- package/assets/sfx/personagem/swing_whoosh_alt_43.ogg +0 -0
- package/assets/sfx/personagem/zombie_creature_01.ogg +0 -0
- package/assets/sfx/personagem/zombie_creature_02.ogg +0 -0
- package/assets/sfx/personagem/zombie_creature_03.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_01.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_02.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_03.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_04.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_05.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_06.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_07.ogg +0 -0
- package/assets/sfx/personagem/zombie_death_08.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_01.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_02.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_03.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_04.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_05.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_06.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_07.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_08.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_09.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_10.ogg +0 -0
- package/assets/sfx/personagem/zombie_groan_11.ogg +0 -0
- package/assets/sfx/veiculos/car_door_open_01.ogg +0 -0
- package/assets/sfx/veiculos/car_door_shut_01.ogg +0 -0
- package/assets/sfx/veiculos/car_gear_shift_01.ogg +0 -0
- package/assets/sfx/veiculos/car_trunk_shut_01.ogg +0 -0
- package/assets/sfx/veiculos/car_window_01.ogg +0 -0
- package/assets/sfx/veiculos/car_window_02.ogg +0 -0
- package/assets/sfx/veiculos/engine_loop_car_high_01.ogg +0 -0
- package/assets/sfx/veiculos/engine_loop_car_idle_01.ogg +0 -0
- package/assets/sfx/veiculos/engine_start_run_stop_01.ogg +0 -0
- package/assets/sfx/veiculos/engine_start_run_stop_02.ogg +0 -0
- package/dist/chunk-KBTUANNG.js +4429 -0
- package/dist/chunk-KBTUANNG.js.map +1 -0
- package/dist/index.d.ts +153 -3
- package/dist/index.global.js +3423 -1112
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +842 -6
- package/dist/index.js.map +1 -1
- package/dist/locale-CJfDpWnI.d.ts +1250 -0
- package/dist/testing.d.ts +84 -2
- package/dist/testing.js +11 -3
- package/package.json +10 -6
- package/dist/chunk-DZITVFMJ.js +0 -2235
- package/dist/chunk-DZITVFMJ.js.map +0 -1
- package/dist/testing-BZQid7h0.d.ts +0 -653
|
@@ -0,0 +1,4429 @@
|
|
|
1
|
+
// src/falloff.ts
|
|
2
|
+
var DISTANCE_CURVES = {
|
|
3
|
+
/** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
|
|
4
|
+
game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
|
|
5
|
+
/** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
|
|
6
|
+
game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
|
|
7
|
+
/** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
|
|
8
|
+
game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
|
|
9
|
+
/** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
|
|
10
|
+
game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
|
|
11
|
+
/** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
|
|
12
|
+
art_scene: { model: "inverse_square", k: 8e-3 },
|
|
13
|
+
/** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
|
|
14
|
+
animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
|
|
15
|
+
};
|
|
16
|
+
function falloffGain(falloff, volume, distance) {
|
|
17
|
+
if (!falloff || falloff.model === "none") return volume;
|
|
18
|
+
if (falloff.model === "linear") {
|
|
19
|
+
const level2 = Math.max(0, 1 - distance / falloff.range) * volume;
|
|
20
|
+
return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
|
|
21
|
+
}
|
|
22
|
+
const level = volume / (1 + distance * distance * falloff.k);
|
|
23
|
+
return falloff.scale === void 0 ? level : falloff.scale * level;
|
|
24
|
+
}
|
|
25
|
+
function isSilent(falloff, gain) {
|
|
26
|
+
if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
|
|
27
|
+
return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
|
|
28
|
+
}
|
|
29
|
+
function cutoffDivisor(falloff, distance) {
|
|
30
|
+
if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
|
|
31
|
+
return 1 + distance * falloff.cutoffK;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/data/game-sfx.json
|
|
35
|
+
var game_sfx_default = {
|
|
36
|
+
sources: ["jogo:L62089-62107"],
|
|
37
|
+
note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
|
|
38
|
+
classes: [
|
|
39
|
+
{ id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
|
|
40
|
+
{ id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
|
|
41
|
+
{ id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
|
|
42
|
+
{ id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
|
|
43
|
+
{ id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
|
|
44
|
+
{ id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
|
|
45
|
+
{ id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
|
|
46
|
+
{ id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
|
|
47
|
+
{ id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
|
|
48
|
+
{ id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
|
|
49
|
+
{ id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
|
|
50
|
+
{ id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
|
|
51
|
+
],
|
|
52
|
+
fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/recipes/noise-class.ts
|
|
56
|
+
var GAME_SFX = game_sfx_default;
|
|
57
|
+
var GAME_SFX_CLASSES = GAME_SFX.classes;
|
|
58
|
+
var GAME_SFX_FALLBACK = GAME_SFX.fallback;
|
|
59
|
+
function noiseClassRecipe(row, id = `sfx_${row.id}`) {
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
bus: row.bus,
|
|
63
|
+
order: "burst",
|
|
64
|
+
duration: row.seconds,
|
|
65
|
+
tail: 0.05,
|
|
66
|
+
layers: [
|
|
67
|
+
{
|
|
68
|
+
source: { kind: "noise", offset: "random" },
|
|
69
|
+
filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
|
|
70
|
+
envelope: [
|
|
71
|
+
[0, row.gain, "set"],
|
|
72
|
+
["1d", 1e-3, "exp", "fixed"]
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
falloff: DISTANCE_CURVES.game_sfx,
|
|
77
|
+
sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
|
|
81
|
+
|
|
82
|
+
// src/library/crowd_rumble.json
|
|
83
|
+
var crowd_rumble_default = {
|
|
84
|
+
id: "crowd_rumble",
|
|
85
|
+
bus: "ambience",
|
|
86
|
+
order: "game_loop",
|
|
87
|
+
source: { kind: "noise", loop: true },
|
|
88
|
+
filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 160 } }] },
|
|
89
|
+
smoothing: { volume: 0.15, frequency: 0.08 },
|
|
90
|
+
startAbove: 2e-3,
|
|
91
|
+
sources: ["jogo:L62116 horda: noise through a 160 Hz lowpass", "jogo:L70424 level min(0.16, near \xD7 0.0016)", "temporary home until the crowd package (Q12)"]
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/library/engine_saw.json
|
|
95
|
+
var engine_saw_default = {
|
|
96
|
+
id: "engine_saw",
|
|
97
|
+
bus: "world",
|
|
98
|
+
order: "game_loop",
|
|
99
|
+
source: { kind: "osc", wave: "sawtooth", loop: true },
|
|
100
|
+
filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 400 } }] },
|
|
101
|
+
smoothing: { volume: 0.15, frequency: 0.08 },
|
|
102
|
+
startAbove: 2e-3,
|
|
103
|
+
sources: [
|
|
104
|
+
"jogo:L62115 motor: sawtooth through a 400 Hz lowpass",
|
|
105
|
+
"jogo:L70421 level 0.035 + speed \xD7 0.003, frequency 38 + speed \xD7 7 (the host drives `set`)",
|
|
106
|
+
"temporary home until the vehicles package (Q12)"
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/library/horn.json
|
|
111
|
+
var horn_default = {
|
|
112
|
+
id: "horn",
|
|
113
|
+
bus: "world",
|
|
114
|
+
order: "chord",
|
|
115
|
+
duration: 0.6,
|
|
116
|
+
tail: 0.02,
|
|
117
|
+
layers: [
|
|
118
|
+
{ source: { kind: "osc", wave: "square", frequency: { set: 415 } }, envelope: [[0, 0.08, "set"], [0.6, 1e-4, "set", "fixed"]] },
|
|
119
|
+
{ source: { kind: "osc", wave: "square", frequency: { set: 523 } }, envelope: [[0, 0.08, "set"], [0.6, 1e-4, "set", "fixed"]] }
|
|
120
|
+
],
|
|
121
|
+
falloff: { model: "none" },
|
|
122
|
+
sources: ["jogo:L70272-70276 buzina: squares at 415 and 523 Hz, 0.08 for 0.6 s", "temporary home until the vehicles package (Q12)"]
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/library/radio_static.json
|
|
126
|
+
var radio_static_default = {
|
|
127
|
+
id: "radio_static",
|
|
128
|
+
bus: "world",
|
|
129
|
+
order: "static",
|
|
130
|
+
duration: 0.6,
|
|
131
|
+
layers: [
|
|
132
|
+
{
|
|
133
|
+
source: { kind: "noise", offset: "random" },
|
|
134
|
+
filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 0.8 }] },
|
|
135
|
+
envelope: [[0, 0.06, "set"], ["1d", 1e-3, "exp", "fixed"]]
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
falloff: { model: "none" },
|
|
139
|
+
sources: ["jogo:L69998-70004 chiado: noise through a 1800 Hz bandpass (Q 0.8), 0.06 falling over 0.6 s", "temporary home until the props or radio package (Q12)"]
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/library/rotor_blades.json
|
|
143
|
+
var rotor_blades_default = {
|
|
144
|
+
id: "rotor_blades",
|
|
145
|
+
bus: "world",
|
|
146
|
+
order: "game_loop",
|
|
147
|
+
source: { kind: "noise", loop: true },
|
|
148
|
+
filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 300 } }] },
|
|
149
|
+
modulators: [{ target: "gain", rate: 11, depth: 0.5, base: 0.5 }],
|
|
150
|
+
smoothing: { volume: 0.15, frequency: 0.08 },
|
|
151
|
+
startAbove: 2e-3,
|
|
152
|
+
sources: [
|
|
153
|
+
"jogo:L62116-62120 rotor: noise through a 300 Hz lowpass, blade beat as an 11 Hz LFO of 0.5 on a 0.5 gain stage",
|
|
154
|
+
"jogo:L70426 level min(0.5, 30 / (distance + 10))",
|
|
155
|
+
"temporary home until the vehicles package (Q12)"
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/library/siren.json
|
|
160
|
+
var siren_default = {
|
|
161
|
+
id: "siren",
|
|
162
|
+
bus: "world",
|
|
163
|
+
order: "siren",
|
|
164
|
+
duration: 9,
|
|
165
|
+
tail: 0.2,
|
|
166
|
+
layers: [
|
|
167
|
+
{
|
|
168
|
+
wet: 1.5,
|
|
169
|
+
source: { kind: "osc", wave: "sine", frequency: { set: 850 } },
|
|
170
|
+
modulators: [{ target: "frequency", rate: 0.18, depth: 280 }],
|
|
171
|
+
filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 1400 } }] },
|
|
172
|
+
envelope: [[0, 1e-4, "set", "fixed"], [2, 0.035, "exp"], [6, 0.035, "set"], [9, 1e-4, "exp", "fixed"]]
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
falloff: { model: "none" },
|
|
176
|
+
sources: ["cena:L1369 siren: 850 Hz sine swept \xB1280 Hz at 0.18 Hz, 1400 Hz lowpass, 0.035 for 9 s, reverb send 1.5", "temporary home until the vehicles package (Q12)"]
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/library/index.ts
|
|
180
|
+
var LIBRARY_SOUNDS = [horn_default, radio_static_default, siren_default];
|
|
181
|
+
var LIBRARY_LOOPS = [engine_saw_default, rotor_blades_default, crowd_rumble_default];
|
|
182
|
+
var LIBRARY_BANK = { id: "library", sounds: LIBRARY_SOUNDS, loops: LIBRARY_LOOPS };
|
|
183
|
+
|
|
184
|
+
// src/data/mix-profiles.json
|
|
185
|
+
var mix_profiles_default = [
|
|
186
|
+
{
|
|
187
|
+
id: "game",
|
|
188
|
+
master: 0.5,
|
|
189
|
+
noise: {
|
|
190
|
+
seconds: 1.5,
|
|
191
|
+
create: "lazy"
|
|
192
|
+
},
|
|
193
|
+
sources: [
|
|
194
|
+
"jogo:L62077-62081 master 0.5 to the output",
|
|
195
|
+
"jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "art_scene",
|
|
200
|
+
master: 0.85,
|
|
201
|
+
compressor: {
|
|
202
|
+
threshold: -12,
|
|
203
|
+
ratio: 5
|
|
204
|
+
},
|
|
205
|
+
reverb: {
|
|
206
|
+
seconds: 2.2,
|
|
207
|
+
decay: 3.4,
|
|
208
|
+
wet: 0.4
|
|
209
|
+
},
|
|
210
|
+
noise: {
|
|
211
|
+
seconds: 2,
|
|
212
|
+
create: "eager"
|
|
213
|
+
},
|
|
214
|
+
sources: [
|
|
215
|
+
"cena:L1308 compressor -12 dB, ratio 5",
|
|
216
|
+
"cena:L1309 master 0.85",
|
|
217
|
+
"cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
|
|
218
|
+
"cena:L1311 wet 0.4",
|
|
219
|
+
"cena:L1312 noise 2 s made with the chain"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "kit",
|
|
224
|
+
master: 0.85,
|
|
225
|
+
compressor: {
|
|
226
|
+
threshold: -12,
|
|
227
|
+
ratio: 5
|
|
228
|
+
},
|
|
229
|
+
reverb: {
|
|
230
|
+
seconds: 2.2,
|
|
231
|
+
decay: 3.4,
|
|
232
|
+
wet: 0.4
|
|
233
|
+
},
|
|
234
|
+
noise: {
|
|
235
|
+
seconds: 2,
|
|
236
|
+
create: "eager"
|
|
237
|
+
},
|
|
238
|
+
sources: [
|
|
239
|
+
"sdk: the art scene chain (cena L1308-L1312) with a limiter after the compressor: the page has none, and a blast with its debris and a fire catching reached +7 dBFS"
|
|
240
|
+
],
|
|
241
|
+
limiter: {
|
|
242
|
+
ceiling: -1,
|
|
243
|
+
knee: -6
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
// src/mix.ts
|
|
249
|
+
var MIX_PROFILES = Object.fromEntries(
|
|
250
|
+
mix_profiles_default.map((profile) => [profile.id, profile])
|
|
251
|
+
);
|
|
252
|
+
var DEFAULT_MIX = "kit";
|
|
253
|
+
function resolveMix(mix, profiles = MIX_PROFILES) {
|
|
254
|
+
if (typeof mix !== "string") return mix;
|
|
255
|
+
const profile = profiles[mix];
|
|
256
|
+
if (!profile) throw new Error(`game-audio: unknown mix profile "${mix}" (known: ${Object.keys(profiles).join(", ")})`);
|
|
257
|
+
return profile;
|
|
258
|
+
}
|
|
259
|
+
function busGain(setting) {
|
|
260
|
+
if (!setting) return 1;
|
|
261
|
+
return setting.muted ? 0 : setting.gain ?? 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/recipe.ts
|
|
265
|
+
var BUS_IDS = ["weapons", "voices", "world", "ambience", "weather", "ui"];
|
|
266
|
+
var BUILD_ORDERS = ["layered", "burst", "scream", "groan", "boom", "static", "chord", "calls", "siren", "weapon"];
|
|
267
|
+
|
|
268
|
+
// src/data/voice-groups.json
|
|
269
|
+
var voice_groups_default = {
|
|
270
|
+
undead_voice: {
|
|
271
|
+
maxConcurrent: 5,
|
|
272
|
+
cooldown: 0.25,
|
|
273
|
+
maxDistance: 40,
|
|
274
|
+
sources: [
|
|
275
|
+
"cena:L1342 `AU.groans > 4` refuses, so five play at once",
|
|
276
|
+
"jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
|
|
277
|
+
"jogo:L70472 the budget counts down by the frame time"
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
fire_ignite: {
|
|
281
|
+
maxConcurrent: 2,
|
|
282
|
+
cooldown: 0.15,
|
|
283
|
+
sources: [
|
|
284
|
+
"sdk: many parts catching at once (a blast sets a shelf alight) are one or two ignitions, not forty"
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
fire_crackle: {
|
|
288
|
+
maxConcurrent: 4,
|
|
289
|
+
sources: [
|
|
290
|
+
"sdk: the crackles of every fire share four voices"
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/voices.ts
|
|
296
|
+
var VOICE_GROUPS = voice_groups_default;
|
|
297
|
+
var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
|
|
298
|
+
var DEFAULT_MAX_VOICES = 32;
|
|
299
|
+
function busPriority(bus) {
|
|
300
|
+
return BUS_PRIORITY[bus];
|
|
301
|
+
}
|
|
302
|
+
function createVoiceManager(options = {}) {
|
|
303
|
+
const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
|
|
304
|
+
const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
|
|
305
|
+
const budgets = /* @__PURE__ */ new Map();
|
|
306
|
+
let voices = [];
|
|
307
|
+
let nextId = 1;
|
|
308
|
+
let peak = 0;
|
|
309
|
+
let dropped = 0;
|
|
310
|
+
const refuse = (reason) => {
|
|
311
|
+
dropped += 1;
|
|
312
|
+
return { accepted: false, reason };
|
|
313
|
+
};
|
|
314
|
+
const groupCheck = (request, evicted) => {
|
|
315
|
+
const group = request.group === void 0 ? void 0 : groups.get(request.group);
|
|
316
|
+
if (!group) return void 0;
|
|
317
|
+
if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
|
|
318
|
+
if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
|
|
319
|
+
const members = voices.filter((voice) => voice.group === request.group);
|
|
320
|
+
if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
|
|
321
|
+
if (!group.steal) return "group_full";
|
|
322
|
+
evicted.push(members[0].id);
|
|
323
|
+
return void 0;
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
request(request) {
|
|
327
|
+
const evicted = [];
|
|
328
|
+
const refusal = groupCheck(request, evicted);
|
|
329
|
+
if (refusal) return refuse(refusal);
|
|
330
|
+
const score = request.priority * request.gain;
|
|
331
|
+
const remaining = voices.filter((voice) => !evicted.includes(voice.id));
|
|
332
|
+
if (remaining.length >= maxVoices) {
|
|
333
|
+
const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
|
|
334
|
+
if (score <= lowest.score) return refuse("global_full");
|
|
335
|
+
evicted.push(lowest.id);
|
|
336
|
+
dropped += 1;
|
|
337
|
+
}
|
|
338
|
+
const group = request.group === void 0 ? void 0 : groups.get(request.group);
|
|
339
|
+
if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
|
|
340
|
+
const id = nextId++;
|
|
341
|
+
voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
|
|
342
|
+
peak = Math.max(peak, voices.length);
|
|
343
|
+
return { accepted: true, id, evicted };
|
|
344
|
+
},
|
|
345
|
+
release(id) {
|
|
346
|
+
voices = voices.filter((voice) => voice.id !== id);
|
|
347
|
+
},
|
|
348
|
+
setLength(id, seconds) {
|
|
349
|
+
const voice = voices.find((entry) => entry.id === id);
|
|
350
|
+
if (voice) voice.remaining = seconds;
|
|
351
|
+
},
|
|
352
|
+
advance(dt) {
|
|
353
|
+
for (const [group, budget] of budgets) budgets.set(group, budget - dt);
|
|
354
|
+
const ended = [];
|
|
355
|
+
for (const voice of voices) {
|
|
356
|
+
voice.remaining -= dt;
|
|
357
|
+
if (voice.remaining <= 0) ended.push(voice.id);
|
|
358
|
+
}
|
|
359
|
+
if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
|
|
360
|
+
return ended;
|
|
361
|
+
},
|
|
362
|
+
setGroup(id, group) {
|
|
363
|
+
if (group) groups.set(id, group);
|
|
364
|
+
else groups.delete(id);
|
|
365
|
+
},
|
|
366
|
+
stats: () => ({ active: voices.length, peak, dropped, maxVoices })
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/bank.ts
|
|
371
|
+
function isPlainObject(value) {
|
|
372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
373
|
+
}
|
|
374
|
+
function mergeRecords(base, patch) {
|
|
375
|
+
const merged = { ...base };
|
|
376
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
377
|
+
if (value === void 0) continue;
|
|
378
|
+
const current = merged[key];
|
|
379
|
+
merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeRecords(current, value) : value;
|
|
380
|
+
}
|
|
381
|
+
return merged;
|
|
382
|
+
}
|
|
383
|
+
function createSoundRegistry(banks = []) {
|
|
384
|
+
const sounds = /* @__PURE__ */ new Map();
|
|
385
|
+
const loops = /* @__PURE__ */ new Map();
|
|
386
|
+
const sets = /* @__PURE__ */ new Map();
|
|
387
|
+
const samples = /* @__PURE__ */ new Map();
|
|
388
|
+
const owners = /* @__PURE__ */ new Map();
|
|
389
|
+
const registry = {
|
|
390
|
+
register(bank) {
|
|
391
|
+
for (const sound of bank.sounds ?? []) {
|
|
392
|
+
sounds.set(sound.id, sound);
|
|
393
|
+
owners.set(sound.id, bank.id);
|
|
394
|
+
}
|
|
395
|
+
for (const loop of bank.loops ?? []) {
|
|
396
|
+
loops.set(loop.id, loop);
|
|
397
|
+
owners.set(loop.id, bank.id);
|
|
398
|
+
}
|
|
399
|
+
for (const set of bank.sets ?? []) {
|
|
400
|
+
if (set.variants.length === 0) throw new Error(`sound set ${set.id} has no variant`);
|
|
401
|
+
sets.set(set.id, set);
|
|
402
|
+
owners.set(set.id, bank.id);
|
|
403
|
+
}
|
|
404
|
+
for (const sample of bank.samples ?? []) {
|
|
405
|
+
samples.set(sample.id, sample);
|
|
406
|
+
owners.set(sample.id, bank.id);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
patch(id, patch) {
|
|
410
|
+
const sound = sounds.get(id);
|
|
411
|
+
if (sound) sounds.set(id, mergeRecords(sound, patch));
|
|
412
|
+
const loop = loops.get(id);
|
|
413
|
+
if (loop) loops.set(id, mergeRecords(loop, patch));
|
|
414
|
+
const sample = samples.get(id);
|
|
415
|
+
if (sample) samples.set(id, mergeRecords(sample, patch));
|
|
416
|
+
},
|
|
417
|
+
remove(id) {
|
|
418
|
+
sounds.delete(id);
|
|
419
|
+
loops.delete(id);
|
|
420
|
+
sets.delete(id);
|
|
421
|
+
samples.delete(id);
|
|
422
|
+
owners.delete(id);
|
|
423
|
+
},
|
|
424
|
+
sound: (id) => sounds.get(id),
|
|
425
|
+
loop: (id) => loops.get(id),
|
|
426
|
+
set: (id) => sets.get(id),
|
|
427
|
+
sample: (id) => samples.get(id),
|
|
428
|
+
soundIds: () => [...sounds.keys()],
|
|
429
|
+
setIds: () => [...sets.keys()],
|
|
430
|
+
loopIds: () => [...loops.keys()],
|
|
431
|
+
sampleIds: () => [...samples.keys()],
|
|
432
|
+
bankOf: (id) => owners.get(id)
|
|
433
|
+
};
|
|
434
|
+
for (const bank of banks) registry.register(bank);
|
|
435
|
+
return registry;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/listener.ts
|
|
439
|
+
var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
|
|
440
|
+
var PAN_LIMIT = 0.85;
|
|
441
|
+
var PAN_MIN_DISTANCE = 2;
|
|
442
|
+
function listenerDistance(listener, position, mode = "xz") {
|
|
443
|
+
const dx = position[0] - listener.position[0];
|
|
444
|
+
const dz = position[2] - listener.position[2];
|
|
445
|
+
if (mode === "xz") return Math.hypot(dx, dz);
|
|
446
|
+
return Math.hypot(dx, position[1] - listener.position[1], dz);
|
|
447
|
+
}
|
|
448
|
+
function panFor(listener, position) {
|
|
449
|
+
const rightX = Math.cos(listener.yaw);
|
|
450
|
+
const rightZ = -Math.sin(listener.yaw);
|
|
451
|
+
const dx = position[0] - listener.position[0];
|
|
452
|
+
const dz = position[2] - listener.position[2];
|
|
453
|
+
const distance = Math.hypot(dx, dz) || 1;
|
|
454
|
+
const pan = (dx * rightX + dz * rightZ) / Math.max(distance, PAN_MIN_DISTANCE);
|
|
455
|
+
return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/rng.ts
|
|
459
|
+
var FNV_OFFSET = 2166136261;
|
|
460
|
+
var FNV_PRIME = 16777619;
|
|
461
|
+
var MIX = 2654435761;
|
|
462
|
+
function createRng(seed) {
|
|
463
|
+
let state = seed >>> 0;
|
|
464
|
+
return () => {
|
|
465
|
+
state = state + 1831565813 >>> 0;
|
|
466
|
+
let mixed = Math.imul(state ^ state >>> 15, state | 1);
|
|
467
|
+
mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
|
|
468
|
+
return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function streamSeed(seed, name) {
|
|
472
|
+
const label = String(name);
|
|
473
|
+
let hash = FNV_OFFSET;
|
|
474
|
+
for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
|
|
475
|
+
return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
|
|
476
|
+
}
|
|
477
|
+
function streamFor(seed, name) {
|
|
478
|
+
return createRng(streamSeed(seed, name));
|
|
479
|
+
}
|
|
480
|
+
function drawBetween(random, min, max) {
|
|
481
|
+
return min + random() * (max - min);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/plan.ts
|
|
485
|
+
var DEFAULT_TAIL = 0.05;
|
|
486
|
+
var DEFAULT_NOISE_SECONDS = 1.5;
|
|
487
|
+
var OCCLUSION_GAIN = 0.5;
|
|
488
|
+
var OCCLUSION_CUTOFF = 0.6;
|
|
489
|
+
function isParamRef(range) {
|
|
490
|
+
return typeof range === "object" && !Array.isArray(range);
|
|
491
|
+
}
|
|
492
|
+
function draw(range, scope) {
|
|
493
|
+
if (typeof range === "number") return range;
|
|
494
|
+
if (isParamRef(range)) {
|
|
495
|
+
const value = scope.params[range.param];
|
|
496
|
+
if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range.param}", which was not given`);
|
|
497
|
+
return value * (range.scale ?? 1) + (range.add ?? 0);
|
|
498
|
+
}
|
|
499
|
+
const [min, max] = range;
|
|
500
|
+
return min + scope.random() * (max - min);
|
|
501
|
+
}
|
|
502
|
+
function drawTime(time, scope) {
|
|
503
|
+
if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
|
|
504
|
+
return draw(time, scope);
|
|
505
|
+
}
|
|
506
|
+
function timeOf(time, duration) {
|
|
507
|
+
if (typeof time === "number") return time;
|
|
508
|
+
if (time === "duration") return duration;
|
|
509
|
+
return Number(time.slice(0, -1)) * duration;
|
|
510
|
+
}
|
|
511
|
+
function drawAutomation(automation, scope) {
|
|
512
|
+
const pending = {};
|
|
513
|
+
for (const key of Object.keys(automation)) {
|
|
514
|
+
if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope);
|
|
515
|
+
else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope);
|
|
516
|
+
else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope);
|
|
517
|
+
else if (key === "curve") pending.curve = automation.curve;
|
|
518
|
+
else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope);
|
|
519
|
+
}
|
|
520
|
+
return pending;
|
|
521
|
+
}
|
|
522
|
+
function finishAutomation(pending, duration) {
|
|
523
|
+
if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
|
|
524
|
+
if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
|
|
525
|
+
if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
|
|
526
|
+
return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
|
|
527
|
+
}
|
|
528
|
+
var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
|
|
529
|
+
function drawFilter(spec, scope) {
|
|
530
|
+
const pending = { spec, frequency: {} };
|
|
531
|
+
for (const key of Object.keys(spec)) {
|
|
532
|
+
if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope);
|
|
533
|
+
else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope);
|
|
534
|
+
else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope);
|
|
535
|
+
else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope);
|
|
536
|
+
}
|
|
537
|
+
return pending;
|
|
538
|
+
}
|
|
539
|
+
function drawModulator(modulator, scope) {
|
|
540
|
+
const pending = { modulator, rate: 0 };
|
|
541
|
+
for (const key of Object.keys(modulator)) {
|
|
542
|
+
if (key === "rate") pending.rate = draw(modulator.rate, scope);
|
|
543
|
+
else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope);
|
|
544
|
+
}
|
|
545
|
+
return pending;
|
|
546
|
+
}
|
|
547
|
+
function drawVoice(source, scope) {
|
|
548
|
+
const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
|
|
549
|
+
for (const key of Object.keys(source)) {
|
|
550
|
+
if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope);
|
|
551
|
+
else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope);
|
|
552
|
+
else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope), q: formant.q }));
|
|
553
|
+
else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope), q: source.breath.q, gain: draw(source.breath.gain, scope) };
|
|
554
|
+
}
|
|
555
|
+
return pending;
|
|
556
|
+
}
|
|
557
|
+
function drawLayer(layer, scope) {
|
|
558
|
+
const pending = { at: 0, gain: 1, filters: [], modulators: [] };
|
|
559
|
+
for (const key of Object.keys(layer)) {
|
|
560
|
+
if (key === "at" && layer.at !== void 0) pending.at = draw(layer.at, scope);
|
|
561
|
+
else if (key === "duration" && layer.duration !== void 0) pending.duration = draw(layer.duration, scope);
|
|
562
|
+
else if (key === "gain" && layer.gain !== void 0) pending.gain = draw(layer.gain, scope);
|
|
563
|
+
else if (key === "source" && layer.source.kind === "osc") pending.automation = drawAutomation(layer.source.frequency, scope);
|
|
564
|
+
else if (key === "source" && layer.source.kind === "formant_voice") pending.voice = drawVoice(layer.source, scope);
|
|
565
|
+
else if (key === "filters" && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope));
|
|
566
|
+
else if (key === "modulators" && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope));
|
|
567
|
+
}
|
|
568
|
+
return pending;
|
|
569
|
+
}
|
|
570
|
+
function drawOffset(source, duration, scope, noiseSeconds) {
|
|
571
|
+
if (source.kind === "formant_voice") return scope.random();
|
|
572
|
+
if (source.kind !== "noise" || source.offset === void 0) return 0;
|
|
573
|
+
if (typeof source.offset === "number") return source.offset;
|
|
574
|
+
if (source.offset === "fit") return scope.random() * Math.max(0, noiseSeconds - duration);
|
|
575
|
+
return scope.random() * (source.offsetScale ?? 1);
|
|
576
|
+
}
|
|
577
|
+
function finishSource(layer, pending, duration, offset) {
|
|
578
|
+
const source = layer.source;
|
|
579
|
+
if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
|
|
580
|
+
if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
|
|
581
|
+
const voice = pending.voice;
|
|
582
|
+
return {
|
|
583
|
+
kind: "formant_voice",
|
|
584
|
+
wave: source.wave,
|
|
585
|
+
frequency: finishAutomation(voice.frequency, duration),
|
|
586
|
+
vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
|
|
587
|
+
formants: voice.formants,
|
|
588
|
+
breath: { ...voice.breath, offset },
|
|
589
|
+
mix: source.mix
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function finishModulator(pending, sourceFrequency) {
|
|
593
|
+
const { modulator } = pending;
|
|
594
|
+
const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
|
|
595
|
+
return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
|
|
596
|
+
}
|
|
597
|
+
function finishEnvelope(envelope, duration, level) {
|
|
598
|
+
return envelope.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
|
|
599
|
+
}
|
|
600
|
+
function planLayer(layer, scope, defaults, levelInEnvelope, soundLevel) {
|
|
601
|
+
const pending = drawLayer(layer, scope);
|
|
602
|
+
const duration = pending.duration ?? defaults.duration;
|
|
603
|
+
if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
|
|
604
|
+
const offset = drawOffset(layer.source, duration, scope, defaults.noiseSeconds);
|
|
605
|
+
const level = pending.gain * soundLevel;
|
|
606
|
+
const sourceFrequency = layer.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
|
|
607
|
+
const planned = {
|
|
608
|
+
at: pending.at,
|
|
609
|
+
duration,
|
|
610
|
+
tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,
|
|
611
|
+
level,
|
|
612
|
+
...layer.wet === void 0 ? {} : { wet: layer.wet },
|
|
613
|
+
source: finishSource(layer, pending, duration, offset),
|
|
614
|
+
...layer.filters ? {
|
|
615
|
+
filters: {
|
|
616
|
+
mode: layer.filters.mode,
|
|
617
|
+
bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
|
|
618
|
+
type: spec.type,
|
|
619
|
+
frequency: finishAutomation(frequency, duration),
|
|
620
|
+
...q === void 0 ? {} : { q },
|
|
621
|
+
...gain === void 0 ? {} : { gain },
|
|
622
|
+
...filterLevel === void 0 ? {} : { level: filterLevel }
|
|
623
|
+
}))
|
|
624
|
+
}
|
|
625
|
+
} : {},
|
|
626
|
+
modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
|
|
627
|
+
envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1)
|
|
628
|
+
};
|
|
629
|
+
if (!layer.repeat) return [planned];
|
|
630
|
+
return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }));
|
|
631
|
+
}
|
|
632
|
+
function scaleAutomation(automation, divisor) {
|
|
633
|
+
const scale = (value) => value === void 0 ? void 0 : value / divisor;
|
|
634
|
+
const scaled = { ...automation };
|
|
635
|
+
for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled[key] = scale(automation[key]);
|
|
636
|
+
return scaled;
|
|
637
|
+
}
|
|
638
|
+
function muffle(layer, divisor) {
|
|
639
|
+
if (divisor === 1 || !layer.filters) return layer;
|
|
640
|
+
const bank = layer.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
|
|
641
|
+
return { ...layer, filters: { ...layer.filters, bank } };
|
|
642
|
+
}
|
|
643
|
+
var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
|
|
644
|
+
function planSound(recipe, input = {}) {
|
|
645
|
+
const listener = input.listener ?? DEFAULT_LISTENER;
|
|
646
|
+
const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
|
|
647
|
+
const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
|
|
648
|
+
const order = recipe.order ?? "layered";
|
|
649
|
+
const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance);
|
|
650
|
+
const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
|
|
651
|
+
const silent = isSilent(recipe.falloff, falloffLevel);
|
|
652
|
+
const base = {
|
|
653
|
+
id: recipe.id,
|
|
654
|
+
bus: recipe.bus,
|
|
655
|
+
order,
|
|
656
|
+
...recipe.group === void 0 ? {} : { group: recipe.group },
|
|
657
|
+
priority: recipe.priority ?? busPriority(recipe.bus),
|
|
658
|
+
level,
|
|
659
|
+
distance,
|
|
660
|
+
...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
|
|
661
|
+
};
|
|
662
|
+
if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
|
|
663
|
+
const scope = { random: input.random ?? createRng(1), params: { ...input.params } };
|
|
664
|
+
for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range, scope);
|
|
665
|
+
const defaults = {
|
|
666
|
+
...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope) },
|
|
667
|
+
...recipe.tail === void 0 ? {} : { tail: recipe.tail },
|
|
668
|
+
noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
|
|
669
|
+
};
|
|
670
|
+
const divisor = cutoffDivisor(recipe.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion);
|
|
671
|
+
const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
|
|
672
|
+
return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/webaudio/noise.ts
|
|
676
|
+
function fillNoise(random, length) {
|
|
677
|
+
const samples = new Float32Array(length);
|
|
678
|
+
for (let index = 0; index < length; index++) samples[index] = random() * 2 - 1;
|
|
679
|
+
return samples;
|
|
680
|
+
}
|
|
681
|
+
function fillImpulse(random, options) {
|
|
682
|
+
const size = Math.floor(options.length);
|
|
683
|
+
return Array.from({ length: options.channels }, () => {
|
|
684
|
+
const samples = new Float32Array(size);
|
|
685
|
+
for (let index = 0; index < options.length; index++) {
|
|
686
|
+
const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
|
|
687
|
+
if (index < size) samples[index] = value;
|
|
688
|
+
}
|
|
689
|
+
return samples;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function bufferOf(context, channels, requestedLength) {
|
|
693
|
+
const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
|
|
694
|
+
channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
|
|
695
|
+
return buffer;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/webaudio/chain.ts
|
|
699
|
+
var LIMITER_POINTS = 4097;
|
|
700
|
+
function limiterCurve(ceilingDb, kneeDb, range, points = LIMITER_POINTS) {
|
|
701
|
+
const ceiling = 10 ** (ceilingDb / 20);
|
|
702
|
+
const knee = Math.min(10 ** (kneeDb / 20), ceiling);
|
|
703
|
+
const curve = new Float32Array(new ArrayBuffer(points * 4));
|
|
704
|
+
for (let i = 0; i < points; i++) {
|
|
705
|
+
const x = (i / (points - 1) * 2 - 1) * range;
|
|
706
|
+
const a = Math.abs(x);
|
|
707
|
+
const y = a <= knee || ceiling === knee ? Math.min(a, ceiling) : knee + (ceiling - knee) * Math.tanh((a - knee) / (ceiling - knee));
|
|
708
|
+
curve[i] = Math.sign(x) * y;
|
|
709
|
+
}
|
|
710
|
+
return curve;
|
|
711
|
+
}
|
|
712
|
+
function buildChain(context, profile, random) {
|
|
713
|
+
let limiter;
|
|
714
|
+
if (profile.limiter) {
|
|
715
|
+
const range = profile.limiter.range ?? 4;
|
|
716
|
+
const input = context.createGain();
|
|
717
|
+
input.gain.value = 1 / range;
|
|
718
|
+
const shaper = context.createWaveShaper();
|
|
719
|
+
shaper.curve = limiterCurve(profile.limiter.ceiling, profile.limiter.knee, range);
|
|
720
|
+
input.connect(shaper);
|
|
721
|
+
shaper.connect(context.destination);
|
|
722
|
+
limiter = { input, shaper };
|
|
723
|
+
}
|
|
724
|
+
const output = limiter?.input ?? context.destination;
|
|
725
|
+
let compressor;
|
|
726
|
+
if (profile.compressor) {
|
|
727
|
+
compressor = context.createDynamicsCompressor();
|
|
728
|
+
compressor.threshold.value = profile.compressor.threshold;
|
|
729
|
+
compressor.ratio.value = profile.compressor.ratio;
|
|
730
|
+
if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
|
|
731
|
+
if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
|
|
732
|
+
if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
|
|
733
|
+
compressor.connect(output);
|
|
734
|
+
}
|
|
735
|
+
const master = context.createGain();
|
|
736
|
+
master.gain.value = profile.master;
|
|
737
|
+
master.connect(compressor ?? output);
|
|
738
|
+
if (!profile.reverb) {
|
|
739
|
+
return {
|
|
740
|
+
master,
|
|
741
|
+
disconnect: () => {
|
|
742
|
+
master.disconnect();
|
|
743
|
+
compressor?.disconnect();
|
|
744
|
+
limiter?.input.disconnect();
|
|
745
|
+
limiter?.shaper.disconnect();
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const convolver = context.createConvolver();
|
|
750
|
+
const length = context.sampleRate * profile.reverb.seconds;
|
|
751
|
+
convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length, decay: profile.reverb.decay }), length);
|
|
752
|
+
const wet = context.createGain();
|
|
753
|
+
wet.gain.value = profile.reverb.wet;
|
|
754
|
+
wet.connect(convolver);
|
|
755
|
+
convolver.connect(master);
|
|
756
|
+
return {
|
|
757
|
+
master,
|
|
758
|
+
wet,
|
|
759
|
+
disconnect: () => {
|
|
760
|
+
wet.disconnect();
|
|
761
|
+
convolver.disconnect();
|
|
762
|
+
master.disconnect();
|
|
763
|
+
compressor?.disconnect();
|
|
764
|
+
limiter?.input.disconnect();
|
|
765
|
+
limiter?.shaper.disconnect();
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/webaudio/context.ts
|
|
771
|
+
function globalContext() {
|
|
772
|
+
const scope = globalThis;
|
|
773
|
+
const Constructor = scope.AudioContext ?? scope.webkitAudioContext;
|
|
774
|
+
return Constructor ? new Constructor() : null;
|
|
775
|
+
}
|
|
776
|
+
function unlockContext(context) {
|
|
777
|
+
if (context.state === "suspended" && typeof context.close === "function") void context.resume();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/loops.ts
|
|
781
|
+
var LOOP_START_ABOVE = 2e-3;
|
|
782
|
+
var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
|
|
783
|
+
function createLoopState(recipe, open = false) {
|
|
784
|
+
const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
|
|
785
|
+
let created = open;
|
|
786
|
+
let volume = recipe.volume ?? 0;
|
|
787
|
+
return {
|
|
788
|
+
set(state) {
|
|
789
|
+
volume = state.volume ?? volume;
|
|
790
|
+
const create = !created;
|
|
791
|
+
if (create && !(volume >= startAbove)) return null;
|
|
792
|
+
created = true;
|
|
793
|
+
const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
|
|
794
|
+
return { create, volume, ...frequency === void 0 ? {} : { frequency } };
|
|
795
|
+
},
|
|
796
|
+
get created() {
|
|
797
|
+
return created;
|
|
798
|
+
},
|
|
799
|
+
reset() {
|
|
800
|
+
created = false;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/webaudio/nodes.ts
|
|
806
|
+
function assign(param, automation, time) {
|
|
807
|
+
if (automation.value !== void 0) param.value = automation.value;
|
|
808
|
+
if (automation.from !== void 0) param.setValueAtTime(automation.from, time);
|
|
809
|
+
}
|
|
810
|
+
function ramp(param, curve, value, at) {
|
|
811
|
+
if (curve === "set") param.setValueAtTime(value, at);
|
|
812
|
+
else if (curve === "exp") param.exponentialRampToValueAtTime(value, at);
|
|
813
|
+
else param.linearRampToValueAtTime(value, at);
|
|
814
|
+
}
|
|
815
|
+
function rampTo(param, automation, time) {
|
|
816
|
+
if (automation.to !== void 0 && automation.at !== void 0) ramp(param, automation.curve ?? "linear", automation.to, time + automation.at);
|
|
817
|
+
}
|
|
818
|
+
function applyEnvelope(param, points, time) {
|
|
819
|
+
for (const point of points) ramp(param, point.curve, point.value, time + point.time);
|
|
820
|
+
}
|
|
821
|
+
function noiseSource(target, loop) {
|
|
822
|
+
const source = target.context.createBufferSource();
|
|
823
|
+
source.buffer = target.noise();
|
|
824
|
+
if (loop) source.loop = true;
|
|
825
|
+
return source;
|
|
826
|
+
}
|
|
827
|
+
function filterNode(context, filter, time) {
|
|
828
|
+
const node = context.createBiquadFilter();
|
|
829
|
+
node.type = filter.type;
|
|
830
|
+
assign(node.frequency, filter.frequency, time);
|
|
831
|
+
if (filter.q !== void 0) node.Q.value = filter.q;
|
|
832
|
+
if (filter.gain !== void 0) node.gain.value = filter.gain;
|
|
833
|
+
return node;
|
|
834
|
+
}
|
|
835
|
+
function gainNode(context, value) {
|
|
836
|
+
const node = context.createGain();
|
|
837
|
+
node.gain.value = value;
|
|
838
|
+
return node;
|
|
839
|
+
}
|
|
840
|
+
function voiceOf(sources) {
|
|
841
|
+
return {
|
|
842
|
+
sources,
|
|
843
|
+
stop() {
|
|
844
|
+
for (const source of sources) {
|
|
845
|
+
try {
|
|
846
|
+
source.stop();
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/webaudio/realize-loop.ts
|
|
855
|
+
function outputStage(recipe, target, gain) {
|
|
856
|
+
if (!recipe.pan) {
|
|
857
|
+
gain.connect(target.output);
|
|
858
|
+
return { out: gain };
|
|
859
|
+
}
|
|
860
|
+
const panner = target.context.createStereoPanner();
|
|
861
|
+
gain.connect(panner);
|
|
862
|
+
panner.connect(target.output);
|
|
863
|
+
return { out: panner, panner };
|
|
864
|
+
}
|
|
865
|
+
var fixed = (range) => typeof range === "number" ? range : 0;
|
|
866
|
+
var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
|
|
867
|
+
function sourceOf(recipe, target) {
|
|
868
|
+
if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
|
|
869
|
+
const oscillator = target.context.createOscillator();
|
|
870
|
+
oscillator.type = recipe.source.wave;
|
|
871
|
+
if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
|
|
872
|
+
return { source: oscillator, frequency: oscillator.frequency };
|
|
873
|
+
}
|
|
874
|
+
function stopAll(sources) {
|
|
875
|
+
return () => {
|
|
876
|
+
for (const source of sources) {
|
|
877
|
+
try {
|
|
878
|
+
source.stop();
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function realizeGameLoop(recipe, target) {
|
|
885
|
+
const { context } = target;
|
|
886
|
+
const gain = context.createGain();
|
|
887
|
+
const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
|
|
888
|
+
gain.gain.value = 0;
|
|
889
|
+
const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
|
|
890
|
+
last?.connect(gain);
|
|
891
|
+
const output = outputStage(recipe, target, gain);
|
|
892
|
+
const { source, frequency } = sourceOf(recipe, target);
|
|
893
|
+
recipe.filters?.bank.forEach((spec, index) => {
|
|
894
|
+
const filter = filters[index];
|
|
895
|
+
filter.type = spec.type;
|
|
896
|
+
filter.frequency.value = fixedFrequency(spec.frequency);
|
|
897
|
+
if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
|
|
898
|
+
});
|
|
899
|
+
const sources = [source];
|
|
900
|
+
for (const modulator of recipe.modulators ?? []) {
|
|
901
|
+
const lfo = context.createOscillator();
|
|
902
|
+
const depth = context.createGain();
|
|
903
|
+
const stage = context.createGain();
|
|
904
|
+
if (modulator.wave) lfo.type = modulator.wave;
|
|
905
|
+
lfo.frequency.value = fixed(modulator.rate);
|
|
906
|
+
depth.gain.value = fixed(modulator.depth);
|
|
907
|
+
stage.gain.value = modulator.base ?? 0;
|
|
908
|
+
lfo.connect(depth);
|
|
909
|
+
depth.connect(stage.gain);
|
|
910
|
+
last?.disconnect();
|
|
911
|
+
last?.connect(stage);
|
|
912
|
+
stage.connect(gain);
|
|
913
|
+
lfo.start();
|
|
914
|
+
sources.push(lfo);
|
|
915
|
+
}
|
|
916
|
+
source.connect(filters[0] ?? gain);
|
|
917
|
+
source.start();
|
|
918
|
+
return { gain, ...output, source, ...frequency ? { frequency } : {}, ...filters[0] ? { cutoff: filters[0].frequency } : {}, stop: stopAll(sources) };
|
|
919
|
+
}
|
|
920
|
+
function realizeChainLoop(recipe, target, volume) {
|
|
921
|
+
const { context } = target;
|
|
922
|
+
const { source, frequency } = sourceOf(recipe, target);
|
|
923
|
+
const filters = (recipe.filters?.bank ?? []).map(
|
|
924
|
+
(spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
|
|
925
|
+
);
|
|
926
|
+
const gain = gainNode(context, volume);
|
|
927
|
+
const lfos = (recipe.modulators ?? []).map((modulator) => {
|
|
928
|
+
const lfo = context.createOscillator();
|
|
929
|
+
if (modulator.wave) lfo.type = modulator.wave;
|
|
930
|
+
lfo.frequency.value = fixed(modulator.rate);
|
|
931
|
+
const depth = gainNode(context, fixed(modulator.depth));
|
|
932
|
+
lfo.connect(depth);
|
|
933
|
+
depth.connect(gain.gain);
|
|
934
|
+
return lfo;
|
|
935
|
+
});
|
|
936
|
+
let tail = source;
|
|
937
|
+
for (const filter of filters) tail = tail.connect(filter);
|
|
938
|
+
tail.connect(gain);
|
|
939
|
+
const output = outputStage(recipe, target, gain);
|
|
940
|
+
source.start();
|
|
941
|
+
for (const lfo of lfos) lfo.start();
|
|
942
|
+
return { gain, ...output, source, ...frequency ? { frequency } : {}, ...filters[0] ? { cutoff: filters[0].frequency } : {}, stop: stopAll([source, ...lfos]) };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/webaudio/engine-loop.ts
|
|
946
|
+
var INERT = { set() {
|
|
947
|
+
}, stop() {
|
|
948
|
+
} };
|
|
949
|
+
function createLoopHandle(recipe, host, volume) {
|
|
950
|
+
if (!recipe) return INERT;
|
|
951
|
+
const loopRecipe = recipe;
|
|
952
|
+
const isChain = loopRecipe.order === "chain_loop";
|
|
953
|
+
const opening = volume ?? loopRecipe.volume ?? 0;
|
|
954
|
+
const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
|
|
955
|
+
let state = createLoopState(loopRecipe);
|
|
956
|
+
let nodes;
|
|
957
|
+
const entry = {
|
|
958
|
+
get created() {
|
|
959
|
+
return nodes !== void 0;
|
|
960
|
+
},
|
|
961
|
+
rewire() {
|
|
962
|
+
nodes?.out.disconnect();
|
|
963
|
+
nodes?.out.connect(host.output(loopRecipe.bus));
|
|
964
|
+
},
|
|
965
|
+
stop() {
|
|
966
|
+
nodes?.stop();
|
|
967
|
+
nodes?.out.disconnect();
|
|
968
|
+
nodes = void 0;
|
|
969
|
+
state = createLoopState(loopRecipe);
|
|
970
|
+
host.unregister(entry);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
const create = (context, startVolume) => {
|
|
974
|
+
if (loopRecipe.source.kind === "noise") host.prepareNoise();
|
|
975
|
+
const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
|
|
976
|
+
const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
|
|
977
|
+
host.register(entry);
|
|
978
|
+
return made;
|
|
979
|
+
};
|
|
980
|
+
const open = () => {
|
|
981
|
+
const context = host.ready();
|
|
982
|
+
if (!context) return;
|
|
983
|
+
nodes = create(context, opening);
|
|
984
|
+
state = createLoopState({ ...loopRecipe, volume: opening }, true);
|
|
985
|
+
};
|
|
986
|
+
if (isChain) open();
|
|
987
|
+
let position;
|
|
988
|
+
let cutoff;
|
|
989
|
+
const placed = loopRecipe.falloff !== void 0 || loopRecipe.pan === true;
|
|
990
|
+
return {
|
|
991
|
+
set(next) {
|
|
992
|
+
if (isChain && !nodes) open();
|
|
993
|
+
const context = host.ready();
|
|
994
|
+
if (!context) return;
|
|
995
|
+
position = next.position ?? position;
|
|
996
|
+
cutoff = next.cutoff ?? cutoff;
|
|
997
|
+
const action = state.set(next);
|
|
998
|
+
if (!action) return;
|
|
999
|
+
nodes ?? (nodes = create(context, 0));
|
|
1000
|
+
const listener = placed && position ? host.listener?.() : void 0;
|
|
1001
|
+
const distance = listener && position ? listenerDistance(listener, position, host.distance) : 0;
|
|
1002
|
+
const level = listener ? falloffGain(loopRecipe.falloff, action.volume, distance) : action.volume;
|
|
1003
|
+
nodes.gain.gain.setTargetAtTime(level, context.currentTime, smoothing.volume);
|
|
1004
|
+
if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
|
|
1005
|
+
if (cutoff !== void 0 && nodes.cutoff) nodes.cutoff.setTargetAtTime(cutoff / cutoffDivisor(loopRecipe.falloff, distance), context.currentTime, smoothing.frequency);
|
|
1006
|
+
if (listener && position && nodes.panner) nodes.panner.pan.setTargetAtTime(panFor(listener, position), context.currentTime, smoothing.frequency);
|
|
1007
|
+
},
|
|
1008
|
+
stop: entry.stop
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/sample.ts
|
|
1013
|
+
var SAMPLE_ENVIRONMENTS = ["indoor", "outdoor"];
|
|
1014
|
+
var DEFAULT_SAMPLE_FALLOFF = { model: "inverse_square", k: 2e-3, minGain: 2e-3 };
|
|
1015
|
+
var DEFAULT_CROSSOVER = { from: 30, to: 80 };
|
|
1016
|
+
var DEFAULT_SAMPLE_PITCH = 0.04;
|
|
1017
|
+
var DEFAULT_TAIL_GAIN = 0.6;
|
|
1018
|
+
var DEFAULT_SAMPLE_CUTOFF_K = 0.01;
|
|
1019
|
+
var OPEN_CUTOFF = 2e4;
|
|
1020
|
+
var LAYER_FLOOR = 1e-3;
|
|
1021
|
+
function pickTake(count, last, random) {
|
|
1022
|
+
if (count <= 1) return 0;
|
|
1023
|
+
if (last === void 0 || last < 0 || last >= count) return Math.min(count - 1, Math.floor(random() * count));
|
|
1024
|
+
const drawn = Math.min(count - 2, Math.floor(random() * (count - 1)));
|
|
1025
|
+
return drawn >= last ? drawn + 1 : drawn;
|
|
1026
|
+
}
|
|
1027
|
+
function farMixAt(crossover, distance) {
|
|
1028
|
+
if (crossover.to <= crossover.from) return distance >= crossover.to ? 1 : 0;
|
|
1029
|
+
const t = (distance - crossover.from) / (crossover.to - crossover.from);
|
|
1030
|
+
return t <= 0 ? 0 : t >= 1 ? 1 : t;
|
|
1031
|
+
}
|
|
1032
|
+
function crossGains(farMix) {
|
|
1033
|
+
return { close: Math.cos(farMix * Math.PI / 2), far: Math.sin(farMix * Math.PI / 2) };
|
|
1034
|
+
}
|
|
1035
|
+
function sampleCutoff(cutoffK, distance, occlusion = 0) {
|
|
1036
|
+
const byDistance = cutoffK > 0 ? OPEN_CUTOFF / (1 + distance * cutoffK) : OPEN_CUTOFF;
|
|
1037
|
+
const occluded = occlusion > 0 ? byDistance / (1 + occlusion * 9) : byDistance;
|
|
1038
|
+
return Math.max(80, Math.min(OPEN_CUTOFF, occluded));
|
|
1039
|
+
}
|
|
1040
|
+
function sampleUrls(sound) {
|
|
1041
|
+
const tiers = (sound.tiers ?? []).flatMap((tier) => [...tier.close, ...tier.far ?? []]);
|
|
1042
|
+
const all = [...sound.close, ...sound.far ?? [], ...tiers, ...sound.tail?.indoor ?? [], ...sound.tail?.outdoor ?? []].map((ref) => ref.url);
|
|
1043
|
+
return [...new Set(all)];
|
|
1044
|
+
}
|
|
1045
|
+
function tierFor(sound, intensity) {
|
|
1046
|
+
if (intensity === void 0 || !sound.tiers) return void 0;
|
|
1047
|
+
let chosen;
|
|
1048
|
+
for (const tier of sound.tiers) if (tier.from <= intensity) chosen = tier;
|
|
1049
|
+
return chosen ?? sound.tiers[0];
|
|
1050
|
+
}
|
|
1051
|
+
function planSample(sound, input) {
|
|
1052
|
+
if (sound.close.length === 0) throw new Error(`sample sound ${sound.id} has no close take`);
|
|
1053
|
+
const listener = input.listener ?? DEFAULT_LISTENER;
|
|
1054
|
+
const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
|
|
1055
|
+
const falloff = sound.falloff ?? DEFAULT_SAMPLE_FALLOFF;
|
|
1056
|
+
const occlusion = Math.max(0, Math.min(1, input.occlusion ?? 0));
|
|
1057
|
+
const level = falloffGain(falloff, (input.volume ?? 1) * (sound.gain ?? 1), distance) * (1 - 0.45 * occlusion);
|
|
1058
|
+
if (isSilent(falloff, level) || level <= 0) return null;
|
|
1059
|
+
const pitch = sound.pitch ?? DEFAULT_SAMPLE_PITCH;
|
|
1060
|
+
const rate = pitch > 0 ? 1 + (input.random() * 2 - 1) * pitch : 1;
|
|
1061
|
+
const environment = input.environment ?? "outdoor";
|
|
1062
|
+
const tier = tierFor(sound, input.intensity);
|
|
1063
|
+
const close = tier?.close.length ? tier.close : sound.close;
|
|
1064
|
+
const far = tier?.far?.length ? tier.far : sound.far ?? [];
|
|
1065
|
+
const farMix = far.length > 0 ? farMixAt(sound.crossover ?? DEFAULT_CROSSOVER, distance) : 0;
|
|
1066
|
+
const gains = crossGains(farMix);
|
|
1067
|
+
const layers = [];
|
|
1068
|
+
const take = (role, refs, gain, at) => {
|
|
1069
|
+
if (refs.length === 0 || gain < LAYER_FLOOR) return;
|
|
1070
|
+
const key = `${sound.id}/${tier && role !== "tail" ? `${tier.name}/` : ""}${role}`;
|
|
1071
|
+
const index = pickTake(refs.length, input.memory?.get(key), input.random);
|
|
1072
|
+
input.memory?.set(key, index);
|
|
1073
|
+
const ref = refs[index];
|
|
1074
|
+
layers.push({ role, url: ref.url, take: index, gain: gain * (ref.gain ?? 1) * level, rate, at });
|
|
1075
|
+
};
|
|
1076
|
+
take("close", close, gains.close, 0);
|
|
1077
|
+
take("far", far, gains.far, 0);
|
|
1078
|
+
const tails = sound.tail?.[environment] ?? [];
|
|
1079
|
+
take("tail", tails, sound.tail?.gain ?? DEFAULT_TAIL_GAIN, sound.tail?.at ?? 0);
|
|
1080
|
+
const pan = (sound.pan ?? true) && input.position ? panFor(listener, input.position) : void 0;
|
|
1081
|
+
return {
|
|
1082
|
+
kind: "sample",
|
|
1083
|
+
id: sound.id,
|
|
1084
|
+
bus: sound.bus,
|
|
1085
|
+
level,
|
|
1086
|
+
distance,
|
|
1087
|
+
...pan === void 0 ? {} : { pan },
|
|
1088
|
+
cutoff: sampleCutoff(sound.cutoffK ?? DEFAULT_SAMPLE_CUTOFF_K, distance, occlusion),
|
|
1089
|
+
rate,
|
|
1090
|
+
environment,
|
|
1091
|
+
...tier ? { tier: tier.name } : {},
|
|
1092
|
+
farMix,
|
|
1093
|
+
layers,
|
|
1094
|
+
priority: sound.priority ?? input.priority ?? busPriority(sound.bus),
|
|
1095
|
+
...sound.group ? { group: sound.group } : {}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/webaudio/sample-loop.ts
|
|
1100
|
+
var DEFAULT_LOOP_CROSSFADE = 0.25;
|
|
1101
|
+
var LOOKAHEAD = 1.5;
|
|
1102
|
+
var VOLUME_SMOOTHING = 0.05;
|
|
1103
|
+
function loopCrossfade(sound) {
|
|
1104
|
+
if (!sound.loop) return void 0;
|
|
1105
|
+
return sound.loop === true ? DEFAULT_LOOP_CROSSFADE : Math.max(0, sound.loop.crossfade ?? DEFAULT_LOOP_CROSSFADE);
|
|
1106
|
+
}
|
|
1107
|
+
var CROSSFADE_STEPS = 8;
|
|
1108
|
+
var equalPowerIn = (t) => Math.sin(Math.max(0, Math.min(1, t)) * Math.PI / 2);
|
|
1109
|
+
var ramp2 = (param, direction, at, seconds) => {
|
|
1110
|
+
const value = (t) => direction === "in" ? equalPowerIn(t) : equalPowerIn(1 - t);
|
|
1111
|
+
param.setValueAtTime(value(0), at);
|
|
1112
|
+
for (let step = 1; step <= CROSSFADE_STEPS; step++) param.linearRampToValueAtTime(value(step / CROSSFADE_STEPS), at + seconds * step / CROSSFADE_STEPS);
|
|
1113
|
+
};
|
|
1114
|
+
function createSampleLoopHandle(sound, host, volume = 0) {
|
|
1115
|
+
const crossfade = loopCrossfade(sound) ?? DEFAULT_LOOP_CROSSFADE;
|
|
1116
|
+
let level = volume;
|
|
1117
|
+
let stopped = false;
|
|
1118
|
+
let fallback;
|
|
1119
|
+
let master;
|
|
1120
|
+
let segments = [];
|
|
1121
|
+
let last;
|
|
1122
|
+
let timer;
|
|
1123
|
+
const entry = {
|
|
1124
|
+
get created() {
|
|
1125
|
+
return master !== void 0;
|
|
1126
|
+
},
|
|
1127
|
+
rewire() {
|
|
1128
|
+
master?.disconnect();
|
|
1129
|
+
master?.connect(host.output(sound.bus));
|
|
1130
|
+
},
|
|
1131
|
+
stop() {
|
|
1132
|
+
stopped = true;
|
|
1133
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1134
|
+
timer = void 0;
|
|
1135
|
+
for (const segment of segments) {
|
|
1136
|
+
try {
|
|
1137
|
+
segment.source.stop();
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
segment.source.disconnect();
|
|
1141
|
+
segment.gain.disconnect();
|
|
1142
|
+
}
|
|
1143
|
+
segments = [];
|
|
1144
|
+
master?.disconnect();
|
|
1145
|
+
master = void 0;
|
|
1146
|
+
fallback?.stop();
|
|
1147
|
+
fallback = void 0;
|
|
1148
|
+
host.unregister(entry);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
const nextBuffer = () => {
|
|
1152
|
+
const takes = sound.close;
|
|
1153
|
+
const take = pickTake(takes.length, last, host.random);
|
|
1154
|
+
last = take;
|
|
1155
|
+
const ref = takes[take];
|
|
1156
|
+
return ref ? host.buffer(ref.url) : void 0;
|
|
1157
|
+
};
|
|
1158
|
+
const schedule = (context, output, at, first) => {
|
|
1159
|
+
const buffer = nextBuffer();
|
|
1160
|
+
if (!buffer) return void 0;
|
|
1161
|
+
const source = context.createBufferSource();
|
|
1162
|
+
source.buffer = buffer;
|
|
1163
|
+
const gain = context.createGain();
|
|
1164
|
+
source.connect(gain);
|
|
1165
|
+
gain.connect(output);
|
|
1166
|
+
const fade = Math.min(crossfade, buffer.duration / 2);
|
|
1167
|
+
if (buffer.duration < 2 * crossfade || sound.close.length === 1) {
|
|
1168
|
+
source.loop = true;
|
|
1169
|
+
if (!first) ramp2(gain.gain, "in", at, Math.max(1e-3, fade));
|
|
1170
|
+
source.start(at);
|
|
1171
|
+
segments.push({ source, gain, end: Number.POSITIVE_INFINITY });
|
|
1172
|
+
return void 0;
|
|
1173
|
+
}
|
|
1174
|
+
if (first) gain.gain.setValueAtTime(1, at);
|
|
1175
|
+
else ramp2(gain.gain, "in", at, fade);
|
|
1176
|
+
const end3 = at + buffer.duration;
|
|
1177
|
+
ramp2(gain.gain, "out", end3 - fade, fade);
|
|
1178
|
+
source.start(at);
|
|
1179
|
+
source.stop(end3);
|
|
1180
|
+
segments.push({ source, gain, end: end3 });
|
|
1181
|
+
source.onended = () => {
|
|
1182
|
+
segments = segments.filter((segment) => segment.source !== source);
|
|
1183
|
+
source.disconnect();
|
|
1184
|
+
gain.disconnect();
|
|
1185
|
+
};
|
|
1186
|
+
return end3 - fade;
|
|
1187
|
+
};
|
|
1188
|
+
const pump = (context, output, at, first) => {
|
|
1189
|
+
if (stopped) return;
|
|
1190
|
+
let next = schedule(context, output, at, first);
|
|
1191
|
+
while (next !== void 0 && next < context.currentTime + LOOKAHEAD) next = schedule(context, output, Math.max(next, context.currentTime), false);
|
|
1192
|
+
if (next === void 0) return;
|
|
1193
|
+
const wait = Math.max(0, (next - context.currentTime - LOOKAHEAD / 2) * 1e3);
|
|
1194
|
+
timer = setTimeout(() => {
|
|
1195
|
+
timer = void 0;
|
|
1196
|
+
pump(context, output, next, false);
|
|
1197
|
+
}, wait);
|
|
1198
|
+
};
|
|
1199
|
+
const fileLevel = () => level * (sound.gain ?? 1);
|
|
1200
|
+
const startFiles = () => {
|
|
1201
|
+
if (stopped || master || !host.isReady(sound)) return master !== void 0;
|
|
1202
|
+
const context = host.ready();
|
|
1203
|
+
if (!context) return false;
|
|
1204
|
+
master = context.createGain();
|
|
1205
|
+
master.gain.setValueAtTime(fileLevel(), context.currentTime);
|
|
1206
|
+
master.connect(host.output(sound.bus));
|
|
1207
|
+
host.register(entry);
|
|
1208
|
+
pump(context, master, context.currentTime, true);
|
|
1209
|
+
if (fallback) {
|
|
1210
|
+
fallback.set({ volume: 0 });
|
|
1211
|
+
const old = fallback;
|
|
1212
|
+
fallback = void 0;
|
|
1213
|
+
setTimeout(() => old.stop(), VOLUME_SMOOTHING * 6e3);
|
|
1214
|
+
}
|
|
1215
|
+
return true;
|
|
1216
|
+
};
|
|
1217
|
+
const ensure = () => {
|
|
1218
|
+
if (stopped || master) return;
|
|
1219
|
+
if (startFiles()) return;
|
|
1220
|
+
if (!fallback) {
|
|
1221
|
+
fallback = host.fallback(level);
|
|
1222
|
+
void host.load(sound).then(() => {
|
|
1223
|
+
if (level > 0 || master) startFiles();
|
|
1224
|
+
}).catch(() => void 0);
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
if (volume > 0) ensure();
|
|
1228
|
+
return {
|
|
1229
|
+
set(next) {
|
|
1230
|
+
if (stopped) return;
|
|
1231
|
+
if (next.volume !== void 0) level = host.muted(sound.bus) ? 0 : Math.max(0, next.volume);
|
|
1232
|
+
if (level > 0) ensure();
|
|
1233
|
+
const context = host.ready();
|
|
1234
|
+
if (master && context) master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
|
|
1235
|
+
fallback?.set(next.volume === void 0 ? next : { ...next, volume: level });
|
|
1236
|
+
},
|
|
1237
|
+
stop: entry.stop
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/webaudio/engine-noise.ts
|
|
1242
|
+
function createNoiseStore(random) {
|
|
1243
|
+
const store = {
|
|
1244
|
+
random,
|
|
1245
|
+
reset() {
|
|
1246
|
+
store.samples = void 0;
|
|
1247
|
+
store.buffer = void 0;
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
return store;
|
|
1251
|
+
}
|
|
1252
|
+
function prepareNoise(store, context, seconds) {
|
|
1253
|
+
store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
|
|
1254
|
+
return store.samples;
|
|
1255
|
+
}
|
|
1256
|
+
function noiseBuffer(store, context, seconds) {
|
|
1257
|
+
const samples = prepareNoise(store, context, seconds);
|
|
1258
|
+
store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
|
|
1259
|
+
return store.buffer;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/webaudio/engine-queue.ts
|
|
1263
|
+
import { DEFERRED_QUEUE_ID } from "@adatechnology/game-contracts";
|
|
1264
|
+
var nowMs = () => typeof performance === "undefined" ? Date.now() : performance.now();
|
|
1265
|
+
function scheduleTask(task) {
|
|
1266
|
+
if (typeof MessageChannel === "undefined") {
|
|
1267
|
+
setTimeout(task, 0);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const channel = new MessageChannel();
|
|
1271
|
+
channel.port1.onmessage = () => {
|
|
1272
|
+
channel.port1.close();
|
|
1273
|
+
task();
|
|
1274
|
+
};
|
|
1275
|
+
channel.port2.postMessage(0);
|
|
1276
|
+
}
|
|
1277
|
+
function createStartQueue(options = {}) {
|
|
1278
|
+
const budgetMs = options.budgetMs ?? 2;
|
|
1279
|
+
const maxLatencyMs = options.maxLatencyMs ?? 250;
|
|
1280
|
+
let jobs = [];
|
|
1281
|
+
let scheduled = false;
|
|
1282
|
+
let attached = false;
|
|
1283
|
+
const runNext = () => {
|
|
1284
|
+
const job = jobs.shift();
|
|
1285
|
+
if (nowMs() - job.queuedAt > maxLatencyMs) job.drop();
|
|
1286
|
+
else job.run();
|
|
1287
|
+
};
|
|
1288
|
+
const drain = () => {
|
|
1289
|
+
scheduled = false;
|
|
1290
|
+
if (attached) return;
|
|
1291
|
+
const began = nowMs();
|
|
1292
|
+
while (jobs.length > 0) {
|
|
1293
|
+
runNext();
|
|
1294
|
+
if (nowMs() - began >= budgetMs) break;
|
|
1295
|
+
}
|
|
1296
|
+
if (jobs.length > 0) schedule();
|
|
1297
|
+
};
|
|
1298
|
+
const schedule = () => {
|
|
1299
|
+
if (scheduled || attached) return;
|
|
1300
|
+
scheduled = true;
|
|
1301
|
+
scheduleTask(drain);
|
|
1302
|
+
};
|
|
1303
|
+
const queue = {
|
|
1304
|
+
push(job) {
|
|
1305
|
+
jobs.push({ ...job, queuedAt: nowMs() });
|
|
1306
|
+
schedule();
|
|
1307
|
+
},
|
|
1308
|
+
clear() {
|
|
1309
|
+
for (const job of jobs) job.drop();
|
|
1310
|
+
jobs = [];
|
|
1311
|
+
},
|
|
1312
|
+
get size() {
|
|
1313
|
+
return jobs.length;
|
|
1314
|
+
},
|
|
1315
|
+
deferredQueue: () => deferredQueue
|
|
1316
|
+
};
|
|
1317
|
+
const deferredQueue = {
|
|
1318
|
+
id: DEFERRED_QUEUE_ID.AUDIO_VOICES,
|
|
1319
|
+
pending: () => jobs.length,
|
|
1320
|
+
run(deadline, clock) {
|
|
1321
|
+
let ran = 0;
|
|
1322
|
+
while (jobs.length > 0) {
|
|
1323
|
+
runNext();
|
|
1324
|
+
ran++;
|
|
1325
|
+
if (clock() >= deadline) break;
|
|
1326
|
+
}
|
|
1327
|
+
return ran;
|
|
1328
|
+
},
|
|
1329
|
+
flush() {
|
|
1330
|
+
while (jobs.length > 0) runNext();
|
|
1331
|
+
},
|
|
1332
|
+
attach() {
|
|
1333
|
+
attached = true;
|
|
1334
|
+
},
|
|
1335
|
+
detach() {
|
|
1336
|
+
attached = false;
|
|
1337
|
+
if (jobs.length > 0) schedule();
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
return queue;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/webaudio/realize-game.ts
|
|
1344
|
+
var end = (layer, start) => start + layer.duration + layer.tail;
|
|
1345
|
+
var firstFilter = (layer) => layer.filters?.bank[0];
|
|
1346
|
+
var offsetOf = (layer) => layer.source.kind === "noise" ? layer.source.offset : 0;
|
|
1347
|
+
function setFilter(node, filter, time) {
|
|
1348
|
+
if (!filter) return;
|
|
1349
|
+
node.type = filter.type;
|
|
1350
|
+
assign(node.frequency, filter.frequency, time);
|
|
1351
|
+
if (filter.q !== void 0) node.Q.value = filter.q;
|
|
1352
|
+
}
|
|
1353
|
+
function realizeBurst(plan, target) {
|
|
1354
|
+
const { context } = target;
|
|
1355
|
+
const layer = plan.layers[0];
|
|
1356
|
+
const time = target.time + layer.at;
|
|
1357
|
+
const gain = context.createGain();
|
|
1358
|
+
gain.connect(target.output);
|
|
1359
|
+
const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
|
|
1360
|
+
const filter = context.createBiquadFilter();
|
|
1361
|
+
source.connect(filter);
|
|
1362
|
+
filter.connect(gain);
|
|
1363
|
+
setFilter(filter, firstFilter(layer), time);
|
|
1364
|
+
applyEnvelope(gain.gain, layer.envelope, time);
|
|
1365
|
+
source.start(time, offsetOf(layer));
|
|
1366
|
+
source.stop(end(layer, time));
|
|
1367
|
+
return voiceOf([source]);
|
|
1368
|
+
}
|
|
1369
|
+
function realizeScream(plan, target) {
|
|
1370
|
+
const { context } = target;
|
|
1371
|
+
const layer = plan.layers[0];
|
|
1372
|
+
const time = target.time + layer.at;
|
|
1373
|
+
const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
|
|
1374
|
+
const gain = context.createGain();
|
|
1375
|
+
gain.connect(target.output);
|
|
1376
|
+
for (const band of layer.filters?.bank ?? []) {
|
|
1377
|
+
const filter = context.createBiquadFilter();
|
|
1378
|
+
filter.type = band.type;
|
|
1379
|
+
if (band.q !== void 0) filter.Q.value = band.q;
|
|
1380
|
+
assign(filter.frequency, band.frequency, time);
|
|
1381
|
+
rampTo(filter.frequency, band.frequency, time);
|
|
1382
|
+
const level = context.createGain();
|
|
1383
|
+
level.gain.value = band.level ?? 1;
|
|
1384
|
+
source.connect(filter);
|
|
1385
|
+
filter.connect(level);
|
|
1386
|
+
level.connect(gain);
|
|
1387
|
+
}
|
|
1388
|
+
const sources = [source];
|
|
1389
|
+
const lfos = layer.modulators.map((modulator) => {
|
|
1390
|
+
const lfo = context.createOscillator();
|
|
1391
|
+
const depth = context.createGain();
|
|
1392
|
+
if (modulator.wave) lfo.type = modulator.wave;
|
|
1393
|
+
lfo.frequency.value = modulator.rate;
|
|
1394
|
+
depth.gain.value = modulator.depth;
|
|
1395
|
+
lfo.connect(depth);
|
|
1396
|
+
depth.connect(gain.gain);
|
|
1397
|
+
return lfo;
|
|
1398
|
+
});
|
|
1399
|
+
applyEnvelope(gain.gain, layer.envelope, time);
|
|
1400
|
+
source.start(time, offsetOf(layer));
|
|
1401
|
+
source.stop(end(layer, time));
|
|
1402
|
+
for (const lfo of lfos) {
|
|
1403
|
+
lfo.start(time);
|
|
1404
|
+
lfo.stop(end(layer, time));
|
|
1405
|
+
sources.push(lfo);
|
|
1406
|
+
}
|
|
1407
|
+
return voiceOf(sources);
|
|
1408
|
+
}
|
|
1409
|
+
function realizeGroan(plan, target) {
|
|
1410
|
+
const { context } = target;
|
|
1411
|
+
const layer = plan.layers[0];
|
|
1412
|
+
const time = target.time + layer.at;
|
|
1413
|
+
const oscillator = context.createOscillator();
|
|
1414
|
+
const gain = context.createGain();
|
|
1415
|
+
const filter = context.createBiquadFilter();
|
|
1416
|
+
const band = firstFilter(layer);
|
|
1417
|
+
if (layer.source.kind === "osc") oscillator.type = layer.source.wave;
|
|
1418
|
+
if (band) {
|
|
1419
|
+
filter.type = band.type;
|
|
1420
|
+
if (band.q !== void 0) filter.Q.value = band.q;
|
|
1421
|
+
}
|
|
1422
|
+
if (layer.source.kind === "osc") {
|
|
1423
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1424
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1425
|
+
}
|
|
1426
|
+
if (band) assign(filter.frequency, band.frequency, time);
|
|
1427
|
+
applyEnvelope(gain.gain, layer.envelope, time);
|
|
1428
|
+
oscillator.connect(filter);
|
|
1429
|
+
filter.connect(gain);
|
|
1430
|
+
gain.connect(target.output);
|
|
1431
|
+
oscillator.start(time);
|
|
1432
|
+
oscillator.stop(end(layer, time));
|
|
1433
|
+
return voiceOf([oscillator]);
|
|
1434
|
+
}
|
|
1435
|
+
function realizeBoom(plan, target) {
|
|
1436
|
+
const { context } = target;
|
|
1437
|
+
const layer = plan.layers[0];
|
|
1438
|
+
const time = target.time + layer.at;
|
|
1439
|
+
const oscillator = context.createOscillator();
|
|
1440
|
+
const gain = context.createGain();
|
|
1441
|
+
if (layer.source.kind === "osc") {
|
|
1442
|
+
oscillator.type = layer.source.wave;
|
|
1443
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1444
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1445
|
+
}
|
|
1446
|
+
applyEnvelope(gain.gain, layer.envelope, time);
|
|
1447
|
+
oscillator.connect(gain);
|
|
1448
|
+
gain.connect(target.output);
|
|
1449
|
+
oscillator.start(time);
|
|
1450
|
+
oscillator.stop(end(layer, time));
|
|
1451
|
+
return voiceOf([oscillator]);
|
|
1452
|
+
}
|
|
1453
|
+
function realizeStatic(plan, target) {
|
|
1454
|
+
const { context } = target;
|
|
1455
|
+
const layer = plan.layers[0];
|
|
1456
|
+
const time = target.time + layer.at;
|
|
1457
|
+
const gain = context.createGain();
|
|
1458
|
+
const source = context.createBufferSource();
|
|
1459
|
+
const filter = context.createBiquadFilter();
|
|
1460
|
+
source.buffer = target.noise();
|
|
1461
|
+
setFilter(filter, firstFilter(layer), time);
|
|
1462
|
+
applyEnvelope(gain.gain, layer.envelope, time);
|
|
1463
|
+
source.connect(filter);
|
|
1464
|
+
filter.connect(gain);
|
|
1465
|
+
gain.connect(target.output);
|
|
1466
|
+
source.start(time, offsetOf(layer));
|
|
1467
|
+
source.stop(end(layer, time));
|
|
1468
|
+
return voiceOf([source]);
|
|
1469
|
+
}
|
|
1470
|
+
function realizeChord(plan, target) {
|
|
1471
|
+
const { context } = target;
|
|
1472
|
+
const layers = plan.layers;
|
|
1473
|
+
const oscillators = layers.map(() => context.createOscillator());
|
|
1474
|
+
const gain = context.createGain();
|
|
1475
|
+
for (let index = layers.length - 1; index >= 0; index--) {
|
|
1476
|
+
const source = layers[index].source;
|
|
1477
|
+
if (source.kind === "osc") oscillators[index].type = source.wave;
|
|
1478
|
+
}
|
|
1479
|
+
layers.forEach((layer, index) => {
|
|
1480
|
+
if (layer.source.kind === "osc") assign(oscillators[index].frequency, layer.source.frequency, target.time + layer.at);
|
|
1481
|
+
});
|
|
1482
|
+
applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
|
|
1483
|
+
for (const oscillator of oscillators) oscillator.connect(gain);
|
|
1484
|
+
gain.connect(target.output);
|
|
1485
|
+
layers.forEach((layer, index) => oscillators[index].start(target.time + layer.at));
|
|
1486
|
+
layers.forEach((layer, index) => oscillators[index].stop(end(layer, target.time + layer.at)));
|
|
1487
|
+
return voiceOf(oscillators);
|
|
1488
|
+
}
|
|
1489
|
+
function realizeCalls(plan, target) {
|
|
1490
|
+
const { context } = target;
|
|
1491
|
+
const gain = context.createGain();
|
|
1492
|
+
const filter = context.createBiquadFilter();
|
|
1493
|
+
if (plan.silent) return voiceOf([]);
|
|
1494
|
+
const band = firstFilter(plan.layers[0]);
|
|
1495
|
+
if (band) filter.type = band.type;
|
|
1496
|
+
filter.connect(gain);
|
|
1497
|
+
gain.connect(target.output);
|
|
1498
|
+
const oscillators = plan.layers.map((layer) => {
|
|
1499
|
+
const time = target.time + layer.at;
|
|
1500
|
+
const oscillator = context.createOscillator();
|
|
1501
|
+
if (layer.source.kind === "osc") {
|
|
1502
|
+
oscillator.type = layer.source.wave;
|
|
1503
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1504
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1505
|
+
}
|
|
1506
|
+
if (band) {
|
|
1507
|
+
assign(filter.frequency, band.frequency, time);
|
|
1508
|
+
if (band.q !== void 0) filter.Q.value = band.q;
|
|
1509
|
+
}
|
|
1510
|
+
const envelope = context.createGain();
|
|
1511
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1512
|
+
oscillator.connect(envelope);
|
|
1513
|
+
envelope.connect(filter);
|
|
1514
|
+
oscillator.start(time);
|
|
1515
|
+
oscillator.stop(end(layer, time));
|
|
1516
|
+
return oscillator;
|
|
1517
|
+
});
|
|
1518
|
+
return voiceOf(oscillators);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/webaudio/realize-layered.ts
|
|
1522
|
+
var end2 = (layer, start) => start + layer.duration + layer.tail;
|
|
1523
|
+
function outputStage2(plan, layer, target) {
|
|
1524
|
+
const { context } = target;
|
|
1525
|
+
const level = gainNode(context, layer.level);
|
|
1526
|
+
let node = level;
|
|
1527
|
+
if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
|
|
1528
|
+
const panner = context.createStereoPanner();
|
|
1529
|
+
panner.pan.value = plan.pan;
|
|
1530
|
+
level.connect(panner);
|
|
1531
|
+
node = panner;
|
|
1532
|
+
}
|
|
1533
|
+
node.connect(target.output);
|
|
1534
|
+
if (layer.wet && target.wet) {
|
|
1535
|
+
const send = gainNode(context, layer.wet);
|
|
1536
|
+
node.connect(send);
|
|
1537
|
+
send.connect(target.wet);
|
|
1538
|
+
}
|
|
1539
|
+
return level;
|
|
1540
|
+
}
|
|
1541
|
+
function realizeNoiseLayer(plan, layer, target) {
|
|
1542
|
+
const { context } = target;
|
|
1543
|
+
const time = target.time + layer.at;
|
|
1544
|
+
const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
|
|
1545
|
+
const filters = (layer.filters?.bank ?? []).map((filter) => {
|
|
1546
|
+
const node = filterNode(context, filter, time);
|
|
1547
|
+
rampTo(node.frequency, filter.frequency, time);
|
|
1548
|
+
return node;
|
|
1549
|
+
});
|
|
1550
|
+
const envelope = context.createGain();
|
|
1551
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1552
|
+
let tail = source;
|
|
1553
|
+
for (const filter of filters) tail = tail.connect(filter);
|
|
1554
|
+
tail.connect(envelope);
|
|
1555
|
+
envelope.connect(outputStage2(plan, layer, target));
|
|
1556
|
+
source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
|
|
1557
|
+
source.stop(end2(layer, time));
|
|
1558
|
+
return [source];
|
|
1559
|
+
}
|
|
1560
|
+
function realizeToneLayer(plan, layer, target) {
|
|
1561
|
+
const { context } = target;
|
|
1562
|
+
const time = target.time + layer.at;
|
|
1563
|
+
const oscillator = context.createOscillator();
|
|
1564
|
+
if (layer.source.kind === "osc") {
|
|
1565
|
+
oscillator.type = layer.source.wave;
|
|
1566
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1567
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1568
|
+
}
|
|
1569
|
+
const envelope = context.createGain();
|
|
1570
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1571
|
+
oscillator.connect(envelope);
|
|
1572
|
+
envelope.connect(outputStage2(plan, layer, target));
|
|
1573
|
+
oscillator.start(time);
|
|
1574
|
+
oscillator.stop(end2(layer, time));
|
|
1575
|
+
return [oscillator];
|
|
1576
|
+
}
|
|
1577
|
+
function realizeVoiceLayer(plan, layer, target) {
|
|
1578
|
+
const source = layer.source;
|
|
1579
|
+
if (source.kind !== "formant_voice") return [];
|
|
1580
|
+
const { context } = target;
|
|
1581
|
+
const time = target.time + layer.at;
|
|
1582
|
+
const oscillator = context.createOscillator();
|
|
1583
|
+
oscillator.type = source.wave;
|
|
1584
|
+
assign(oscillator.frequency, source.frequency, time);
|
|
1585
|
+
rampTo(oscillator.frequency, source.frequency, time);
|
|
1586
|
+
const vibrato = context.createOscillator();
|
|
1587
|
+
vibrato.frequency.value = source.vibrato.rate;
|
|
1588
|
+
const vibratoDepth = gainNode(context, source.vibrato.depth);
|
|
1589
|
+
vibrato.connect(vibratoDepth);
|
|
1590
|
+
vibratoDepth.connect(oscillator.frequency);
|
|
1591
|
+
const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
|
|
1592
|
+
const breath = noiseSource(target, true);
|
|
1593
|
+
const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
|
|
1594
|
+
const breathLevel = gainNode(context, source.breath.gain);
|
|
1595
|
+
const mix = gainNode(context, source.mix);
|
|
1596
|
+
for (const formant of formants) oscillator.connect(formant);
|
|
1597
|
+
breath.connect(breathBand);
|
|
1598
|
+
breathBand.connect(breathLevel);
|
|
1599
|
+
for (const formant of formants) formant.connect(mix);
|
|
1600
|
+
breathLevel.connect(mix);
|
|
1601
|
+
const envelope = context.createGain();
|
|
1602
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1603
|
+
mix.connect(envelope);
|
|
1604
|
+
envelope.connect(outputStage2(plan, layer, target));
|
|
1605
|
+
const stop = end2(layer, time);
|
|
1606
|
+
oscillator.start(time);
|
|
1607
|
+
vibrato.start(time);
|
|
1608
|
+
breath.start(time, source.breath.offset);
|
|
1609
|
+
oscillator.stop(stop);
|
|
1610
|
+
vibrato.stop(stop);
|
|
1611
|
+
breath.stop(stop);
|
|
1612
|
+
return [oscillator, vibrato, breath];
|
|
1613
|
+
}
|
|
1614
|
+
function realizeLayered(plan, target) {
|
|
1615
|
+
const sources = plan.layers.flatMap((layer) => {
|
|
1616
|
+
if (layer.source.kind === "noise") return realizeNoiseLayer(plan, layer, target);
|
|
1617
|
+
if (layer.source.kind === "osc") return realizeToneLayer(plan, layer, target);
|
|
1618
|
+
return realizeVoiceLayer(plan, layer, target);
|
|
1619
|
+
});
|
|
1620
|
+
return voiceOf(sources);
|
|
1621
|
+
}
|
|
1622
|
+
function realizeSiren(plan, target) {
|
|
1623
|
+
const { context } = target;
|
|
1624
|
+
const layer = plan.layers[0];
|
|
1625
|
+
const time = target.time + layer.at;
|
|
1626
|
+
const oscillator = context.createOscillator();
|
|
1627
|
+
if (layer.source.kind === "osc") {
|
|
1628
|
+
oscillator.type = layer.source.wave;
|
|
1629
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1630
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1631
|
+
}
|
|
1632
|
+
const depths = layer.modulators.map((modulator) => {
|
|
1633
|
+
const lfo = context.createOscillator();
|
|
1634
|
+
if (modulator.wave) lfo.type = modulator.wave;
|
|
1635
|
+
lfo.frequency.value = modulator.rate;
|
|
1636
|
+
const depth = gainNode(context, modulator.depth);
|
|
1637
|
+
lfo.connect(depth);
|
|
1638
|
+
if (modulator.target === "frequency") depth.connect(oscillator.frequency);
|
|
1639
|
+
return { lfo, depth, target: modulator.target };
|
|
1640
|
+
});
|
|
1641
|
+
const lfos = depths.map(({ lfo }) => lfo);
|
|
1642
|
+
const envelope = context.createGain();
|
|
1643
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1644
|
+
for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope.gain);
|
|
1645
|
+
const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
|
|
1646
|
+
let tail = oscillator;
|
|
1647
|
+
for (const filter of filters) tail = tail.connect(filter);
|
|
1648
|
+
tail.connect(envelope);
|
|
1649
|
+
envelope.connect(target.output);
|
|
1650
|
+
if (layer.wet && target.wet) {
|
|
1651
|
+
const send = gainNode(context, layer.wet);
|
|
1652
|
+
envelope.connect(send);
|
|
1653
|
+
send.connect(target.wet);
|
|
1654
|
+
}
|
|
1655
|
+
const stop = end2(layer, time);
|
|
1656
|
+
oscillator.start(time);
|
|
1657
|
+
for (const lfo of lfos) lfo.start(time);
|
|
1658
|
+
oscillator.stop(stop);
|
|
1659
|
+
for (const lfo of lfos) lfo.stop(stop);
|
|
1660
|
+
return voiceOf([oscillator, ...lfos]);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/webaudio/realize-weapon.ts
|
|
1664
|
+
function noiseLayer(layer, target, time) {
|
|
1665
|
+
const { context } = target;
|
|
1666
|
+
const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
|
|
1667
|
+
const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
|
|
1668
|
+
const envelope = context.createGain();
|
|
1669
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1670
|
+
let tail = source;
|
|
1671
|
+
for (const filter of filters) tail = tail.connect(filter);
|
|
1672
|
+
tail.connect(envelope).connect(target.output);
|
|
1673
|
+
source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
|
|
1674
|
+
source.stop(time + layer.duration + layer.tail);
|
|
1675
|
+
return source;
|
|
1676
|
+
}
|
|
1677
|
+
function toneLayer(layer, target, time) {
|
|
1678
|
+
const { context } = target;
|
|
1679
|
+
const oscillator = context.createOscillator();
|
|
1680
|
+
if (layer.source.kind === "osc") {
|
|
1681
|
+
oscillator.type = layer.source.wave;
|
|
1682
|
+
assign(oscillator.frequency, layer.source.frequency, time);
|
|
1683
|
+
rampTo(oscillator.frequency, layer.source.frequency, time);
|
|
1684
|
+
}
|
|
1685
|
+
const envelope = context.createGain();
|
|
1686
|
+
applyEnvelope(envelope.gain, layer.envelope, time);
|
|
1687
|
+
oscillator.connect(envelope).connect(target.output);
|
|
1688
|
+
oscillator.start(time);
|
|
1689
|
+
oscillator.stop(time + layer.duration + layer.tail);
|
|
1690
|
+
return oscillator;
|
|
1691
|
+
}
|
|
1692
|
+
function realizeWeapon(plan, target) {
|
|
1693
|
+
const sources = plan.layers.map((layer) => {
|
|
1694
|
+
const time = target.time + layer.at;
|
|
1695
|
+
return layer.source.kind === "noise" ? noiseLayer(layer, target, time) : toneLayer(layer, target, time);
|
|
1696
|
+
});
|
|
1697
|
+
return voiceOf(sources);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/webaudio/realize.ts
|
|
1701
|
+
var BUILDERS = {
|
|
1702
|
+
layered: realizeLayered,
|
|
1703
|
+
burst: realizeBurst,
|
|
1704
|
+
scream: realizeScream,
|
|
1705
|
+
groan: realizeGroan,
|
|
1706
|
+
boom: realizeBoom,
|
|
1707
|
+
static: realizeStatic,
|
|
1708
|
+
chord: realizeChord,
|
|
1709
|
+
calls: realizeCalls,
|
|
1710
|
+
siren: realizeSiren,
|
|
1711
|
+
weapon: realizeWeapon
|
|
1712
|
+
};
|
|
1713
|
+
function realize(plan, target) {
|
|
1714
|
+
return BUILDERS[plan.order](plan, target);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/variation.ts
|
|
1718
|
+
var tuneAutomation = (automation, rate) => {
|
|
1719
|
+
const tuned = { ...automation };
|
|
1720
|
+
if (automation.value !== void 0) tuned.value = automation.value * rate;
|
|
1721
|
+
if (automation.from !== void 0) tuned.from = automation.from * rate;
|
|
1722
|
+
if (automation.to !== void 0) tuned.to = automation.to * rate;
|
|
1723
|
+
if (automation.at !== void 0) tuned.at = automation.at / rate;
|
|
1724
|
+
return tuned;
|
|
1725
|
+
};
|
|
1726
|
+
var tuneFilter = (filter, rate) => ({ ...filter, frequency: tuneAutomation(filter.frequency, rate) });
|
|
1727
|
+
var tuneSource = (source, rate) => {
|
|
1728
|
+
switch (source.kind) {
|
|
1729
|
+
case "noise":
|
|
1730
|
+
return source;
|
|
1731
|
+
case "osc":
|
|
1732
|
+
return { ...source, frequency: tuneAutomation(source.frequency, rate) };
|
|
1733
|
+
case "formant_voice":
|
|
1734
|
+
return {
|
|
1735
|
+
...source,
|
|
1736
|
+
frequency: tuneAutomation(source.frequency, rate),
|
|
1737
|
+
formants: source.formants.map((formant) => ({ ...formant, frequency: formant.frequency * rate })),
|
|
1738
|
+
breath: { ...source.breath, frequency: source.breath.frequency * rate }
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
var tuneModulator = (modulator, rate) => ({
|
|
1743
|
+
...modulator,
|
|
1744
|
+
rate: modulator.rate * rate,
|
|
1745
|
+
...modulator.target === "frequency" ? { depth: modulator.depth * rate } : {}
|
|
1746
|
+
});
|
|
1747
|
+
var tuneLayer = (layer, rate) => ({
|
|
1748
|
+
...layer,
|
|
1749
|
+
at: layer.at / rate,
|
|
1750
|
+
duration: layer.duration / rate,
|
|
1751
|
+
tail: layer.tail / rate,
|
|
1752
|
+
source: tuneSource(layer.source, rate),
|
|
1753
|
+
...layer.filters ? { filters: { ...layer.filters, bank: layer.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {},
|
|
1754
|
+
modulators: layer.modulators.map((modulator) => tuneModulator(modulator, rate)),
|
|
1755
|
+
envelope: layer.envelope.map((point) => ({ ...point, time: point.time / rate }))
|
|
1756
|
+
});
|
|
1757
|
+
function retunePlan(plan, rate) {
|
|
1758
|
+
if (rate === 1 || !(rate > 0)) return plan;
|
|
1759
|
+
return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer) => tuneLayer(layer, rate)) };
|
|
1760
|
+
}
|
|
1761
|
+
function createSetPicker() {
|
|
1762
|
+
const last = /* @__PURE__ */ new Map();
|
|
1763
|
+
return {
|
|
1764
|
+
pick(set, random) {
|
|
1765
|
+
const count = set.variants.length;
|
|
1766
|
+
const previous = last.get(set.id);
|
|
1767
|
+
let index;
|
|
1768
|
+
if (count === 1) index = 0;
|
|
1769
|
+
else if (set.repeat || previous === void 0) index = Math.min(count - 1, Math.floor(random() * count));
|
|
1770
|
+
else {
|
|
1771
|
+
index = Math.min(count - 2, Math.floor(random() * (count - 1)));
|
|
1772
|
+
if (index >= previous) index += 1;
|
|
1773
|
+
}
|
|
1774
|
+
last.set(set.id, index);
|
|
1775
|
+
const [min, max] = set.rate ?? [1, 1];
|
|
1776
|
+
const rate = min === max ? min : min + random() * (max - min);
|
|
1777
|
+
return { id: set.variants[index], rate };
|
|
1778
|
+
},
|
|
1779
|
+
reset: () => last.clear()
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/webaudio/sample-store.ts
|
|
1784
|
+
function fetchLoader(baseUrl) {
|
|
1785
|
+
return async (url2) => {
|
|
1786
|
+
const target = baseUrl ? new URL(url2, baseUrl).toString() : url2;
|
|
1787
|
+
const response = await fetch(target);
|
|
1788
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${target}`);
|
|
1789
|
+
return response.arrayBuffer();
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function inlineLoader(files) {
|
|
1793
|
+
return async (url2) => {
|
|
1794
|
+
const text = files[url2];
|
|
1795
|
+
if (text === void 0) throw new Error(`no inline file for ${url2}`);
|
|
1796
|
+
const base64 = text.startsWith("data:") ? text.slice(text.indexOf(",") + 1) : text;
|
|
1797
|
+
const binary = atob(base64);
|
|
1798
|
+
const bytes = new Uint8Array(binary.length);
|
|
1799
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
1800
|
+
return bytes.buffer;
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
var decode = (context, bytes) => new Promise((resolve, reject) => {
|
|
1804
|
+
const result = context.decodeAudioData(bytes, resolve, reject);
|
|
1805
|
+
result?.catch?.(reject);
|
|
1806
|
+
});
|
|
1807
|
+
function createSampleStore(loader = fetchLoader()) {
|
|
1808
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
1809
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1810
|
+
const failed = /* @__PURE__ */ new Map();
|
|
1811
|
+
let generation = 0;
|
|
1812
|
+
const loadOne = (url2, context) => {
|
|
1813
|
+
const existing = buffers.get(url2);
|
|
1814
|
+
if (existing) return Promise.resolve(existing);
|
|
1815
|
+
const inFlight = pending.get(url2);
|
|
1816
|
+
if (inFlight) return inFlight;
|
|
1817
|
+
const started = generation;
|
|
1818
|
+
failed.delete(url2);
|
|
1819
|
+
const promise = loader(url2).then((bytes) => decode(context, bytes)).then((buffer) => {
|
|
1820
|
+
if (started === generation) buffers.set(url2, buffer);
|
|
1821
|
+
return buffer;
|
|
1822
|
+
}).catch((error) => {
|
|
1823
|
+
if (started === generation) failed.set(url2, error instanceof Error ? error.message : String(error));
|
|
1824
|
+
throw error;
|
|
1825
|
+
}).finally(() => {
|
|
1826
|
+
if (pending.get(url2) === promise) pending.delete(url2);
|
|
1827
|
+
});
|
|
1828
|
+
pending.set(url2, promise);
|
|
1829
|
+
return promise;
|
|
1830
|
+
};
|
|
1831
|
+
return {
|
|
1832
|
+
status(url2) {
|
|
1833
|
+
if (buffers.has(url2)) return "ready";
|
|
1834
|
+
if (pending.has(url2)) return "loading";
|
|
1835
|
+
if (failed.has(url2)) return "failed";
|
|
1836
|
+
return "none";
|
|
1837
|
+
},
|
|
1838
|
+
buffer: (url2) => buffers.get(url2),
|
|
1839
|
+
ready: (urls) => urls.every((url2) => buffers.has(url2)),
|
|
1840
|
+
seconds: (url2) => buffers.get(url2)?.duration,
|
|
1841
|
+
async load(urls, context) {
|
|
1842
|
+
const results = await Promise.allSettled(urls.map((url2) => loadOne(url2, context)));
|
|
1843
|
+
const loaded = [];
|
|
1844
|
+
const errors = [];
|
|
1845
|
+
results.forEach((result, index) => {
|
|
1846
|
+
const url2 = urls[index];
|
|
1847
|
+
if (result.status === "fulfilled") loaded.push(url2);
|
|
1848
|
+
else errors.push({ url: url2, error: result.reason instanceof Error ? result.reason.message : String(result.reason) });
|
|
1849
|
+
});
|
|
1850
|
+
return { loaded, failed: errors };
|
|
1851
|
+
},
|
|
1852
|
+
clear() {
|
|
1853
|
+
generation++;
|
|
1854
|
+
buffers.clear();
|
|
1855
|
+
pending.clear();
|
|
1856
|
+
failed.clear();
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/webaudio/realize-sample.ts
|
|
1862
|
+
function realizeSample(plan, target) {
|
|
1863
|
+
const context = target.context;
|
|
1864
|
+
let head = target.output;
|
|
1865
|
+
if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
|
|
1866
|
+
const panner = context.createStereoPanner();
|
|
1867
|
+
panner.pan.value = plan.pan;
|
|
1868
|
+
panner.connect(head);
|
|
1869
|
+
head = panner;
|
|
1870
|
+
}
|
|
1871
|
+
if (plan.cutoff < OPEN_CUTOFF) {
|
|
1872
|
+
const lowpass = context.createBiquadFilter();
|
|
1873
|
+
lowpass.type = "lowpass";
|
|
1874
|
+
lowpass.frequency.value = plan.cutoff;
|
|
1875
|
+
lowpass.Q.value = 0.7;
|
|
1876
|
+
lowpass.connect(head);
|
|
1877
|
+
head = lowpass;
|
|
1878
|
+
}
|
|
1879
|
+
const voice = context.createGain();
|
|
1880
|
+
voice.connect(head);
|
|
1881
|
+
head = voice;
|
|
1882
|
+
const sources = [];
|
|
1883
|
+
for (const layer of plan.layers) {
|
|
1884
|
+
const buffer = target.buffer(layer.url);
|
|
1885
|
+
if (!buffer) continue;
|
|
1886
|
+
const source = context.createBufferSource();
|
|
1887
|
+
source.buffer = buffer;
|
|
1888
|
+
source.playbackRate.value = layer.rate;
|
|
1889
|
+
const gain = gainNode(context, layer.gain);
|
|
1890
|
+
source.connect(gain);
|
|
1891
|
+
gain.connect(head);
|
|
1892
|
+
source.start(target.time + layer.at);
|
|
1893
|
+
sources.push(source);
|
|
1894
|
+
}
|
|
1895
|
+
const realized = voiceOf(sources);
|
|
1896
|
+
return {
|
|
1897
|
+
...realized,
|
|
1898
|
+
fade(at, seconds) {
|
|
1899
|
+
voice.gain.setValueAtTime(1, at);
|
|
1900
|
+
voice.gain.linearRampToValueAtTime(0, at + seconds);
|
|
1901
|
+
for (const source of sources) {
|
|
1902
|
+
try {
|
|
1903
|
+
source.stop(at + seconds);
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function sampleLength(plan, seconds) {
|
|
1911
|
+
let length = 0;
|
|
1912
|
+
for (const layer of plan.layers) length = Math.max(length, layer.at + (seconds(layer.url) ?? 0) / layer.rate);
|
|
1913
|
+
return length;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/webaudio/engine.ts
|
|
1917
|
+
function limiterCurve2(knee, ceiling, range, points = 2049) {
|
|
1918
|
+
const curve = new Float32Array(points);
|
|
1919
|
+
const room = Math.max(1e-6, ceiling - knee);
|
|
1920
|
+
for (let index = 0; index < points; index++) {
|
|
1921
|
+
const value = (index / (points - 1) * 2 - 1) * range;
|
|
1922
|
+
const size = Math.abs(value);
|
|
1923
|
+
const bent = size <= knee ? size : knee + room * Math.tanh((size - knee) / room);
|
|
1924
|
+
curve[index] = Math.sign(value) * bent;
|
|
1925
|
+
}
|
|
1926
|
+
return curve;
|
|
1927
|
+
}
|
|
1928
|
+
var usesNoise = (recipe) => recipe.layers.some((layer) => layer.source.kind !== "osc");
|
|
1929
|
+
var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
|
|
1930
|
+
function createAudioEngine(options = {}) {
|
|
1931
|
+
const seed = options.seed ?? 1;
|
|
1932
|
+
const registry = createSoundRegistry(options.banks);
|
|
1933
|
+
const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
|
|
1934
|
+
const engineRandom = createRng(seed);
|
|
1935
|
+
const shared = createNoiseStore(engineRandom);
|
|
1936
|
+
const busSettings = /* @__PURE__ */ new Map();
|
|
1937
|
+
const busNodes = /* @__PURE__ */ new Map();
|
|
1938
|
+
const meters = /* @__PURE__ */ new Map();
|
|
1939
|
+
const active = /* @__PURE__ */ new Map();
|
|
1940
|
+
const loops = /* @__PURE__ */ new Set();
|
|
1941
|
+
const named = /* @__PURE__ */ new Map();
|
|
1942
|
+
let mix = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
|
|
1943
|
+
let context = null;
|
|
1944
|
+
let chain = null;
|
|
1945
|
+
let listener = DEFAULT_LISTENER;
|
|
1946
|
+
let lastTime = 0;
|
|
1947
|
+
let environment = options.environment ?? "outdoor";
|
|
1948
|
+
const sampleLimiters = /* @__PURE__ */ new Map();
|
|
1949
|
+
const sampleOutput = (bus) => {
|
|
1950
|
+
const setting = options.samples?.limiter;
|
|
1951
|
+
if (setting === false || !context) return outputFor(bus);
|
|
1952
|
+
const existing = sampleLimiters.get(bus);
|
|
1953
|
+
if (existing) return existing;
|
|
1954
|
+
const range = 4;
|
|
1955
|
+
const input = context.createGain();
|
|
1956
|
+
input.gain.value = 1 / range;
|
|
1957
|
+
const shaper = context.createWaveShaper();
|
|
1958
|
+
shaper.curve = limiterCurve2(setting?.knee ?? 0.5, setting?.ceiling ?? 0.891, range);
|
|
1959
|
+
ensureBus(bus);
|
|
1960
|
+
input.connect(shaper);
|
|
1961
|
+
shaper.connect(outputFor(bus));
|
|
1962
|
+
sampleLimiters.set(bus, input);
|
|
1963
|
+
return input;
|
|
1964
|
+
};
|
|
1965
|
+
const chokes = /* @__PURE__ */ new Map();
|
|
1966
|
+
const choked = (id) => {
|
|
1967
|
+
const list = (chokes.get(id) ?? []).filter((entry) => active.has(entry.id));
|
|
1968
|
+
chokes.set(id, list);
|
|
1969
|
+
return list;
|
|
1970
|
+
};
|
|
1971
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
1972
|
+
const announce = (event) => {
|
|
1973
|
+
options.onVoiceStart?.(event);
|
|
1974
|
+
for (const watcher of watchers) watcher(event);
|
|
1975
|
+
};
|
|
1976
|
+
const sources = [];
|
|
1977
|
+
const baseLoader = options.samples?.loader ?? fetchLoader(options.samples?.baseUrl);
|
|
1978
|
+
const sampleStore = createSampleStore((url2) => (sources.find((source) => source.accepts(url2))?.loader ?? baseLoader)(url2));
|
|
1979
|
+
const memory = /* @__PURE__ */ new Map();
|
|
1980
|
+
const latencyOf = (audio) => {
|
|
1981
|
+
const live = audio;
|
|
1982
|
+
return (live.baseLatency ?? 0) + (live.outputLatency ?? 0);
|
|
1983
|
+
};
|
|
1984
|
+
const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
|
|
1985
|
+
const watched = /* @__PURE__ */ new Set();
|
|
1986
|
+
const meterOf = (bus) => {
|
|
1987
|
+
const existing = meters.get(bus);
|
|
1988
|
+
if (existing) return existing;
|
|
1989
|
+
const node = busNodes.get(bus);
|
|
1990
|
+
if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
|
|
1991
|
+
const analyser = context.createAnalyser();
|
|
1992
|
+
analyser.fftSize = 1024;
|
|
1993
|
+
node.connect(analyser);
|
|
1994
|
+
const made = { analyser, data: new Float32Array(analyser.fftSize) };
|
|
1995
|
+
meters.set(bus, made);
|
|
1996
|
+
return made;
|
|
1997
|
+
};
|
|
1998
|
+
const ensureBus = (bus) => {
|
|
1999
|
+
if (!context || !chain || busNodes.has(bus)) return;
|
|
2000
|
+
const node = context.createGain();
|
|
2001
|
+
node.connect(chain.master);
|
|
2002
|
+
busNodes.set(bus, node);
|
|
2003
|
+
if (watched.has(bus)) meterOf(bus);
|
|
2004
|
+
};
|
|
2005
|
+
const ready = () => {
|
|
2006
|
+
if (context) return context;
|
|
2007
|
+
const created = (options.createContext ?? globalContext)();
|
|
2008
|
+
if (!created) return null;
|
|
2009
|
+
context = created;
|
|
2010
|
+
chain = buildChain(created, mix, engineRandom);
|
|
2011
|
+
if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
|
|
2012
|
+
for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
|
|
2013
|
+
busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
|
|
2014
|
+
}
|
|
2015
|
+
for (const [bus, setting] of busSettings) {
|
|
2016
|
+
ensureBus(bus);
|
|
2017
|
+
busNodes.get(bus).gain.value = busGain(setting);
|
|
2018
|
+
}
|
|
2019
|
+
lastTime = created.currentTime;
|
|
2020
|
+
return created;
|
|
2021
|
+
};
|
|
2022
|
+
const advance = () => {
|
|
2023
|
+
if (!context) return;
|
|
2024
|
+
const elapsed = context.currentTime - lastTime;
|
|
2025
|
+
lastTime = context.currentTime;
|
|
2026
|
+
if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
|
|
2027
|
+
};
|
|
2028
|
+
const inputFor = (recipe, playOptions, random) => ({
|
|
2029
|
+
random,
|
|
2030
|
+
listener,
|
|
2031
|
+
noiseSeconds: mix.noise.seconds,
|
|
2032
|
+
...options.distance ? { distanceMode: options.distance } : {},
|
|
2033
|
+
...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
|
|
2034
|
+
...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
|
|
2035
|
+
...playOptions.position ? { position: playOptions.position } : {},
|
|
2036
|
+
...playOptions.params ? { params: playOptions.params } : {},
|
|
2037
|
+
...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
|
|
2038
|
+
});
|
|
2039
|
+
const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null;
|
|
2040
|
+
const build = (plan, store, delay, requestedAt, fallbackOf) => {
|
|
2041
|
+
const audio = context;
|
|
2042
|
+
const time = audio.currentTime + delay;
|
|
2043
|
+
const realized = realize(plan, {
|
|
2044
|
+
context: audio,
|
|
2045
|
+
output: outputFor(plan.bus),
|
|
2046
|
+
...chain?.wet ? { wet: chain.wet } : {},
|
|
2047
|
+
noise: () => noiseBuffer(store, audio, mix.noise.seconds),
|
|
2048
|
+
time
|
|
2049
|
+
});
|
|
2050
|
+
const recipeBank = registry.bankOf(plan.id);
|
|
2051
|
+
announce({
|
|
2052
|
+
id: plan.id,
|
|
2053
|
+
kind: "recipe",
|
|
2054
|
+
...fallbackOf ? { fallbackOf } : {},
|
|
2055
|
+
...recipeBank ? { bank: recipeBank } : {},
|
|
2056
|
+
requestedAt,
|
|
2057
|
+
startAt: time,
|
|
2058
|
+
outputLatency: latencyOf(audio),
|
|
2059
|
+
level: plan.level,
|
|
2060
|
+
distance: plan.distance,
|
|
2061
|
+
...plan.pan === void 0 ? {} : { pan: plan.pan }
|
|
2062
|
+
});
|
|
2063
|
+
return realized;
|
|
2064
|
+
};
|
|
2065
|
+
const deferVoice = (plan, store, voiceId, delay, fallbackOf) => {
|
|
2066
|
+
const requestedAt = context?.currentTime ?? 0;
|
|
2067
|
+
let built = null;
|
|
2068
|
+
let cancelled = false;
|
|
2069
|
+
const pending = {
|
|
2070
|
+
get sources() {
|
|
2071
|
+
return built?.sources ?? [];
|
|
2072
|
+
},
|
|
2073
|
+
stop() {
|
|
2074
|
+
cancelled = true;
|
|
2075
|
+
built?.stop();
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
startQueue.push({
|
|
2079
|
+
run() {
|
|
2080
|
+
if (!cancelled && context) built = build(plan, store, delay, requestedAt, fallbackOf);
|
|
2081
|
+
},
|
|
2082
|
+
drop() {
|
|
2083
|
+
if (voiceId === void 0) return;
|
|
2084
|
+
voices.release(voiceId);
|
|
2085
|
+
active.delete(voiceId);
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
return pending;
|
|
2089
|
+
};
|
|
2090
|
+
const start = (plan, store, voiceId, delay = 0, fallbackOf, urgent = false) => {
|
|
2091
|
+
const realized = startQueue && !urgent ? deferVoice(plan, store, voiceId, delay, fallbackOf) : build(plan, store, delay, context?.currentTime ?? 0, fallbackOf);
|
|
2092
|
+
if (voiceId === void 0) return null;
|
|
2093
|
+
voices.setLength(voiceId, plan.length + delay);
|
|
2094
|
+
active.set(voiceId, realized);
|
|
2095
|
+
return {
|
|
2096
|
+
id: voiceId,
|
|
2097
|
+
kind: "recipe",
|
|
2098
|
+
plan,
|
|
2099
|
+
stop() {
|
|
2100
|
+
realized.stop();
|
|
2101
|
+
voices.release(voiceId);
|
|
2102
|
+
active.delete(voiceId);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
};
|
|
2106
|
+
const admit = (recipe, gain, distance) => {
|
|
2107
|
+
const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance, length: 0 });
|
|
2108
|
+
if (!decision.accepted) return null;
|
|
2109
|
+
for (const id of decision.evicted) {
|
|
2110
|
+
active.get(id)?.stop();
|
|
2111
|
+
active.delete(id);
|
|
2112
|
+
}
|
|
2113
|
+
return decision.id;
|
|
2114
|
+
};
|
|
2115
|
+
const picker = createSetPicker();
|
|
2116
|
+
const resolve = (id, random, chooser = picker) => {
|
|
2117
|
+
if (registry.sound(id)) return { id, rate: 1 };
|
|
2118
|
+
const set = registry.set(id);
|
|
2119
|
+
return set ? chooser.pick(set, random) : { id, rate: 1 };
|
|
2120
|
+
};
|
|
2121
|
+
const loadSounds = (sounds) => {
|
|
2122
|
+
const audio = ready();
|
|
2123
|
+
if (!audio) return Promise.resolve({ loaded: [], failed: sounds.flatMap(sampleUrls).map((url2) => ({ url: url2, error: "no audio context" })) });
|
|
2124
|
+
return sampleStore.load([...new Set(sounds.flatMap(sampleUrls))], audio);
|
|
2125
|
+
};
|
|
2126
|
+
const sampleInput = (playOptions, random, withMemory) => ({
|
|
2127
|
+
random,
|
|
2128
|
+
listener,
|
|
2129
|
+
environment: playOptions.environment ?? environment,
|
|
2130
|
+
...withMemory ? { memory } : {},
|
|
2131
|
+
...options.distance ? { distanceMode: options.distance } : {},
|
|
2132
|
+
...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
|
|
2133
|
+
...playOptions.intensity === void 0 ? {} : { intensity: playOptions.intensity },
|
|
2134
|
+
...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
|
|
2135
|
+
...playOptions.position ? { position: playOptions.position } : {},
|
|
2136
|
+
...playOptions.occlusion !== void 0 ? { occlusion: playOptions.occlusion } : options.occlusion && playOptions.position ? { occlusion: options.occlusion(playOptions.position) } : {}
|
|
2137
|
+
});
|
|
2138
|
+
const playSample = (sample, playOptions, random, store) => {
|
|
2139
|
+
if (busSettings.get(sample.bus)?.muted) return null;
|
|
2140
|
+
if (!sampleStore.ready(sampleUrls(sample))) {
|
|
2141
|
+
if (options.samples?.autoLoad !== false && ready()) void loadSounds([sample]).catch(() => void 0);
|
|
2142
|
+
if (sample.fallback && sample.fallback !== sample.id) return playWith(sample.fallback, playOptions, random, store, sample.id);
|
|
2143
|
+
return registry.sound(sample.id) ? void 0 : null;
|
|
2144
|
+
}
|
|
2145
|
+
const plan = planSample(sample, sampleInput(playOptions, playOptions.seed === void 0 ? random : createRng(playOptions.seed), true));
|
|
2146
|
+
if (!plan) return null;
|
|
2147
|
+
const audio = ready();
|
|
2148
|
+
if (!audio) return null;
|
|
2149
|
+
unlockContext(audio);
|
|
2150
|
+
advance();
|
|
2151
|
+
const voiceId = admit(plan, plan.level, plan.distance);
|
|
2152
|
+
if (voiceId === null) return null;
|
|
2153
|
+
const requestedAt = audio.currentTime;
|
|
2154
|
+
const time = requestedAt + (playOptions.delay ?? 0);
|
|
2155
|
+
const chokeList = sample.choke ? choked(sample.id) : void 0;
|
|
2156
|
+
const realized = realizeSample(plan, { context: audio, output: sampleOutput(plan.bus), buffer: (url2) => sampleStore.buffer(url2), time });
|
|
2157
|
+
const sampleBank = registry.bankOf(plan.id);
|
|
2158
|
+
announce({
|
|
2159
|
+
id: plan.id,
|
|
2160
|
+
kind: "sample",
|
|
2161
|
+
...sampleBank ? { bank: sampleBank } : {},
|
|
2162
|
+
requestedAt,
|
|
2163
|
+
startAt: time,
|
|
2164
|
+
outputLatency: latencyOf(audio),
|
|
2165
|
+
level: plan.level,
|
|
2166
|
+
distance: plan.distance,
|
|
2167
|
+
...plan.pan === void 0 ? {} : { pan: plan.pan },
|
|
2168
|
+
layers: plan.layers.map((layer) => ({ role: layer.role, url: layer.url, take: layer.take, rate: layer.rate })),
|
|
2169
|
+
...plan.tier ? { tier: plan.tier } : {}
|
|
2170
|
+
});
|
|
2171
|
+
voices.setLength(voiceId, sampleLength(plan, (url2) => sampleStore.seconds(url2)) + (playOptions.delay ?? 0));
|
|
2172
|
+
active.set(voiceId, realized);
|
|
2173
|
+
if (chokeList && sample.choke) {
|
|
2174
|
+
chokeList.push({ id: voiceId, voice: realized });
|
|
2175
|
+
while (chokeList.length > Math.max(1, sample.choke.voices)) {
|
|
2176
|
+
const oldest = chokeList.shift();
|
|
2177
|
+
if (!oldest) break;
|
|
2178
|
+
oldest.voice.fade(time, sample.choke.fade ?? 0.03);
|
|
2179
|
+
voices.release(oldest.id);
|
|
2180
|
+
active.delete(oldest.id);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return {
|
|
2184
|
+
id: voiceId,
|
|
2185
|
+
kind: "sample",
|
|
2186
|
+
plan,
|
|
2187
|
+
stop() {
|
|
2188
|
+
realized.stop();
|
|
2189
|
+
voices.release(voiceId);
|
|
2190
|
+
active.delete(voiceId);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
};
|
|
2194
|
+
const playWith = (asked, playOptions, streamRandom, store, fallbackOf) => {
|
|
2195
|
+
const sample = fallbackOf === void 0 ? registry.sample(asked) : void 0;
|
|
2196
|
+
if (sample) {
|
|
2197
|
+
const played = playSample(sample, playOptions, streamRandom, store);
|
|
2198
|
+
if (played !== void 0) return played;
|
|
2199
|
+
}
|
|
2200
|
+
const seeded = playOptions.seed === void 0 ? null : createRng(playOptions.seed);
|
|
2201
|
+
const { id, rate } = resolve(asked, seeded ?? streamRandom);
|
|
2202
|
+
const random = seeded ?? streamRandom;
|
|
2203
|
+
const recipe = registry.sound(id);
|
|
2204
|
+
if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
|
|
2205
|
+
const distance = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
|
|
2206
|
+
const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance);
|
|
2207
|
+
const silent = isSilent(recipe.falloff, level);
|
|
2208
|
+
if (silent && !recipe.keepSilent) return null;
|
|
2209
|
+
const audio = ready();
|
|
2210
|
+
if (!audio) return null;
|
|
2211
|
+
unlockContext(audio);
|
|
2212
|
+
advance();
|
|
2213
|
+
const voiceId = silent ? void 0 : admit(recipe, level, distance);
|
|
2214
|
+
if (voiceId === null) return null;
|
|
2215
|
+
if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds);
|
|
2216
|
+
const plan = planSound(recipe, inputFor(recipe, playOptions, random));
|
|
2217
|
+
return plan ? start(retunePlan(plan, rate), store, voiceId, playOptions.delay, fallbackOf, playOptions.urgent) : null;
|
|
2218
|
+
};
|
|
2219
|
+
const randomFor = (playOptions) => {
|
|
2220
|
+
if (playOptions.channel === void 0) return engineRandom;
|
|
2221
|
+
const existing = named.get(playOptions.channel);
|
|
2222
|
+
if (existing) return existing;
|
|
2223
|
+
const stream = streamFor(seed, playOptions.channel);
|
|
2224
|
+
named.set(playOptions.channel, stream);
|
|
2225
|
+
return stream;
|
|
2226
|
+
};
|
|
2227
|
+
const planWith = (asked, playOptions, random) => {
|
|
2228
|
+
const { id, rate } = resolve(asked, random, createSetPicker());
|
|
2229
|
+
const recipe = registry.sound(id);
|
|
2230
|
+
const plan = recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
|
|
2231
|
+
return plan ? retunePlan(plan, rate) : null;
|
|
2232
|
+
};
|
|
2233
|
+
const loopHost = {
|
|
2234
|
+
ready,
|
|
2235
|
+
output: outputFor,
|
|
2236
|
+
prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
|
|
2237
|
+
noise: () => noiseBuffer(shared, context, mix.noise.seconds),
|
|
2238
|
+
register: (loop) => void loops.add(loop),
|
|
2239
|
+
unregister: (loop) => void loops.delete(loop),
|
|
2240
|
+
listener: () => listener,
|
|
2241
|
+
...options.distance ? { distance: options.distance } : {}
|
|
2242
|
+
};
|
|
2243
|
+
return {
|
|
2244
|
+
registry,
|
|
2245
|
+
register: (bank) => registry.register(bank),
|
|
2246
|
+
plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),
|
|
2247
|
+
planSample(id, playOptions = {}) {
|
|
2248
|
+
const sample = registry.sample(id);
|
|
2249
|
+
return sample ? planSample(sample, sampleInput(playOptions, createRng(playOptions.seed ?? seed), false)) : null;
|
|
2250
|
+
},
|
|
2251
|
+
loadSamples(ids) {
|
|
2252
|
+
const chosen = (ids ?? registry.sampleIds()).map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
|
|
2253
|
+
return loadSounds(chosen);
|
|
2254
|
+
},
|
|
2255
|
+
sampleStatus(id) {
|
|
2256
|
+
const sample = registry.sample(id);
|
|
2257
|
+
if (!sample) return "none";
|
|
2258
|
+
const statuses = sampleUrls(sample).map((url2) => sampleStore.status(url2));
|
|
2259
|
+
if (statuses.every((status) => status === "ready")) return "ready";
|
|
2260
|
+
if (statuses.includes("failed")) return "failed";
|
|
2261
|
+
if (statuses.includes("loading")) return "loading";
|
|
2262
|
+
return statuses.includes("ready") ? "loading" : "none";
|
|
2263
|
+
},
|
|
2264
|
+
setEnvironment(next) {
|
|
2265
|
+
environment = next;
|
|
2266
|
+
},
|
|
2267
|
+
output: () => chain?.master ?? null,
|
|
2268
|
+
addSampleSource(accepts, loader) {
|
|
2269
|
+
sources.push({ accepts, loader });
|
|
2270
|
+
},
|
|
2271
|
+
watchVoices(listener2) {
|
|
2272
|
+
watchers.add(listener2);
|
|
2273
|
+
return () => void watchers.delete(listener2);
|
|
2274
|
+
},
|
|
2275
|
+
play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
|
|
2276
|
+
playPlan(plan, playOptions = {}) {
|
|
2277
|
+
const audio = ready();
|
|
2278
|
+
if (!audio) return null;
|
|
2279
|
+
advance();
|
|
2280
|
+
const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
|
|
2281
|
+
const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
|
|
2282
|
+
if (voiceId === null) return null;
|
|
2283
|
+
if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
|
|
2284
|
+
return start(plan, shared, voiceId, playOptions.delay);
|
|
2285
|
+
},
|
|
2286
|
+
loop(id, loopOptions = {}) {
|
|
2287
|
+
const sample = registry.sample(id);
|
|
2288
|
+
if (!sample?.loop) return createLoopHandle(registry.loop(id), loopHost, loopOptions.volume);
|
|
2289
|
+
return createSampleLoopHandle(
|
|
2290
|
+
sample,
|
|
2291
|
+
{
|
|
2292
|
+
ready,
|
|
2293
|
+
output: sampleOutput,
|
|
2294
|
+
buffer: (url2) => sampleStore.buffer(url2),
|
|
2295
|
+
isReady: (sound) => sampleStore.ready(sampleUrls(sound)),
|
|
2296
|
+
load: (sound) => options.samples?.autoLoad === false ? Promise.resolve() : loadSounds([sound]),
|
|
2297
|
+
fallback: (volume) => createLoopHandle(registry.loop(sample.fallback ?? sample.id) ?? registry.loop(sample.id), loopHost, volume),
|
|
2298
|
+
random: engineRandom,
|
|
2299
|
+
register: loopHost.register,
|
|
2300
|
+
unregister: loopHost.unregister,
|
|
2301
|
+
muted: (bus) => busSettings.get(bus)?.muted === true
|
|
2302
|
+
},
|
|
2303
|
+
loopOptions.volume
|
|
2304
|
+
);
|
|
2305
|
+
},
|
|
2306
|
+
channel(channelOptions) {
|
|
2307
|
+
const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng(channelOptions.seed));
|
|
2308
|
+
const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
|
|
2309
|
+
return {
|
|
2310
|
+
random,
|
|
2311
|
+
play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
|
|
2312
|
+
plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed))
|
|
2313
|
+
};
|
|
2314
|
+
},
|
|
2315
|
+
setListener(next) {
|
|
2316
|
+
listener = next;
|
|
2317
|
+
},
|
|
2318
|
+
setBus(id, setting) {
|
|
2319
|
+
busSettings.set(id, { ...busSettings.get(id), ...setting });
|
|
2320
|
+
if (!context) return;
|
|
2321
|
+
ensureBus(id);
|
|
2322
|
+
busNodes.get(id).gain.value = busGain(busSettings.get(id));
|
|
2323
|
+
},
|
|
2324
|
+
setMix(next) {
|
|
2325
|
+
const previous = mix;
|
|
2326
|
+
mix = resolveMix(next, options.profiles);
|
|
2327
|
+
if (!context || !chain) return;
|
|
2328
|
+
const old = chain;
|
|
2329
|
+
chain = buildChain(context, mix, engineRandom);
|
|
2330
|
+
if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
|
|
2331
|
+
for (const node of busNodes.values()) {
|
|
2332
|
+
node.disconnect();
|
|
2333
|
+
node.connect(chain.master);
|
|
2334
|
+
}
|
|
2335
|
+
for (const loop of loops) loop.rewire();
|
|
2336
|
+
old.disconnect();
|
|
2337
|
+
},
|
|
2338
|
+
setMaster(gain) {
|
|
2339
|
+
mix = { ...mix, master: gain };
|
|
2340
|
+
if (chain) chain.master.gain.value = gain;
|
|
2341
|
+
},
|
|
2342
|
+
unlock() {
|
|
2343
|
+
const audio = ready();
|
|
2344
|
+
if (audio) unlockContext(audio);
|
|
2345
|
+
},
|
|
2346
|
+
warmUp() {
|
|
2347
|
+
const audio = ready();
|
|
2348
|
+
if (!audio) return false;
|
|
2349
|
+
noiseBuffer(shared, audio, mix.noise.seconds);
|
|
2350
|
+
return true;
|
|
2351
|
+
},
|
|
2352
|
+
level(bus) {
|
|
2353
|
+
watched.add(bus);
|
|
2354
|
+
ensureBus(bus);
|
|
2355
|
+
const meter = meterOf(bus);
|
|
2356
|
+
if (!meter) return 0;
|
|
2357
|
+
meter.analyser.getFloatTimeDomainData(meter.data);
|
|
2358
|
+
let sum = 0;
|
|
2359
|
+
for (const sample of meter.data) sum += sample * sample;
|
|
2360
|
+
return Math.sqrt(sum / meter.data.length);
|
|
2361
|
+
},
|
|
2362
|
+
deferredQueue: () => startQueue?.deferredQueue() ?? null,
|
|
2363
|
+
stats() {
|
|
2364
|
+
advance();
|
|
2365
|
+
const voiceStats = voices.stats();
|
|
2366
|
+
return {
|
|
2367
|
+
context: context ? context.state : "none",
|
|
2368
|
+
voices: voiceStats.active,
|
|
2369
|
+
peak: voiceStats.peak,
|
|
2370
|
+
dropped: voiceStats.dropped,
|
|
2371
|
+
maxVoices: voiceStats.maxVoices,
|
|
2372
|
+
loops: [...loops].filter((loop) => loop.created).length
|
|
2373
|
+
};
|
|
2374
|
+
},
|
|
2375
|
+
dispose() {
|
|
2376
|
+
startQueue?.clear();
|
|
2377
|
+
for (const id of active.keys()) voices.release(id);
|
|
2378
|
+
for (const loop of [...loops]) loop.stop();
|
|
2379
|
+
active.clear();
|
|
2380
|
+
void context?.close();
|
|
2381
|
+
context = null;
|
|
2382
|
+
chain = null;
|
|
2383
|
+
busNodes.clear();
|
|
2384
|
+
meters.clear();
|
|
2385
|
+
shared.reset();
|
|
2386
|
+
sampleStore.clear();
|
|
2387
|
+
memory.clear();
|
|
2388
|
+
sampleLimiters.clear();
|
|
2389
|
+
chokes.clear();
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/bench/locale.ts
|
|
2395
|
+
var EN = {
|
|
2396
|
+
group: "Sound",
|
|
2397
|
+
name: "Sound area",
|
|
2398
|
+
probe: "Sound probe",
|
|
2399
|
+
controls: {
|
|
2400
|
+
sound: "Sound",
|
|
2401
|
+
distance: "Distance",
|
|
2402
|
+
bearing: "Bearing",
|
|
2403
|
+
volume: "Volume",
|
|
2404
|
+
mix: "Mixing chain",
|
|
2405
|
+
seed: "Seed",
|
|
2406
|
+
play: "Play",
|
|
2407
|
+
voice_storm: "Voice storm",
|
|
2408
|
+
loop: "Loop",
|
|
2409
|
+
loop_level: "Loop level",
|
|
2410
|
+
loop_pitch: "Loop pitch",
|
|
2411
|
+
source: "Source"
|
|
2412
|
+
},
|
|
2413
|
+
hints: {
|
|
2414
|
+
distance: "Metres from the listener: the level falls and the sound is muffled with it.",
|
|
2415
|
+
bearing: "Degrees from straight ahead, positive to the right: shows the panning.",
|
|
2416
|
+
mix: "The game chain (master only) or the art scene chain (compressor and reverb).",
|
|
2417
|
+
seed: "The same seed gives the same sound.",
|
|
2418
|
+
voice_storm: "Twenty of the chosen sound at once: the voice limiter drops what its group and the global cap refuse.",
|
|
2419
|
+
loop_pitch: "Frequency of an oscillator loop, in hertz (0 leaves it where it is).",
|
|
2420
|
+
source: "Where the sound comes from: the distance and bearing set here, or the probe standing in the area."
|
|
2421
|
+
},
|
|
2422
|
+
options: { none: "None", bench: "Bench settings", probe: "The probe" },
|
|
2423
|
+
layers: { reverb: "Reverb", compressor: "Compressor", panning: "Panning", muffling: "Muffling by distance" },
|
|
2424
|
+
metrics: {
|
|
2425
|
+
voices: "Voices playing",
|
|
2426
|
+
peak: "Most voices at once",
|
|
2427
|
+
dropped: "Voices dropped",
|
|
2428
|
+
loops: "Loops running",
|
|
2429
|
+
context: "Audio context",
|
|
2430
|
+
plan_length: "Chosen sound: length",
|
|
2431
|
+
plan_level: "Chosen sound: level",
|
|
2432
|
+
plan_pan: "Chosen sound: pan",
|
|
2433
|
+
level_weapons: "Level: weapons",
|
|
2434
|
+
level_voices: "Level: voices",
|
|
2435
|
+
level_world: "Level: world",
|
|
2436
|
+
level_ambience: "Level: ambience",
|
|
2437
|
+
level_weather: "Level: weather",
|
|
2438
|
+
level_ui: "Level: interface"
|
|
2439
|
+
},
|
|
2440
|
+
scenarios: {
|
|
2441
|
+
panning: "A sound to the right is heard on the right, one to the left on the left, one ahead in the middle",
|
|
2442
|
+
distance: "The farther a sound, the quieter, until it is not played",
|
|
2443
|
+
determinism: "The same seed gives the same sound plan, another seed another one",
|
|
2444
|
+
limiter: "The voice limiter holds a storm of voices to its group rule and its global cap"
|
|
2445
|
+
},
|
|
2446
|
+
mixes: { game: "Game", art_scene: "Art scene", kit: "Kit (art scene + limiter)" },
|
|
2447
|
+
sheet: { title: "Sound plan", level: "level", distance: "distance", pan: "pan", length: "seconds", silent: "too far to be heard", layer: "layer", none: "no sound chosen" }
|
|
2448
|
+
};
|
|
2449
|
+
var PT_BR = {
|
|
2450
|
+
group: "Som",
|
|
2451
|
+
name: "\xC1rea de som",
|
|
2452
|
+
probe: "Sonda de som",
|
|
2453
|
+
controls: {
|
|
2454
|
+
sound: "Som",
|
|
2455
|
+
distance: "Dist\xE2ncia",
|
|
2456
|
+
bearing: "Dire\xE7\xE3o",
|
|
2457
|
+
volume: "Volume",
|
|
2458
|
+
mix: "Cadeia de mixagem",
|
|
2459
|
+
seed: "Semente",
|
|
2460
|
+
play: "Tocar",
|
|
2461
|
+
voice_storm: "Tempestade de vozes",
|
|
2462
|
+
loop: "La\xE7o",
|
|
2463
|
+
loop_level: "N\xEDvel do la\xE7o",
|
|
2464
|
+
loop_pitch: "Altura do la\xE7o",
|
|
2465
|
+
source: "Origem"
|
|
2466
|
+
},
|
|
2467
|
+
hints: {
|
|
2468
|
+
distance: "Metros at\xE9 o ouvinte: o n\xEDvel cai e o som abafa junto.",
|
|
2469
|
+
bearing: "Graus a partir da frente, positivo para a direita: mostra o pan.",
|
|
2470
|
+
mix: "A cadeia do jogo (s\xF3 o master) ou a da Cena de Arte (compressor e reverb).",
|
|
2471
|
+
seed: "A mesma semente d\xE1 o mesmo som.",
|
|
2472
|
+
voice_storm: "Vinte do som escolhido de uma vez: o limite de vozes derruba o que a regra do grupo e o teto global recusam.",
|
|
2473
|
+
loop_pitch: "Frequ\xEAncia de um la\xE7o de oscilador, em hertz (0 deixa onde est\xE1).",
|
|
2474
|
+
source: "De onde vem o som: a dist\xE2ncia e a dire\xE7\xE3o daqui, ou a sonda que est\xE1 na \xE1rea."
|
|
2475
|
+
},
|
|
2476
|
+
options: { none: "Nenhum", bench: "Ajustes da bancada", probe: "A sonda" },
|
|
2477
|
+
layers: { reverb: "Reverb", compressor: "Compressor", panning: "Pan", muffling: "Abafamento pela dist\xE2ncia" },
|
|
2478
|
+
metrics: {
|
|
2479
|
+
voices: "Vozes tocando",
|
|
2480
|
+
peak: "Mais vozes de uma vez",
|
|
2481
|
+
dropped: "Vozes derrubadas",
|
|
2482
|
+
loops: "La\xE7os ligados",
|
|
2483
|
+
context: "Contexto de \xE1udio",
|
|
2484
|
+
plan_length: "Som escolhido: dura\xE7\xE3o",
|
|
2485
|
+
plan_level: "Som escolhido: n\xEDvel",
|
|
2486
|
+
plan_pan: "Som escolhido: pan",
|
|
2487
|
+
level_weapons: "N\xEDvel: armas",
|
|
2488
|
+
level_voices: "N\xEDvel: vozes",
|
|
2489
|
+
level_world: "N\xEDvel: mundo",
|
|
2490
|
+
level_ambience: "N\xEDvel: ambiente",
|
|
2491
|
+
level_weather: "N\xEDvel: clima",
|
|
2492
|
+
level_ui: "N\xEDvel: interface"
|
|
2493
|
+
},
|
|
2494
|
+
scenarios: {
|
|
2495
|
+
panning: "Um som \xE0 direita sai \xE0 direita, \xE0 esquerda sai \xE0 esquerda, \xE0 frente sai no meio",
|
|
2496
|
+
distance: "Quanto mais longe, mais baixo, at\xE9 n\xE3o tocar",
|
|
2497
|
+
determinism: "A mesma semente d\xE1 o mesmo plano de som, outra semente d\xE1 outro",
|
|
2498
|
+
limiter: "O limite de vozes segura uma tempestade de vozes na regra do grupo e no teto global"
|
|
2499
|
+
},
|
|
2500
|
+
mixes: { game: "Jogo", art_scene: "Cena de Arte", kit: "Kit (Cena de Arte + limitador)" },
|
|
2501
|
+
sheet: { title: "Plano do som", level: "n\xEDvel", distance: "dist\xE2ncia", pan: "pan", length: "segundos", silent: "longe demais para ouvir", layer: "camada", none: "nenhum som escolhido" }
|
|
2502
|
+
};
|
|
2503
|
+
var audioBenchTextOf = (locale) => locale === "en" ? EN : PT_BR;
|
|
2504
|
+
|
|
2505
|
+
// src/bench/module.ts
|
|
2506
|
+
var BENCH_BANK = {
|
|
2507
|
+
id: "audio_bench",
|
|
2508
|
+
sounds: [
|
|
2509
|
+
{
|
|
2510
|
+
id: "bench_ping",
|
|
2511
|
+
bus: "ui",
|
|
2512
|
+
pan: true,
|
|
2513
|
+
duration: 0.25,
|
|
2514
|
+
layers: [
|
|
2515
|
+
{
|
|
2516
|
+
gain: 0.5,
|
|
2517
|
+
wet: 0.3,
|
|
2518
|
+
source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
|
|
2519
|
+
envelope: [
|
|
2520
|
+
[0, 1e-4, "set", "fixed"],
|
|
2521
|
+
[2e-3, 1, "exp"],
|
|
2522
|
+
["1d", 1e-4, "exp", "fixed"]
|
|
2523
|
+
]
|
|
2524
|
+
}
|
|
2525
|
+
],
|
|
2526
|
+
falloff: DISTANCE_CURVES.art_scene,
|
|
2527
|
+
sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
id: "bench_hiss",
|
|
2531
|
+
bus: "ui",
|
|
2532
|
+
pan: true,
|
|
2533
|
+
duration: [0.3, 0.9],
|
|
2534
|
+
layers: [
|
|
2535
|
+
{
|
|
2536
|
+
gain: 0.4,
|
|
2537
|
+
source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
|
|
2538
|
+
filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
|
|
2539
|
+
envelope: [
|
|
2540
|
+
[0, 1e-4, "set", "fixed"],
|
|
2541
|
+
[2e-3, 1, "exp"],
|
|
2542
|
+
["1d", 1e-4, "exp", "fixed"]
|
|
2543
|
+
]
|
|
2544
|
+
}
|
|
2545
|
+
],
|
|
2546
|
+
falloff: DISTANCE_CURVES.art_scene,
|
|
2547
|
+
sources: ["sdk: bench probe, a highpass hiss of random length"]
|
|
2548
|
+
}
|
|
2549
|
+
]
|
|
2550
|
+
};
|
|
2551
|
+
var DISTANCES = [0, 10, 40, 60, 120, 160];
|
|
2552
|
+
var STORM = 20;
|
|
2553
|
+
var PROBE_ID = "audio_probe";
|
|
2554
|
+
var PROBE_AT = [0, 0, -10];
|
|
2555
|
+
var NONE = "none";
|
|
2556
|
+
var DEFAULT_SOUND = "bench_ping";
|
|
2557
|
+
var variantOf = (profile, reverb, compressor) => {
|
|
2558
|
+
const { reverb: wet, compressor: chain, ...rest } = profile;
|
|
2559
|
+
return { ...rest, id: `${profile.id}${reverb ? "" : "+dry"}${compressor ? "" : "+open"}`, ...wet && reverb ? { reverb: wet } : {}, ...chain && compressor ? { compressor: chain } : {} };
|
|
2560
|
+
};
|
|
2561
|
+
function createAudioTestModule(options = {}) {
|
|
2562
|
+
const locale = options.locale ?? "pt-BR";
|
|
2563
|
+
const text = audioBenchTextOf(locale);
|
|
2564
|
+
const banks = [BENCH_BANK, ...options.banks ?? [GAME_SFX_BANK, LIBRARY_BANK]];
|
|
2565
|
+
let engine = options.engine ?? createAudioEngine({ seed: options.seed ?? 1, banks });
|
|
2566
|
+
for (const bank of banks) engine.register(bank);
|
|
2567
|
+
let host = null;
|
|
2568
|
+
let loop;
|
|
2569
|
+
const state = {
|
|
2570
|
+
sound: options.sound ?? DEFAULT_SOUND,
|
|
2571
|
+
distance: 10,
|
|
2572
|
+
bearing: 0,
|
|
2573
|
+
volume: 1,
|
|
2574
|
+
mix: DEFAULT_MIX,
|
|
2575
|
+
seed: options.seed ?? 1,
|
|
2576
|
+
source: "bench",
|
|
2577
|
+
loop: NONE,
|
|
2578
|
+
loopLevel: 0.1,
|
|
2579
|
+
loopPitch: 0,
|
|
2580
|
+
reverb: true,
|
|
2581
|
+
compressor: true,
|
|
2582
|
+
panning: true,
|
|
2583
|
+
muffling: true
|
|
2584
|
+
};
|
|
2585
|
+
const nameOf = (id) => {
|
|
2586
|
+
const translated = options.translate?.(`sound.${id}`, locale);
|
|
2587
|
+
if (translated) return translated;
|
|
2588
|
+
const bank = engine.registry.bankOf(id);
|
|
2589
|
+
return bank ? `${bank} / ${id}` : id;
|
|
2590
|
+
};
|
|
2591
|
+
const soundOptions = () => engine.registry.soundIds().map((id) => ({ id, name: nameOf(id) }));
|
|
2592
|
+
const loopOptions = () => [{ id: NONE, name: text.options.none }, ...engine.registry.loopIds().map((id) => ({ id, name: nameOf(id) }))];
|
|
2593
|
+
const listener = () => {
|
|
2594
|
+
const camera = host?.camera;
|
|
2595
|
+
const three = host?.THREE;
|
|
2596
|
+
if (typeof camera?.getWorldDirection !== "function" || typeof three?.Vector3 !== "function" || !camera.position) return { position: [0, 0, 0], yaw: 0 };
|
|
2597
|
+
const direction = camera.getWorldDirection(new three.Vector3());
|
|
2598
|
+
return { position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-direction.x, -direction.z) };
|
|
2599
|
+
};
|
|
2600
|
+
const probePosition = () => {
|
|
2601
|
+
const scene = host?.scene;
|
|
2602
|
+
const three = host?.THREE;
|
|
2603
|
+
if (typeof scene?.traverse !== "function" || typeof three?.Vector3 !== "function") return void 0;
|
|
2604
|
+
let found;
|
|
2605
|
+
scene.traverse((object) => {
|
|
2606
|
+
if (found || object.userData?.[PROBE_ID] !== true) return;
|
|
2607
|
+
const at = object.getWorldPosition(new three.Vector3());
|
|
2608
|
+
found = [at.x, at.y, at.z];
|
|
2609
|
+
});
|
|
2610
|
+
return found;
|
|
2611
|
+
};
|
|
2612
|
+
const sourceAt = (ears) => {
|
|
2613
|
+
const probe = state.source === "probe" ? probePosition() : void 0;
|
|
2614
|
+
if (probe) return probe;
|
|
2615
|
+
const bearing = state.bearing * Math.PI / 180;
|
|
2616
|
+
const forward = [-Math.sin(ears.yaw), -Math.cos(ears.yaw)];
|
|
2617
|
+
const right = [Math.cos(ears.yaw), -Math.sin(ears.yaw)];
|
|
2618
|
+
return [ears.position[0] + state.distance * (forward[0] * Math.cos(bearing) + right[0] * Math.sin(bearing)), ears.position[1], ears.position[2] + state.distance * (forward[1] * Math.cos(bearing) + right[1] * Math.sin(bearing))];
|
|
2619
|
+
};
|
|
2620
|
+
const playOptions = () => {
|
|
2621
|
+
const ears = listener();
|
|
2622
|
+
engine.setListener(ears);
|
|
2623
|
+
const at = sourceAt(ears);
|
|
2624
|
+
const distance = Math.hypot(at[0] - ears.position[0], at[2] - ears.position[2]);
|
|
2625
|
+
return { ...state.panning ? { position: at } : { distance }, volume: state.volume, seed: state.seed };
|
|
2626
|
+
};
|
|
2627
|
+
const withMuffling = (id, run) => {
|
|
2628
|
+
const falloff = engine.registry.sound(id)?.falloff;
|
|
2629
|
+
if (state.muffling || !falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return run();
|
|
2630
|
+
engine.registry.patch(id, { falloff: { cutoffK: 0 } });
|
|
2631
|
+
try {
|
|
2632
|
+
return run();
|
|
2633
|
+
} finally {
|
|
2634
|
+
engine.registry.patch(id, { falloff: { cutoffK: falloff.cutoffK } });
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
const play = () => {
|
|
2638
|
+
engine.unlock();
|
|
2639
|
+
const chosen = state.sound;
|
|
2640
|
+
return withMuffling(chosen, () => engine.play(chosen, playOptions()));
|
|
2641
|
+
};
|
|
2642
|
+
const plan = () => {
|
|
2643
|
+
const chosen = state.sound;
|
|
2644
|
+
return withMuffling(chosen, () => engine.plan(chosen, playOptions()));
|
|
2645
|
+
};
|
|
2646
|
+
const sheet = () => {
|
|
2647
|
+
const planned = plan();
|
|
2648
|
+
if (!planned) return `${text.sheet.title}: ${engine.registry.sound(state.sound) ? text.sheet.silent : text.sheet.none}`;
|
|
2649
|
+
const head = `${text.sheet.title}: ${planned.id} (${planned.bus}) ${text.sheet.level} ${planned.level.toFixed(4)} ${text.sheet.distance} ${planned.distance.toFixed(1)} m${planned.pan === void 0 ? "" : ` ${text.sheet.pan} ${planned.pan.toFixed(3)}`} ${planned.length.toFixed(3)} ${text.sheet.length}`;
|
|
2650
|
+
const layers2 = planned.layers.map((layer, index) => {
|
|
2651
|
+
const source = layer.source.kind === "noise" ? `noise@${layer.source.offset.toFixed(4)}` : layer.source.kind === "osc" ? `${layer.source.wave}@${layer.source.frequency.value ?? layer.source.frequency.from}` : layer.source.kind;
|
|
2652
|
+
const filters = (layer.filters?.bank ?? []).map((filter) => `${filter.type}@${(filter.frequency.value ?? filter.frequency.from ?? 0).toFixed(1)}`).join(" ");
|
|
2653
|
+
const envelope = layer.envelope.map((point) => `${point.time.toFixed(3)}:${point.value.toPrecision(3)}`).join(" ");
|
|
2654
|
+
return ` ${text.sheet.layer} ${index + 1}: ${source} at ${layer.at.toFixed(3)} for ${layer.duration.toFixed(3)} ${filters} [${envelope}]`;
|
|
2655
|
+
});
|
|
2656
|
+
return [head, ...layers2].join("\n");
|
|
2657
|
+
};
|
|
2658
|
+
const applyMix = () => {
|
|
2659
|
+
const profile = engine && MIX_PROFILES[state.mix];
|
|
2660
|
+
if (profile) engine.setMix(variantOf(profile, state.reverb, state.compressor));
|
|
2661
|
+
};
|
|
2662
|
+
const setLoop = () => {
|
|
2663
|
+
loop?.stop();
|
|
2664
|
+
loop = void 0;
|
|
2665
|
+
if (state.loop === NONE) return;
|
|
2666
|
+
engine.unlock();
|
|
2667
|
+
loop = engine.loop(state.loop);
|
|
2668
|
+
loop.set({ volume: state.loopLevel, ...state.loopPitch > 0 ? { frequency: state.loopPitch } : {} });
|
|
2669
|
+
};
|
|
2670
|
+
const storm = () => {
|
|
2671
|
+
engine.unlock();
|
|
2672
|
+
const base = playOptions();
|
|
2673
|
+
const chosen = state.sound;
|
|
2674
|
+
for (let index = 0; index < STORM; index++) {
|
|
2675
|
+
const spread = base.position ? { position: [base.position[0] + (index - STORM / 2), base.position[1], base.position[2]] } : {};
|
|
2676
|
+
withMuffling(chosen, () => engine.play(chosen, { ...base, ...spread, seed: state.seed + index }));
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
const onChange = (change) => {
|
|
2680
|
+
if (change.kind === "layer") {
|
|
2681
|
+
if (change.id === "reverb" || change.id === "compressor") {
|
|
2682
|
+
state[change.id] = change.on;
|
|
2683
|
+
applyMix();
|
|
2684
|
+
} else if (change.id === "panning" || change.id === "muffling") state[change.id] = change.on;
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (change.kind === "action") {
|
|
2688
|
+
if (change.id === "play") play();
|
|
2689
|
+
else if (change.id === "voice_storm") storm();
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
const { id, value } = change;
|
|
2693
|
+
if (id === "sound" && typeof value === "string") state.sound = value;
|
|
2694
|
+
else if (id === "distance" && typeof value === "string") state.distance = Number(value);
|
|
2695
|
+
else if (id === "bearing" && typeof value === "number") state.bearing = value;
|
|
2696
|
+
else if (id === "volume" && typeof value === "number") state.volume = value;
|
|
2697
|
+
else if (id === "seed" && typeof value === "number") state.seed = value;
|
|
2698
|
+
else if (id === "source" && typeof value === "string") state.source = value;
|
|
2699
|
+
else if (id === "mix" && typeof value === "string") {
|
|
2700
|
+
state.mix = value;
|
|
2701
|
+
applyMix();
|
|
2702
|
+
} else if (id === "loop" && typeof value === "string") {
|
|
2703
|
+
state.loop = value;
|
|
2704
|
+
setLoop();
|
|
2705
|
+
} else if (id === "loop_level" && typeof value === "number") {
|
|
2706
|
+
state.loopLevel = value;
|
|
2707
|
+
loop?.set({ volume: value });
|
|
2708
|
+
} else if (id === "loop_pitch" && typeof value === "number") {
|
|
2709
|
+
state.loopPitch = value;
|
|
2710
|
+
if (value > 0) loop?.set({ frequency: value });
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const items = () => {
|
|
2714
|
+
const probe = options.probe;
|
|
2715
|
+
if (!probe) return [];
|
|
2716
|
+
return [
|
|
2717
|
+
{
|
|
2718
|
+
id: PROBE_ID,
|
|
2719
|
+
name: text.probe,
|
|
2720
|
+
movable: false,
|
|
2721
|
+
build: (THREE) => {
|
|
2722
|
+
const root = probe(THREE);
|
|
2723
|
+
root.userData = root.userData ?? {};
|
|
2724
|
+
root.userData[PROBE_ID] = true;
|
|
2725
|
+
return root;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
];
|
|
2729
|
+
};
|
|
2730
|
+
const controls = () => [
|
|
2731
|
+
{ kind: "choice", id: "sound", name: text.controls.sound, options: soundOptions(), default: state.sound },
|
|
2732
|
+
{ kind: "choice", id: "source", name: text.controls.source, hint: text.hints.source, options: [{ id: "bench", name: text.options.bench }, { id: "probe", name: text.options.probe }], default: state.source },
|
|
2733
|
+
{ kind: "choice", id: "distance", name: text.controls.distance, hint: text.hints.distance, options: DISTANCES.map((meters) => ({ id: String(meters), name: `${meters} m` })), default: String(state.distance) },
|
|
2734
|
+
{ kind: "number", id: "bearing", name: text.controls.bearing, hint: text.hints.bearing, unit: "\xB0", min: -180, max: 180, step: 15, default: state.bearing },
|
|
2735
|
+
{ kind: "number", id: "volume", name: text.controls.volume, min: 0, max: 1, step: 0.05, default: state.volume },
|
|
2736
|
+
{ kind: "choice", id: "mix", name: text.controls.mix, hint: text.hints.mix, options: Object.keys(MIX_PROFILES).map((id) => ({ id, name: text.mixes[id] ?? id })), default: state.mix },
|
|
2737
|
+
{ kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: state.seed },
|
|
2738
|
+
{ kind: "action", id: "play", name: text.controls.play },
|
|
2739
|
+
{ kind: "action", id: "voice_storm", name: text.controls.voice_storm, hint: text.hints.voice_storm },
|
|
2740
|
+
{ kind: "choice", id: "loop", name: text.controls.loop, options: loopOptions(), default: state.loop },
|
|
2741
|
+
{ kind: "number", id: "loop_level", name: text.controls.loop_level, min: 0, max: 0.6, step: 0.01, default: state.loopLevel },
|
|
2742
|
+
{ kind: "number", id: "loop_pitch", name: text.controls.loop_pitch, hint: text.hints.loop_pitch, unit: "Hz", min: 0, max: 400, step: 5, default: state.loopPitch }
|
|
2743
|
+
];
|
|
2744
|
+
const layers = () => ["reverb", "compressor", "panning", "muffling"].map((id) => ({ id, name: text.layers[id] }));
|
|
2745
|
+
const metrics = () => [
|
|
2746
|
+
{ id: "voices", name: text.metrics.voices, digits: 0, read: () => engine.stats().voices },
|
|
2747
|
+
{ id: "peak", name: text.metrics.peak, digits: 0, max: engine.stats().maxVoices > 0 && Number.isFinite(engine.stats().maxVoices) ? engine.stats().maxVoices : void 0, read: () => engine.stats().peak },
|
|
2748
|
+
{ id: "dropped", name: text.metrics.dropped, digits: 0, read: () => engine.stats().dropped },
|
|
2749
|
+
{ id: "loops", name: text.metrics.loops, digits: 0, read: () => engine.stats().loops },
|
|
2750
|
+
{ id: "context", name: text.metrics.context, read: () => engine.stats().context },
|
|
2751
|
+
{ id: "plan_length", name: text.metrics.plan_length, unit: "s", digits: 2, read: () => plan()?.length ?? 0 },
|
|
2752
|
+
{ id: "plan_level", name: text.metrics.plan_level, digits: 3, read: () => plan()?.level ?? 0 },
|
|
2753
|
+
{ id: "plan_pan", name: text.metrics.plan_pan, digits: 2, read: () => plan()?.pan ?? 0 },
|
|
2754
|
+
...BUS_IDS.map((bus) => ({ id: `level_${bus}`, name: text.metrics[`level_${bus}`], digits: 3, read: () => engine.level(bus) }))
|
|
2755
|
+
];
|
|
2756
|
+
const scenarios = () => [
|
|
2757
|
+
{
|
|
2758
|
+
id: "panning",
|
|
2759
|
+
name: text.scenarios.panning,
|
|
2760
|
+
run: ({ expect }) => {
|
|
2761
|
+
engine.setListener({ position: [0, 0, 0], yaw: 0 });
|
|
2762
|
+
const pan = (at) => engine.plan("bench_ping", { position: at })?.pan ?? Number.NaN;
|
|
2763
|
+
expect(pan([10, 0, 0]) > 0.3, "a sound to the right pans right", { pan: pan([10, 0, 0]) });
|
|
2764
|
+
expect(pan([-10, 0, 0]) < -0.3, "a sound to the left pans left", { pan: pan([-10, 0, 0]) });
|
|
2765
|
+
expect(Math.abs(pan([0, 0, -10])) < 0.05, "a sound ahead stays in the middle", { pan: pan([0, 0, -10]) });
|
|
2766
|
+
engine.setListener({ position: [0, 0, 0], yaw: Math.PI / 2 });
|
|
2767
|
+
expect(pan([0, 0, -10]) > 0.3, "turning the listener left puts what was ahead on the right", { pan: pan([0, 0, -10]) });
|
|
2768
|
+
engine.setListener(listener());
|
|
2769
|
+
}
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
id: "distance",
|
|
2773
|
+
name: text.scenarios.distance,
|
|
2774
|
+
run: ({ expect }) => {
|
|
2775
|
+
const levels = [0, 10, 40, 100].map((meters) => engine.plan("bench_ping", { distance: meters })?.level ?? Number.NaN);
|
|
2776
|
+
expect(levels.every((level, index) => index === 0 || level < levels[index - 1]), "the level falls with the distance", { levels });
|
|
2777
|
+
const far = engine.registry.sound("sfx_pistol") ? engine.plan("sfx_pistol", { distance: 400 }) : null;
|
|
2778
|
+
expect(far === null, "a game noise class is not played too far to be heard");
|
|
2779
|
+
}
|
|
2780
|
+
},
|
|
2781
|
+
{
|
|
2782
|
+
id: "determinism",
|
|
2783
|
+
name: text.scenarios.determinism,
|
|
2784
|
+
run: ({ expect }) => {
|
|
2785
|
+
const a = engine.plan("bench_hiss", { seed: 5 });
|
|
2786
|
+
const b = engine.plan("bench_hiss", { seed: 5 });
|
|
2787
|
+
const c = engine.plan("bench_hiss", { seed: 6 });
|
|
2788
|
+
expect(JSON.stringify(a) === JSON.stringify(b), "the same seed gives the same plan");
|
|
2789
|
+
expect(JSON.stringify(a) !== JSON.stringify(c), "another seed gives another plan");
|
|
2790
|
+
}
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
id: "limiter",
|
|
2794
|
+
name: text.scenarios.limiter,
|
|
2795
|
+
run: ({ expect }) => {
|
|
2796
|
+
const voices = createVoiceManager({ maxVoices: 8 });
|
|
2797
|
+
const request = (group) => voices.request({ ...group ? { group } : {}, priority: 3, gain: 0.5, distance: 5, length: 1 });
|
|
2798
|
+
const grouped = Array.from({ length: STORM }, () => request("undead_voice")).filter((decision) => decision.accepted).length;
|
|
2799
|
+
expect(grouped === 1, "the group cooldown lets one voice of a storm through", { accepted: grouped });
|
|
2800
|
+
const plain = Array.from({ length: STORM }, () => request()).filter((decision) => decision.accepted).length;
|
|
2801
|
+
expect(plain <= 8, "the global cap holds the rest", { accepted: plain });
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
];
|
|
2805
|
+
const area = () => ({
|
|
2806
|
+
placements: options.probe ? [{ item: PROBE_ID, position: PROBE_AT, asleep: true }] : [],
|
|
2807
|
+
view: { eye: [0, 1.7, 3], target: [0, 1.4, -10] }
|
|
2808
|
+
});
|
|
2809
|
+
const module = {
|
|
2810
|
+
id: "audio_area",
|
|
2811
|
+
name: text.name,
|
|
2812
|
+
ground: { surface: "concrete_slab" },
|
|
2813
|
+
group: text.group,
|
|
2814
|
+
get engine() {
|
|
2815
|
+
return engine;
|
|
2816
|
+
},
|
|
2817
|
+
items: () => items(),
|
|
2818
|
+
area: () => area(),
|
|
2819
|
+
controls: () => controls(),
|
|
2820
|
+
layers: () => layers(),
|
|
2821
|
+
metrics: () => metrics(),
|
|
2822
|
+
scenarios: () => scenarios(),
|
|
2823
|
+
sheet,
|
|
2824
|
+
play,
|
|
2825
|
+
attach(lent) {
|
|
2826
|
+
host = lent;
|
|
2827
|
+
const shared = lent.audio?.engine;
|
|
2828
|
+
if (shared && shared !== engine) {
|
|
2829
|
+
for (const bank of banks) shared.register(bank);
|
|
2830
|
+
engine = shared;
|
|
2831
|
+
}
|
|
2832
|
+
for (const bus of BUS_IDS) {
|
|
2833
|
+
engine.setBus(bus, {});
|
|
2834
|
+
engine.level(bus);
|
|
2835
|
+
}
|
|
2836
|
+
lent.bench?.patch("sound", { options: soundOptions() });
|
|
2837
|
+
lent.bench?.patch("loop", { options: loopOptions() });
|
|
2838
|
+
const off = lent.bench?.onChange(onChange);
|
|
2839
|
+
const offFrame = lent.onFrame(() => {
|
|
2840
|
+
if (!shared) engine.setListener(listener());
|
|
2841
|
+
});
|
|
2842
|
+
return () => {
|
|
2843
|
+
off?.();
|
|
2844
|
+
offFrame();
|
|
2845
|
+
loop?.stop();
|
|
2846
|
+
loop = void 0;
|
|
2847
|
+
host = null;
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
};
|
|
2851
|
+
return module;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// src/catalog.ts
|
|
2855
|
+
var SFX_CATEGORIES = ["armas", "impactos", "explosoes-fogo", "personagem", "veiculos", "itens-portas", "ambiente"];
|
|
2856
|
+
var entries = (category, list) => list.map(([id, priority, current]) => ({ id, category, priority, ...current ? { current } : {} }));
|
|
2857
|
+
var SFX_CATALOG = [
|
|
2858
|
+
...entries("armas", [
|
|
2859
|
+
["pistol_22lr", 2, "sfx_pistol"],
|
|
2860
|
+
["pistol_9mm", 1, "sfx_pistol"],
|
|
2861
|
+
["pistol_45", 1, "sfx_pistol"],
|
|
2862
|
+
["revolver_38_357", 2, "sfx_pistol"],
|
|
2863
|
+
["revolver_44_50ae", 3, "sfx_pistol"],
|
|
2864
|
+
["smg_9mm_auto", 1, "sfx_pistol"],
|
|
2865
|
+
["shotgun_12", 1, "sfx_shotgun"],
|
|
2866
|
+
["rifle_5_56", 1, "sfx_rifle"],
|
|
2867
|
+
["rifle_7_62x39", 1, "sfx_rifle"],
|
|
2868
|
+
["rifle_auto_burst", 1, "sfx_rifle"],
|
|
2869
|
+
["rifle_308_3006", 2, "sfx_rifle"],
|
|
2870
|
+
["sniper_338_300wm", 2, "sfx_rifle"],
|
|
2871
|
+
["antimateriel_50bmg", 3, "sfx_rifle"],
|
|
2872
|
+
["suppressed_pistol", 2],
|
|
2873
|
+
["suppressed_rifle", 2],
|
|
2874
|
+
["gunshot_far_pistol", 1],
|
|
2875
|
+
["gunshot_far_rifle", 1],
|
|
2876
|
+
["gunshot_far_shotgun", 1],
|
|
2877
|
+
["gunshot_far_heavy", 2],
|
|
2878
|
+
["gunshot_tail_indoor", 1],
|
|
2879
|
+
["gunshot_tail_outdoor", 1],
|
|
2880
|
+
["bullet_crack_by", 2],
|
|
2881
|
+
["bullet_whiz", 2],
|
|
2882
|
+
["ricochet_metal", 2, "ping"],
|
|
2883
|
+
["dry_fire", 1],
|
|
2884
|
+
["mag_out", 1],
|
|
2885
|
+
["mag_in", 1],
|
|
2886
|
+
["slide_pull_release", 1],
|
|
2887
|
+
["pump_cycle", 1],
|
|
2888
|
+
["casing_drop_concreto_pistol", 1],
|
|
2889
|
+
["casing_drop_concreto_rifle", 1],
|
|
2890
|
+
["casing_drop_concreto_shell", 1],
|
|
2891
|
+
["casing_drop_madeira_pistol", 1],
|
|
2892
|
+
["casing_drop_madeira_rifle", 1],
|
|
2893
|
+
["casing_drop_madeira_shell", 1],
|
|
2894
|
+
["casing_drop_metal_pistol", 1],
|
|
2895
|
+
["casing_drop_metal_rifle", 1],
|
|
2896
|
+
["casing_drop_metal_shell", 1],
|
|
2897
|
+
["casing_drop_terra_pistol", 1],
|
|
2898
|
+
["casing_drop_terra_rifle", 1],
|
|
2899
|
+
["casing_drop_terra_shell", 1],
|
|
2900
|
+
["rocket_fire", 2],
|
|
2901
|
+
["smg_45_auto", 2, "sfx_pistol"],
|
|
2902
|
+
["weapon_draw", 2],
|
|
2903
|
+
["suppressor_attach", 3],
|
|
2904
|
+
["suppressor_detach", 3]
|
|
2905
|
+
]),
|
|
2906
|
+
...entries("explosoes-fogo", [
|
|
2907
|
+
["explosion_grenade", 1, "boom"],
|
|
2908
|
+
["explosion_big", 1, "boom_big"],
|
|
2909
|
+
["explosion_car", 2, "boom_big"],
|
|
2910
|
+
["explosion_far", 1],
|
|
2911
|
+
["debris_rain_stone", 1],
|
|
2912
|
+
["molotov_break_ignite", 1, "glass_shatter"],
|
|
2913
|
+
["fire_ignite", 1, "hiss"],
|
|
2914
|
+
["fire_loop_small", 1],
|
|
2915
|
+
["fire_loop_large", 1],
|
|
2916
|
+
["fire_extinguish", 2, "hiss"],
|
|
2917
|
+
["gas_leak_hiss", 2],
|
|
2918
|
+
["structure_collapse", 2, "crumble"],
|
|
2919
|
+
["grenade_bounce", 1],
|
|
2920
|
+
["c4_plant", 2],
|
|
2921
|
+
["c4_beep", 2],
|
|
2922
|
+
["c4_disarm", 2]
|
|
2923
|
+
]),
|
|
2924
|
+
...entries("personagem", [
|
|
2925
|
+
["footstep_concrete_walk", 1, "sfx_step"],
|
|
2926
|
+
["footstep_wood_walk", 1, "sfx_step"],
|
|
2927
|
+
["footstep_dirt_walk", 1, "sfx_step"],
|
|
2928
|
+
["footstep_grass_walk", 1, "sfx_step"],
|
|
2929
|
+
["footstep_gravel_walk", 1, "sfx_step"],
|
|
2930
|
+
["land_concrete", 1, "sfx_step"],
|
|
2931
|
+
["punch_swing", 1],
|
|
2932
|
+
["punch_hit_body", 1, "sfx_flesh"],
|
|
2933
|
+
["kick_hit", 1, "sfx_flesh"],
|
|
2934
|
+
["knife_stab", 1],
|
|
2935
|
+
["blunt_hit", 1],
|
|
2936
|
+
["bite", 1, "sfx_flesh"],
|
|
2937
|
+
["body_fall", 1],
|
|
2938
|
+
["player_hurt", 2],
|
|
2939
|
+
["footstep_tile_walk", 1, "sfx_step"],
|
|
2940
|
+
["footstep_water_walk", 1, "sfx_step"],
|
|
2941
|
+
["footstep_grate_walk", 2, "sfx_step"],
|
|
2942
|
+
["footstep_duct_walk", 3, "sfx_step"],
|
|
2943
|
+
["ladder_climb", 2],
|
|
2944
|
+
["swim", 2],
|
|
2945
|
+
["zombie_groan", 1],
|
|
2946
|
+
["zombie_alert", 1],
|
|
2947
|
+
["zombie_attack", 1],
|
|
2948
|
+
["zombie_pain", 1],
|
|
2949
|
+
["zombie_claw_hit", 1, "sfx_flesh"],
|
|
2950
|
+
["zombie_claw_miss", 2]
|
|
2951
|
+
]),
|
|
2952
|
+
...entries("veiculos", [
|
|
2953
|
+
["engine_loop_car", 1],
|
|
2954
|
+
["engine_start", 1],
|
|
2955
|
+
["crash_light", 1, "crash"],
|
|
2956
|
+
["crash_heavy", 1, "crash"],
|
|
2957
|
+
["car_door_open", 1, "latch"],
|
|
2958
|
+
["car_door_close", 1, "shut_metal"],
|
|
2959
|
+
["horn_car", 2, "horn"],
|
|
2960
|
+
["tire_skid_asphalt", 2]
|
|
2961
|
+
]),
|
|
2962
|
+
...entries("itens-portas", [
|
|
2963
|
+
["item_pickup_metal", 1],
|
|
2964
|
+
["can_open", 1],
|
|
2965
|
+
["can_crush", 1],
|
|
2966
|
+
["door_wood_open", 1, "latch"],
|
|
2967
|
+
["door_wood_close", 1, "shut_wood"],
|
|
2968
|
+
["door_creak", 1, "creak_wood"],
|
|
2969
|
+
["door_locked_rattle", 1, "rattle"],
|
|
2970
|
+
["drawer_open", 2, "slide"],
|
|
2971
|
+
["fridge_close", 2, "shut_metal"]
|
|
2972
|
+
]),
|
|
2973
|
+
...entries("ambiente", [
|
|
2974
|
+
["rain_loop", 2],
|
|
2975
|
+
["wind_loop", 2],
|
|
2976
|
+
["thunder_near", 2],
|
|
2977
|
+
["thunder_far", 2],
|
|
2978
|
+
["water_splash", 2],
|
|
2979
|
+
["helicopter_loop", 3]
|
|
2980
|
+
])
|
|
2981
|
+
];
|
|
2982
|
+
var SFX_MATERIALS = [
|
|
2983
|
+
["steel", "metal"],
|
|
2984
|
+
["aluminium", "metal"],
|
|
2985
|
+
["wood", "wood"],
|
|
2986
|
+
["plywood", "wood"],
|
|
2987
|
+
["mdf", "wood"],
|
|
2988
|
+
["glass", "glass"],
|
|
2989
|
+
["tempered_glass", "glass"],
|
|
2990
|
+
["ceramic", "glass"],
|
|
2991
|
+
["concrete", "masonry"],
|
|
2992
|
+
["brick", "masonry"],
|
|
2993
|
+
["plaster", "masonry"],
|
|
2994
|
+
["plastic", "soft"],
|
|
2995
|
+
["fabric", "soft"],
|
|
2996
|
+
["paper", "soft"],
|
|
2997
|
+
["foliage", "soft"],
|
|
2998
|
+
["soil", "soft"],
|
|
2999
|
+
["flesh", "soft"]
|
|
3000
|
+
];
|
|
3001
|
+
var SFX_BLOWS = ["bullet", "blunt", "blade", "break", "drag"];
|
|
3002
|
+
var CURRENT_BY_GROUP = {
|
|
3003
|
+
metal: { bullet: "clang", blunt: "clang", blade: "clang", break: "clang", drag: "drag_metal" },
|
|
3004
|
+
wood: { bullet: "wood_hit", blunt: "wood_hit", blade: "wood_hit", break: "crack", drag: "drag_wood" },
|
|
3005
|
+
glass: { bullet: "ping", blunt: "ping", break: "glass_shatter", drag: "drag_glass" },
|
|
3006
|
+
masonry: { bullet: "chip", blunt: "chip", blade: "chip", break: "crumble", drag: "drag_masonry" },
|
|
3007
|
+
soft: { bullet: "thud", blunt: "thud", blade: "thud", break: "thud", drag: "drag_soft" }
|
|
3008
|
+
};
|
|
3009
|
+
var impactSoundId = (material, blow) => `impact_${material}_${blow}`;
|
|
3010
|
+
var SFX_IMPACTS = SFX_MATERIALS.flatMap(
|
|
3011
|
+
([material, group]) => SFX_BLOWS.map((blow) => {
|
|
3012
|
+
const current = CURRENT_BY_GROUP[group]?.[blow];
|
|
3013
|
+
return { id: impactSoundId(material, blow), category: "impactos", priority: ["steel", "wood", "glass", "concrete", "brick", "soil", "flesh"].includes(material) && blow === "bullet" ? 1 : 2, ...current ? { current } : {} };
|
|
3014
|
+
})
|
|
3015
|
+
);
|
|
3016
|
+
function sfxState(entry, has) {
|
|
3017
|
+
if (has.sample(entry.id)) return "file";
|
|
3018
|
+
if (entry.current && has.sound(entry.current) || has.sound(entry.id)) return "synth";
|
|
3019
|
+
return "missing";
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/measure/calibration.ts
|
|
3023
|
+
var CALIBRATION_ID = "calibration_shot";
|
|
3024
|
+
var CALIBRATION_RATE = 48e3;
|
|
3025
|
+
function encodeWav(samples, sampleRate = CALIBRATION_RATE) {
|
|
3026
|
+
const bytes = new ArrayBuffer(44 + samples.length * 2);
|
|
3027
|
+
const view = new DataView(bytes);
|
|
3028
|
+
const text = (at, value) => {
|
|
3029
|
+
for (let index = 0; index < value.length; index++) view.setUint8(at + index, value.charCodeAt(index));
|
|
3030
|
+
};
|
|
3031
|
+
text(0, "RIFF");
|
|
3032
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
3033
|
+
text(8, "WAVE");
|
|
3034
|
+
text(12, "fmt ");
|
|
3035
|
+
view.setUint32(16, 16, true);
|
|
3036
|
+
view.setUint16(20, 1, true);
|
|
3037
|
+
view.setUint16(22, 1, true);
|
|
3038
|
+
view.setUint32(24, sampleRate, true);
|
|
3039
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
3040
|
+
view.setUint16(32, 2, true);
|
|
3041
|
+
view.setUint16(34, 16, true);
|
|
3042
|
+
text(36, "data");
|
|
3043
|
+
view.setUint32(40, samples.length * 2, true);
|
|
3044
|
+
for (let index = 0; index < samples.length; index++) {
|
|
3045
|
+
const value = Math.max(-1, Math.min(1, samples[index]));
|
|
3046
|
+
view.setInt16(44 + index * 2, value < 0 ? value * 32768 : value * 32767, true);
|
|
3047
|
+
}
|
|
3048
|
+
return bytes;
|
|
3049
|
+
}
|
|
3050
|
+
function shotSamples(shot, sampleRate = CALIBRATION_RATE) {
|
|
3051
|
+
const random = createRng(shot.seed);
|
|
3052
|
+
const length = Math.round(shot.seconds * sampleRate);
|
|
3053
|
+
const samples = new Float32Array(length);
|
|
3054
|
+
let low = 0;
|
|
3055
|
+
let phase = 0;
|
|
3056
|
+
const attack = Math.max(1, Math.round((shot.attack ?? 0) * sampleRate));
|
|
3057
|
+
for (let index = 0; index < length; index++) {
|
|
3058
|
+
const time = index / sampleRate;
|
|
3059
|
+
const envelope = Math.exp(-time / shot.decay) * Math.min(1, (index + 1) / attack);
|
|
3060
|
+
phase += 2 * Math.PI * shot.thump * (1 + 0.6 * Math.exp(-time * 30)) / sampleRate;
|
|
3061
|
+
const noise = random() * 2 - 1;
|
|
3062
|
+
low += shot.brightness * (noise - low);
|
|
3063
|
+
const fade = Math.min(1, (length - index) / (0.01 * sampleRate));
|
|
3064
|
+
samples[index] = shot.gain * envelope * fade * (0.55 * Math.sin(phase) + shot.noise * low);
|
|
3065
|
+
}
|
|
3066
|
+
return samples;
|
|
3067
|
+
}
|
|
3068
|
+
var TAKES = {
|
|
3069
|
+
"close_1.wav": { seconds: 0.4, thump: 150, noise: 1, decay: 0.06, brightness: 0.9, gain: 0.85, seed: 1 },
|
|
3070
|
+
"close_2.wav": { seconds: 0.4, thump: 160, noise: 1, decay: 0.065, brightness: 0.85, gain: 0.85, seed: 2 },
|
|
3071
|
+
"close_3.wav": { seconds: 0.4, thump: 142, noise: 1, decay: 0.055, brightness: 0.95, gain: 0.85, seed: 3 },
|
|
3072
|
+
"close_4.wav": { seconds: 0.4, thump: 170, noise: 1, decay: 0.07, brightness: 0.8, gain: 0.85, seed: 4 },
|
|
3073
|
+
"far_1.wav": { seconds: 0.9, thump: 90, noise: 0.8, decay: 0.2, brightness: 0.08, gain: 0.7, seed: 5, attack: 4e-3 },
|
|
3074
|
+
"far_2.wav": { seconds: 0.9, thump: 95, noise: 0.8, decay: 0.22, brightness: 0.07, gain: 0.7, seed: 6, attack: 4e-3 },
|
|
3075
|
+
"light_1.wav": { seconds: 0.25, thump: 220, noise: 0.6, decay: 0.03, brightness: 0.6, gain: 0.35, seed: 7 },
|
|
3076
|
+
"light_2.wav": { seconds: 0.25, thump: 230, noise: 0.6, decay: 0.03, brightness: 0.55, gain: 0.35, seed: 8 },
|
|
3077
|
+
"heavy_1.wav": { seconds: 0.6, thump: 110, noise: 1.2, decay: 0.11, brightness: 0.95, gain: 0.95, seed: 9 },
|
|
3078
|
+
"heavy_2.wav": { seconds: 0.6, thump: 105, noise: 1.2, decay: 0.12, brightness: 0.9, gain: 0.95, seed: 10 },
|
|
3079
|
+
"tail_in.wav": { seconds: 0.5, thump: 0, noise: 1, decay: 0.08, brightness: 0.4, gain: 0.25, seed: 11, attack: 0.01 },
|
|
3080
|
+
"tail_out.wav": { seconds: 2, thump: 0, noise: 1, decay: 0.45, brightness: 0.25, gain: 0.2, seed: 12, attack: 0.02 },
|
|
3081
|
+
"loop_1.wav": { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.5, gain: 0.5, seed: 13, attack: 0.01 },
|
|
3082
|
+
"loop_2.wav": { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.45, gain: 0.5, seed: 14, attack: 0.01 },
|
|
3083
|
+
"loop_3.wav": { seconds: 0.6, thump: 0, noise: 1, decay: 100, brightness: 0.55, gain: 0.5, seed: 15, attack: 0.01 }
|
|
3084
|
+
};
|
|
3085
|
+
var PREFIX = "calibration/";
|
|
3086
|
+
var url = (name) => `${PREFIX}${name}`;
|
|
3087
|
+
var CALIBRATION_SOUND = {
|
|
3088
|
+
id: CALIBRATION_ID,
|
|
3089
|
+
bus: "weapons",
|
|
3090
|
+
close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })),
|
|
3091
|
+
far: ["far_1.wav", "far_2.wav"].map((name) => ({ url: url(name) })),
|
|
3092
|
+
tiers: [
|
|
3093
|
+
{ name: "light", from: 0, close: ["light_1.wav", "light_2.wav"].map((name) => ({ url: url(name) })) },
|
|
3094
|
+
{ name: "medium", from: 0.34, close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })) },
|
|
3095
|
+
{ name: "heavy", from: 0.67, close: ["heavy_1.wav", "heavy_2.wav"].map((name) => ({ url: url(name) })) }
|
|
3096
|
+
],
|
|
3097
|
+
tail: { indoor: [{ url: url("tail_in.wav") }], outdoor: [{ url: url("tail_out.wav") }] },
|
|
3098
|
+
pitch: 0.04,
|
|
3099
|
+
choke: { voices: 2 },
|
|
3100
|
+
fallback: "sfx_pistol",
|
|
3101
|
+
sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
|
|
3102
|
+
};
|
|
3103
|
+
var CALIBRATION_LOOP_ID = "calibration_loop";
|
|
3104
|
+
var CALIBRATION_LOOP_SOUND = {
|
|
3105
|
+
id: CALIBRATION_LOOP_ID,
|
|
3106
|
+
bus: "ambience",
|
|
3107
|
+
close: ["loop_1.wav", "loop_2.wav", "loop_3.wav"].map((name) => ({ url: url(name) })),
|
|
3108
|
+
loop: { crossfade: 0.15 },
|
|
3109
|
+
sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
|
|
3110
|
+
};
|
|
3111
|
+
var isCalibrationUrl = (url2) => url2.startsWith(PREFIX);
|
|
3112
|
+
function calibrationLoader(next) {
|
|
3113
|
+
const made = /* @__PURE__ */ new Map();
|
|
3114
|
+
return async (requested) => {
|
|
3115
|
+
if (!requested.startsWith(PREFIX)) {
|
|
3116
|
+
if (next) return next(requested);
|
|
3117
|
+
throw new Error(`not a calibration file: ${requested}`);
|
|
3118
|
+
}
|
|
3119
|
+
const name = requested.slice(PREFIX.length);
|
|
3120
|
+
const shot = TAKES[name];
|
|
3121
|
+
if (!shot) throw new Error(`no calibration take ${name}`);
|
|
3122
|
+
let bytes = made.get(name);
|
|
3123
|
+
if (!bytes) {
|
|
3124
|
+
bytes = encodeWav(shotSamples(shot));
|
|
3125
|
+
made.set(name, bytes);
|
|
3126
|
+
}
|
|
3127
|
+
return bytes.slice(0);
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// src/measure/analyze.ts
|
|
3132
|
+
var dbOf = (amplitude) => amplitude > 0 ? 20 * Math.log10(amplitude) : -Infinity;
|
|
3133
|
+
var clampIndex = (channels, index) => Math.max(0, Math.min(channels[0]?.length ?? 0, Math.round(index)));
|
|
3134
|
+
function peakOf(channels, start = 0, end3 = Number.POSITIVE_INFINITY) {
|
|
3135
|
+
let peak = 0;
|
|
3136
|
+
const from = clampIndex(channels, start);
|
|
3137
|
+
for (const channel of channels) {
|
|
3138
|
+
const to = Math.min(channel.length, end3);
|
|
3139
|
+
for (let index = from; index < to; index++) {
|
|
3140
|
+
const value = Math.abs(channel[index]);
|
|
3141
|
+
if (value > peak) peak = value;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
return peak;
|
|
3145
|
+
}
|
|
3146
|
+
function peakIndex(channels, start = 0, end3 = Number.POSITIVE_INFINITY) {
|
|
3147
|
+
let peak = -1;
|
|
3148
|
+
let at = -1;
|
|
3149
|
+
const from = clampIndex(channels, start);
|
|
3150
|
+
for (const channel of channels) {
|
|
3151
|
+
const to = Math.min(channel.length, end3);
|
|
3152
|
+
for (let index = from; index < to; index++) {
|
|
3153
|
+
const value = Math.abs(channel[index]);
|
|
3154
|
+
if (value > peak) {
|
|
3155
|
+
peak = value;
|
|
3156
|
+
at = index;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return at;
|
|
3161
|
+
}
|
|
3162
|
+
function onsetIndex(channels, start = 0, threshold = 1e-3) {
|
|
3163
|
+
const length = channels[0]?.length ?? 0;
|
|
3164
|
+
for (let index = clampIndex(channels, start); index < length; index++) {
|
|
3165
|
+
for (const channel of channels) if (Math.abs(channel[index]) >= threshold) return index;
|
|
3166
|
+
}
|
|
3167
|
+
return -1;
|
|
3168
|
+
}
|
|
3169
|
+
function rmsDb(channels, start, end3) {
|
|
3170
|
+
let sum = 0;
|
|
3171
|
+
let count = 0;
|
|
3172
|
+
const from = clampIndex(channels, start);
|
|
3173
|
+
for (const channel of channels) {
|
|
3174
|
+
const to = Math.min(channel.length, Math.round(end3));
|
|
3175
|
+
for (let index = from; index < to; index++) {
|
|
3176
|
+
const value = channel[index];
|
|
3177
|
+
sum += value * value;
|
|
3178
|
+
count++;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return count === 0 ? -Infinity : dbOf(Math.sqrt(sum / count));
|
|
3182
|
+
}
|
|
3183
|
+
function centroidHz(channels, sampleRate, start, size = 2048) {
|
|
3184
|
+
const from = clampIndex(channels, start);
|
|
3185
|
+
const frame = new Float64Array(size);
|
|
3186
|
+
for (const channel of channels) {
|
|
3187
|
+
for (let index = 0; index < size; index++) frame[index] = frame[index] + (channel[from + index] ?? 0);
|
|
3188
|
+
}
|
|
3189
|
+
for (let index = 0; index < size; index++) frame[index] = frame[index] * (0.5 - 0.5 * Math.cos(2 * Math.PI * index / (size - 1)));
|
|
3190
|
+
let weighted = 0;
|
|
3191
|
+
let total = 0;
|
|
3192
|
+
const bins = size / 2;
|
|
3193
|
+
for (let bin = 1; bin < bins; bin++) {
|
|
3194
|
+
let re = 0;
|
|
3195
|
+
let im = 0;
|
|
3196
|
+
const step = 2 * Math.PI * bin / size;
|
|
3197
|
+
for (let index = 0; index < size; index++) {
|
|
3198
|
+
const value = frame[index];
|
|
3199
|
+
re += value * Math.cos(step * index);
|
|
3200
|
+
im -= value * Math.sin(step * index);
|
|
3201
|
+
}
|
|
3202
|
+
const magnitude = Math.hypot(re, im);
|
|
3203
|
+
weighted += magnitude * (bin * sampleRate / size);
|
|
3204
|
+
total += magnitude;
|
|
3205
|
+
}
|
|
3206
|
+
return total > 0 ? weighted / total : 0;
|
|
3207
|
+
}
|
|
3208
|
+
function balanceOf(left, right, start, end3) {
|
|
3209
|
+
let l = 0;
|
|
3210
|
+
let r = 0;
|
|
3211
|
+
const to = Math.min(left.length, right.length, Math.round(end3));
|
|
3212
|
+
for (let index = Math.max(0, Math.round(start)); index < to; index++) {
|
|
3213
|
+
l += left[index] ** 2;
|
|
3214
|
+
r += right[index] ** 2;
|
|
3215
|
+
}
|
|
3216
|
+
return l + r > 0 ? (r - l) / (r + l) : 0;
|
|
3217
|
+
}
|
|
3218
|
+
function ringSeconds(channels, sampleRate, start, floorDb = -60) {
|
|
3219
|
+
const window = Math.max(1, Math.round(sampleRate * 0.01));
|
|
3220
|
+
const length = channels[0]?.length ?? 0;
|
|
3221
|
+
let last = clampIndex(channels, start);
|
|
3222
|
+
for (let index = last; index < length; index += window) {
|
|
3223
|
+
if (dbOf(peakOf(channels, index, index + window)) > floorDb) last = index + window;
|
|
3224
|
+
}
|
|
3225
|
+
return (last - start) / sampleRate;
|
|
3226
|
+
}
|
|
3227
|
+
function countHits(channels, sampleRate, threshold) {
|
|
3228
|
+
const window = Math.max(1, Math.round(sampleRate * 2e-3));
|
|
3229
|
+
const length = channels[0]?.length ?? 0;
|
|
3230
|
+
const envelope = [];
|
|
3231
|
+
for (let index = 0; index < length; index += window) envelope.push(peakOf(channels, index, index + window));
|
|
3232
|
+
const history = 10;
|
|
3233
|
+
const refractory = Math.round(0.03 / 2e-3);
|
|
3234
|
+
let hits = 0;
|
|
3235
|
+
let quietUntil = -1;
|
|
3236
|
+
for (let index = 0; index < envelope.length; index++) {
|
|
3237
|
+
const level = envelope[index];
|
|
3238
|
+
if (index < quietUntil || level < threshold) continue;
|
|
3239
|
+
let sum = 0;
|
|
3240
|
+
for (let back = Math.max(0, index - history); back < index; back++) sum += envelope[back];
|
|
3241
|
+
const before = index === 0 ? 0 : sum / Math.min(history, index);
|
|
3242
|
+
if (level >= 2 * before) {
|
|
3243
|
+
hits++;
|
|
3244
|
+
quietUntil = index + refractory;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return hits;
|
|
3248
|
+
}
|
|
3249
|
+
function endDropDb(channels, sampleRate, seconds = 5e-3) {
|
|
3250
|
+
const length = channels[0]?.length ?? 0;
|
|
3251
|
+
const whole = peakOf(channels);
|
|
3252
|
+
if (whole === 0) return -Infinity;
|
|
3253
|
+
return dbOf(peakOf(channels, length - Math.round(sampleRate * seconds), length) / whole);
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// src/measure/criteria.ts
|
|
3257
|
+
var CRITERIA = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10"];
|
|
3258
|
+
var CRITERIA_TARGETS = {
|
|
3259
|
+
/** C1: event to the first sample, and onset to the peak (one 60 fps frame). */
|
|
3260
|
+
syncMs: 20,
|
|
3261
|
+
peakAfterOnsetMs: 1e3 / 60,
|
|
3262
|
+
/** C3: distances measured, metres. */
|
|
3263
|
+
distances: [1, 10, 50, 200],
|
|
3264
|
+
/** C5: plays, distinct takes and the pitch range (fraction). */
|
|
3265
|
+
plays: 10,
|
|
3266
|
+
minTakes: 4,
|
|
3267
|
+
pitch: [0.03, 0.05],
|
|
3268
|
+
/** C6: the highest peak, dBFS. */
|
|
3269
|
+
peakDb: -1,
|
|
3270
|
+
/** C8: the end of each file against its peak, dB. */
|
|
3271
|
+
endDropDb: -40,
|
|
3272
|
+
/** C9: a burst of rapid fire. */
|
|
3273
|
+
burst: 20,
|
|
3274
|
+
burstSpacing: 0.06
|
|
3275
|
+
};
|
|
3276
|
+
var EVENT_AT = 0.2;
|
|
3277
|
+
var offlineFactory = (options) => options.createOfflineContext ?? ((channels, length, sampleRate) => {
|
|
3278
|
+
const Constructor = globalThis.OfflineAudioContext;
|
|
3279
|
+
if (!Constructor) throw new Error("no OfflineAudioContext here: the harness runs in a browser");
|
|
3280
|
+
return new Constructor(channels, length, sampleRate);
|
|
3281
|
+
});
|
|
3282
|
+
async function render(options, seconds, act, engineExtras = {}) {
|
|
3283
|
+
const sampleRate = options.sampleRate ?? 48e3;
|
|
3284
|
+
const context = offlineFactory(options)(2, Math.ceil(seconds * sampleRate), sampleRate);
|
|
3285
|
+
const events = [];
|
|
3286
|
+
const engine = createAudioEngine({
|
|
3287
|
+
banks: options.banks,
|
|
3288
|
+
createContext: () => context,
|
|
3289
|
+
samples: { loader: options.loader, autoLoad: false },
|
|
3290
|
+
onVoiceStart: (event) => events.push(event),
|
|
3291
|
+
...options.mix ? { mix: options.mix } : {},
|
|
3292
|
+
...engineExtras.occlusion ? { occlusion: engineExtras.occlusion } : {},
|
|
3293
|
+
...engineExtras.environment ? { environment: engineExtras.environment } : {}
|
|
3294
|
+
});
|
|
3295
|
+
const ids = [.../* @__PURE__ */ new Set([options.sound, ...(options.actions ?? []).map((action) => action.play), ...options.loudnessOrder ?? []])].filter((id) => engine.registry.sample(id));
|
|
3296
|
+
const report = await engine.loadSamples(ids);
|
|
3297
|
+
if (report.failed.length > 0) throw new Error(`files did not load: ${report.failed.map((entry) => `${entry.url} (${entry.error})`).join(", ")}`);
|
|
3298
|
+
const handles = act(engine);
|
|
3299
|
+
const buffer = await context.startRendering();
|
|
3300
|
+
const channels = Array.from({ length: buffer.numberOfChannels }, (_, index) => buffer.getChannelData(index));
|
|
3301
|
+
return { channels, sampleRate, events, handles };
|
|
3302
|
+
}
|
|
3303
|
+
var ms = (seconds) => `${(seconds * 1e3).toFixed(1)} ms`;
|
|
3304
|
+
var db = (value) => Number.isFinite(value) ? `${value.toFixed(1)} dB` : "\u2212\u221E";
|
|
3305
|
+
var decreasing = (values) => values.every((value, index) => index === 0 || value < values[index - 1]);
|
|
3306
|
+
async function c1(options) {
|
|
3307
|
+
const result = await render(options, 1.2, (engine) => engine.play(options.sound, { distance: 1, delay: EVENT_AT }) ? 1 : 0);
|
|
3308
|
+
const onset = onsetIndex(result.channels, 0, 1e-3);
|
|
3309
|
+
const event = result.events.find((entry) => entry.id === options.sound);
|
|
3310
|
+
if (onset < 0 || !event) return { id: "C1", pass: false, value: "sem som", target: `\u2264 ${CRITERIA_TARGETS.syncMs} ms` };
|
|
3311
|
+
const delay = onset / result.sampleRate - EVENT_AT + (event.startAt - event.requestedAt - EVENT_AT) + (options.baseLatency ?? 0);
|
|
3312
|
+
const peak = peakIndex(result.channels, onset, onset + Math.round(result.sampleRate * 0.25));
|
|
3313
|
+
const toPeak = (peak - onset) / result.sampleRate;
|
|
3314
|
+
const pass = delay <= CRITERIA_TARGETS.syncMs / 1e3 && toPeak <= CRITERIA_TARGETS.peakAfterOnsetMs / 1e3;
|
|
3315
|
+
return {
|
|
3316
|
+
id: "C1",
|
|
3317
|
+
pass,
|
|
3318
|
+
value: `${ms(delay)}, pico +${ms(toPeak)}`,
|
|
3319
|
+
target: `\u2264 ${CRITERIA_TARGETS.syncMs} ms; pico no quadro do contato`,
|
|
3320
|
+
data: { delay, toPeak, baseLatency: options.baseLatency ?? 0, outputLatency: options.outputLatency ?? 0 }
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
async function c2(options) {
|
|
3324
|
+
const actions = options.actions ?? [{ action: options.sound, play: options.sound, expect: options.sound }];
|
|
3325
|
+
const result = await render(options, 0.5, (engine) => {
|
|
3326
|
+
let played = 0;
|
|
3327
|
+
for (const action of actions) if (engine.play(action.play, { distance: 1, ...action.options })) played++;
|
|
3328
|
+
return played;
|
|
3329
|
+
});
|
|
3330
|
+
const wrong = actions.filter((action, index) => {
|
|
3331
|
+
const event = result.events[index];
|
|
3332
|
+
return !event || (event.fallbackOf ?? event.id) !== action.expect || event.kind !== "sample";
|
|
3333
|
+
});
|
|
3334
|
+
return {
|
|
3335
|
+
id: "C2",
|
|
3336
|
+
pass: wrong.length === 0,
|
|
3337
|
+
value: `${actions.length - wrong.length}/${actions.length} certas`,
|
|
3338
|
+
target: "100% das a\xE7\xF5es batem com a tabela (arquivo, n\xE3o o substituto)",
|
|
3339
|
+
data: { wrong: wrong.map((action) => action.action), events: result.events.map((event) => `${event.kind}:${event.id}`) }
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
async function c3(options, sound) {
|
|
3343
|
+
const levels = [];
|
|
3344
|
+
const brightness = [];
|
|
3345
|
+
for (const distance of CRITERIA_TARGETS.distances) {
|
|
3346
|
+
const result = await render(options, 1.2, (engine) => engine.play(options.sound, { position: [0, 0, -distance], seed: 1, delay: EVENT_AT }) ? 1 : 0);
|
|
3347
|
+
const onset = onsetIndex(result.channels, 0, 1e-5);
|
|
3348
|
+
levels.push(onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3));
|
|
3349
|
+
brightness.push(onset < 0 ? 0 : centroidHz(result.channels, result.sampleRate, onset));
|
|
3350
|
+
}
|
|
3351
|
+
const far = planSample(sound, { random: createRng(1), distance: CRITERIA_TARGETS.distances.at(-1) });
|
|
3352
|
+
const switched = (far?.farMix ?? 0) === 1 && (far?.layers.some((layer) => layer.role === "far") ?? false);
|
|
3353
|
+
const pass = decreasing(levels) && decreasing(brightness) && switched;
|
|
3354
|
+
return {
|
|
3355
|
+
id: "C3",
|
|
3356
|
+
pass,
|
|
3357
|
+
value: `${levels.map((level) => db(level)).join(" \u203A ")}; ${brightness.map((hz) => `${Math.round(hz)} Hz`).join(" \u203A ")}${switched ? "; longe = far" : "; sem camada far"}`,
|
|
3358
|
+
target: "cai e escurece com a dist\xE2ncia; troca para a vers\xE3o de longe depois de ~50 m",
|
|
3359
|
+
data: { distances: CRITERIA_TARGETS.distances, levels, brightness, switched }
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
async function c4(options) {
|
|
3363
|
+
const side = async (x) => {
|
|
3364
|
+
const result = await render(options, 0.8, (engine) => engine.play(options.sound, { position: [x, 0, -2], seed: 1, delay: EVENT_AT }) ? 1 : 0);
|
|
3365
|
+
const [left2, right2] = result.channels;
|
|
3366
|
+
return balanceOf(left2, right2, 0, left2.length);
|
|
3367
|
+
};
|
|
3368
|
+
const right = await side(10);
|
|
3369
|
+
const left = await side(-10);
|
|
3370
|
+
const bright = async (occluded) => {
|
|
3371
|
+
const result = await render(options, 0.8, (engine) => engine.play(options.sound, { position: [0, 0, -10], seed: 1, delay: EVENT_AT }) ? 1 : 0, occluded ? { occlusion: () => 1 } : {});
|
|
3372
|
+
const onset = onsetIndex(result.channels, 0, 1e-5);
|
|
3373
|
+
return onset < 0 ? 0 : centroidHz(result.channels, result.sampleRate, onset);
|
|
3374
|
+
};
|
|
3375
|
+
const clear = await bright(false);
|
|
3376
|
+
const wall = await bright(true);
|
|
3377
|
+
const pass = right > 0.2 && left < -0.2 && wall < clear * 0.7;
|
|
3378
|
+
return {
|
|
3379
|
+
id: "C4",
|
|
3380
|
+
pass,
|
|
3381
|
+
value: `dir ${right.toFixed(2)} esq ${left.toFixed(2)}; parede ${Math.round(wall)} Hz de ${Math.round(clear)} Hz`,
|
|
3382
|
+
target: "lado certo; atr\xE1s da parede fica abafado",
|
|
3383
|
+
data: { right, left, clear, wall }
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
async function c5(options, sound) {
|
|
3387
|
+
const result = await render(options, 0.3 * CRITERIA_TARGETS.plays + 1, (engine) => {
|
|
3388
|
+
let played = 0;
|
|
3389
|
+
for (let index = 0; index < CRITERIA_TARGETS.plays; index++) if (engine.play(options.sound, { distance: 1, delay: 0.1 + index * 0.3 })) played++;
|
|
3390
|
+
return played;
|
|
3391
|
+
});
|
|
3392
|
+
const close = result.events.map((event) => event.layers?.find((layer) => layer.role === "close"));
|
|
3393
|
+
const takes = close.map((layer) => layer?.url ?? "");
|
|
3394
|
+
const repeats = takes.filter((take, index) => index > 0 && take === takes[index - 1]).length;
|
|
3395
|
+
const distinct = new Set(takes).size;
|
|
3396
|
+
const rates = close.map((layer) => layer?.rate ?? 1);
|
|
3397
|
+
const spread = (Math.max(...rates) - Math.min(...rates)) / 2;
|
|
3398
|
+
const pitch = sound.pitch ?? 0.04;
|
|
3399
|
+
const pass = distinct >= CRITERIA_TARGETS.minTakes && repeats === 0 && pitch >= CRITERIA_TARGETS.pitch[0] && pitch <= CRITERIA_TARGETS.pitch[1] && spread > 0.01;
|
|
3400
|
+
return {
|
|
3401
|
+
id: "C5",
|
|
3402
|
+
pass,
|
|
3403
|
+
value: `${distinct} tomadas, ${repeats} repeti\xE7\xF5es seguidas, tom \xB1${(pitch * 100).toFixed(0)}%`,
|
|
3404
|
+
target: `\u2265 ${CRITERIA_TARGETS.minTakes} varia\xE7\xF5es, nunca a mesma duas vezes, tom \xB13\u20135%`,
|
|
3405
|
+
data: { takes, rates }
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
async function c6(options) {
|
|
3409
|
+
const peakAt = async (id) => {
|
|
3410
|
+
const result = await render(options, 1.2, (engine) => engine.play(id, { distance: 1, seed: 1, delay: EVENT_AT }) ? 1 : 0);
|
|
3411
|
+
const onset = onsetIndex(result.channels, 0, 1e-5);
|
|
3412
|
+
return { peak: dbOf(peakOf(result.channels)), rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3) };
|
|
3413
|
+
};
|
|
3414
|
+
const own = await peakAt(options.sound);
|
|
3415
|
+
const order = options.loudnessOrder ?? [];
|
|
3416
|
+
const loudness = [];
|
|
3417
|
+
for (const id of order) loudness.push((await peakAt(id)).rms);
|
|
3418
|
+
const ordered = order.length < 2 || decreasing(loudness);
|
|
3419
|
+
const pass = own.peak <= CRITERIA_TARGETS.peakDb && ordered;
|
|
3420
|
+
return {
|
|
3421
|
+
id: "C6",
|
|
3422
|
+
pass,
|
|
3423
|
+
value: `pico ${db(own.peak)}, RMS ${db(own.rms)}${order.length > 1 ? `; ordem ${ordered ? "ok" : "errada"}` : ""}`,
|
|
3424
|
+
target: "tiro > explos\xE3o > impacto > passo; pico \u2264 \u22121 dBFS",
|
|
3425
|
+
data: { peak: own.peak, rms: own.rms, order, loudness }
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
async function c7(options, sound) {
|
|
3429
|
+
if (!sound.tiers || sound.tiers.length < 2) return { id: "C7", pass: false, value: "sem camadas leve/m\xE9dio/forte", target: "muda de camada, n\xE3o s\xF3 de volume" };
|
|
3430
|
+
const at = async (intensity) => {
|
|
3431
|
+
const result = await render(options, 1.2, (engine) => engine.play(options.sound, { distance: 1, seed: 1, intensity, delay: EVENT_AT }) ? 1 : 0);
|
|
3432
|
+
const onset = onsetIndex(result.channels, 0, 1e-5);
|
|
3433
|
+
const event = result.events[0];
|
|
3434
|
+
return { tier: event?.tier ?? "", rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3), files: event?.layers?.find((layer) => layer.role === "close")?.url ?? "" };
|
|
3435
|
+
};
|
|
3436
|
+
const light = await at(0.1);
|
|
3437
|
+
const heavy = await at(0.9);
|
|
3438
|
+
const pass = light.tier !== heavy.tier && light.files !== heavy.files && heavy.rms > light.rms;
|
|
3439
|
+
return {
|
|
3440
|
+
id: "C7",
|
|
3441
|
+
pass,
|
|
3442
|
+
value: `${light.tier} ${db(light.rms)} \u2192 ${heavy.tier} ${db(heavy.rms)}`,
|
|
3443
|
+
target: "golpe fraco x forte troca a camada (leve/m\xE9dio/forte), n\xE3o s\xF3 o volume",
|
|
3444
|
+
data: { light, heavy }
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
async function c8(options, sound) {
|
|
3448
|
+
const sampleRate = options.sampleRate ?? 48e3;
|
|
3449
|
+
const decoder = offlineFactory(options)(1, sampleRate, sampleRate);
|
|
3450
|
+
const drops = [];
|
|
3451
|
+
for (const url2 of sampleUrls(sound)) {
|
|
3452
|
+
const buffer = await decoder.decodeAudioData(await options.loader(url2));
|
|
3453
|
+
drops.push({ url: url2, drop: endDropDb([buffer.getChannelData(0)], buffer.sampleRate) });
|
|
3454
|
+
}
|
|
3455
|
+
const worst = drops.reduce((max, entry) => Math.max(max, entry.drop), -Infinity);
|
|
3456
|
+
const ring = async (environment) => {
|
|
3457
|
+
const result = await render(options, 3, (engine) => engine.play(options.sound, { distance: 1, seed: 1, environment, delay: EVENT_AT }) ? 1 : 0);
|
|
3458
|
+
return ringSeconds(result.channels, result.sampleRate, Math.round(EVENT_AT * result.sampleRate));
|
|
3459
|
+
};
|
|
3460
|
+
const indoor = await ring("indoor");
|
|
3461
|
+
const outdoor = await ring("outdoor");
|
|
3462
|
+
const hasTails = Boolean(sound.tail?.indoor?.length && sound.tail?.outdoor?.length);
|
|
3463
|
+
const pass = worst <= CRITERIA_TARGETS.endDropDb && hasTails && outdoor > indoor;
|
|
3464
|
+
return {
|
|
3465
|
+
id: "C8",
|
|
3466
|
+
pass,
|
|
3467
|
+
value: `fim ${db(worst)}; cauda dentro ${indoor.toFixed(2)} s, fora ${outdoor.toFixed(2)} s${hasTails ? "" : " (sem as duas caudas)"}`,
|
|
3468
|
+
target: "n\xE3o corta seco; cauda de interior x exterior coerente",
|
|
3469
|
+
data: { drops, indoor, outdoor }
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
async function c9(options) {
|
|
3473
|
+
const { burst, burstSpacing } = CRITERIA_TARGETS;
|
|
3474
|
+
const first = 0.1;
|
|
3475
|
+
const result = await render(options, burst * burstSpacing + 1.5, (engine) => {
|
|
3476
|
+
let played = 0;
|
|
3477
|
+
for (let index = 0; index < burst; index++) if (engine.play(options.sound, { distance: 1, delay: first + index * burstSpacing })) played++;
|
|
3478
|
+
return played;
|
|
3479
|
+
});
|
|
3480
|
+
const peak = dbOf(peakOf(result.channels));
|
|
3481
|
+
const slots = [];
|
|
3482
|
+
for (let index = 0; index < burst; index++) {
|
|
3483
|
+
const at = Math.round((first + index * burstSpacing) * result.sampleRate);
|
|
3484
|
+
slots.push(rmsDb(result.channels, at, at + Math.round(burstSpacing * result.sampleRate)));
|
|
3485
|
+
}
|
|
3486
|
+
const sorted = [...slots].sort((a, b) => a - b);
|
|
3487
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
3488
|
+
const vanished = slots.filter((level) => level < median - 6).length;
|
|
3489
|
+
const buildUp = slots.slice(-5).reduce((sum, level) => sum + level, 0) / 5 - slots[0];
|
|
3490
|
+
const pass = result.handles === burst && vanished === 0 && buildUp <= 6 && peak <= CRITERIA_TARGETS.peakDb;
|
|
3491
|
+
return {
|
|
3492
|
+
id: "C9",
|
|
3493
|
+
pass,
|
|
3494
|
+
value: `${result.handles}/${burst} tocaram, ${vanished} sumiram, ac\xFAmulo ${buildUp >= 0 ? "+" : ""}${buildUp.toFixed(1)} dB, pico ${db(peak)}`,
|
|
3495
|
+
target: "rajada sem amontoar (ac\xFAmulo \u2264 6 dB) nem sumir; pico \u2264 \u22121 dBFS",
|
|
3496
|
+
data: { handles: result.handles, slots, vanished, buildUp, peak }
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
async function measureCriteria(options, only = CRITERIA) {
|
|
3500
|
+
const probe = createAudioEngine({ banks: options.banks, createContext: () => null });
|
|
3501
|
+
const sound = probe.registry.sample(options.sound);
|
|
3502
|
+
if (!sound) throw new Error(`${options.sound} is not a sample sound of these banks`);
|
|
3503
|
+
const results = [];
|
|
3504
|
+
for (const id of only) {
|
|
3505
|
+
if (id === "C1") results.push(await c1(options));
|
|
3506
|
+
else if (id === "C2") results.push(await c2(options));
|
|
3507
|
+
else if (id === "C3") results.push(await c3(options, sound));
|
|
3508
|
+
else if (id === "C4") results.push(await c4(options));
|
|
3509
|
+
else if (id === "C5") results.push(await c5(options, sound));
|
|
3510
|
+
else if (id === "C6") results.push(await c6(options));
|
|
3511
|
+
else if (id === "C7") results.push(await c7(options, sound));
|
|
3512
|
+
else if (id === "C8") results.push(await c8(options, sound));
|
|
3513
|
+
else if (id === "C9") results.push(await c9(options));
|
|
3514
|
+
else results.push({ id: "C10", pass: null, value: "gravar e ouvir", target: "aprovado pelo ouvido do dono (print + grava\xE7\xE3o na 4280)" });
|
|
3515
|
+
}
|
|
3516
|
+
return results;
|
|
3517
|
+
}
|
|
3518
|
+
async function recordOutput(engine, seconds, act) {
|
|
3519
|
+
engine.unlock();
|
|
3520
|
+
const output = engine.output();
|
|
3521
|
+
const Recorder = globalThis.MediaRecorder;
|
|
3522
|
+
const context = output?.context;
|
|
3523
|
+
if (!output || !Recorder || !context || typeof context.createMediaStreamDestination !== "function") return null;
|
|
3524
|
+
const tap = context.createMediaStreamDestination();
|
|
3525
|
+
output.connect(tap);
|
|
3526
|
+
const recorder = new Recorder(tap.stream);
|
|
3527
|
+
const chunks = [];
|
|
3528
|
+
recorder.ondataavailable = (event) => chunks.push(event.data);
|
|
3529
|
+
const done = new Promise((resolve) => {
|
|
3530
|
+
recorder.onstop = () => resolve();
|
|
3531
|
+
});
|
|
3532
|
+
recorder.start();
|
|
3533
|
+
act();
|
|
3534
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
3535
|
+
recorder.stop();
|
|
3536
|
+
await done;
|
|
3537
|
+
output.disconnect(tap);
|
|
3538
|
+
return new Blob(chunks, { type: recorder.mimeType || "audio/webm" });
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// src/manifest.ts
|
|
3542
|
+
var SDK_LICENSES = ["CC0", "CC-BY"];
|
|
3543
|
+
var SFX_LAYERS = ["close", "far", "tail_in", "tail_out"];
|
|
3544
|
+
var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
|
|
3545
|
+
function parseSfxName(name) {
|
|
3546
|
+
const match = /^(.*?)(?:_(close|far|tail_in|tail_out))?(?:_(\d+))?$/.exec(name);
|
|
3547
|
+
const id = match?.[1] || name;
|
|
3548
|
+
const layer = match?.[2] ?? "close";
|
|
3549
|
+
const take = match?.[3] ? Number(match[3]) : 1;
|
|
3550
|
+
return { id, layer, take };
|
|
3551
|
+
}
|
|
3552
|
+
function splitTier(id) {
|
|
3553
|
+
const match = /^(.*)_(light|medium|heavy)$/.exec(id);
|
|
3554
|
+
return match ? { base: match[1], tier: match[2] } : { base: id };
|
|
3555
|
+
}
|
|
3556
|
+
var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
|
|
3557
|
+
function sampleBankFromManifest(manifest, options) {
|
|
3558
|
+
const baseUrl = options.baseUrl ?? "";
|
|
3559
|
+
const samples = [];
|
|
3560
|
+
const tiersOf = /* @__PURE__ */ new Map();
|
|
3561
|
+
for (const sound of manifest.sounds) {
|
|
3562
|
+
const { base, tier } = splitTier(sound.id);
|
|
3563
|
+
if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
|
|
3564
|
+
}
|
|
3565
|
+
const bases = /* @__PURE__ */ new Map();
|
|
3566
|
+
for (const sound of manifest.sounds) {
|
|
3567
|
+
if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
|
|
3568
|
+
}
|
|
3569
|
+
for (const [base, tiers] of tiersOf) {
|
|
3570
|
+
if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
|
|
3571
|
+
}
|
|
3572
|
+
for (const sound of bases.values()) {
|
|
3573
|
+
const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
|
|
3574
|
+
const close = refsOf(sound.layers.close, baseUrl);
|
|
3575
|
+
if (!numbers || close.length === 0) continue;
|
|
3576
|
+
const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
|
|
3577
|
+
const name = splitTier(tier.id).tier;
|
|
3578
|
+
const tierFar = refsOf(tier.layers.far, baseUrl);
|
|
3579
|
+
return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
|
|
3580
|
+
}).sort((a, b) => a.from - b.from);
|
|
3581
|
+
const far = refsOf(sound.layers.far, baseUrl);
|
|
3582
|
+
const indoor = refsOf(sound.layers.tail_in, baseUrl);
|
|
3583
|
+
const outdoor = refsOf(sound.layers.tail_out, baseUrl);
|
|
3584
|
+
const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
|
|
3585
|
+
samples.push({
|
|
3586
|
+
id: sound.id,
|
|
3587
|
+
bus: numbers.bus,
|
|
3588
|
+
close,
|
|
3589
|
+
...far.length > 0 ? { far } : {},
|
|
3590
|
+
...tiers.length > 0 ? { tiers } : {},
|
|
3591
|
+
...numbers.crossover ? { crossover: numbers.crossover } : {},
|
|
3592
|
+
...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
|
|
3593
|
+
...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
|
|
3594
|
+
...numbers.gain === void 0 ? {} : { gain: numbers.gain },
|
|
3595
|
+
...numbers.falloff ? { falloff: numbers.falloff } : {},
|
|
3596
|
+
...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
|
|
3597
|
+
...numbers.group ? { group: numbers.group } : {},
|
|
3598
|
+
...numbers.priority === void 0 ? {} : { priority: numbers.priority },
|
|
3599
|
+
...numbers.fallback ? { fallback: numbers.fallback } : {},
|
|
3600
|
+
sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
return { id: options.id, samples };
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
// src/kit-samples.ts
|
|
3607
|
+
var KIT_CATEGORY_BUS = {
|
|
3608
|
+
armas: "weapons",
|
|
3609
|
+
impactos: "world",
|
|
3610
|
+
"explosoes-fogo": "world",
|
|
3611
|
+
personagem: "world",
|
|
3612
|
+
veiculos: "world",
|
|
3613
|
+
"itens-portas": "world",
|
|
3614
|
+
ambiente: "ambience"
|
|
3615
|
+
};
|
|
3616
|
+
var VOICE_IDS = /^(player_(hurt|death|effort|breath)|scream|zombie_)/;
|
|
3617
|
+
var KIT_SAMPLE_ALIASES = {
|
|
3618
|
+
// a house door: the latch giving, the hinge, the leaf meeting the frame, a locked one shaken; a drawer on its runners
|
|
3619
|
+
latch: "metal_latch",
|
|
3620
|
+
creak_wood: "door_creak",
|
|
3621
|
+
shut_wood: "door_wood_shut",
|
|
3622
|
+
rattle: "door_locked_rattle",
|
|
3623
|
+
// a drawer stays on its recipe: the recorded runs (1 to 1.9 s) outlast a drawer's pull
|
|
3624
|
+
// an item taken in the hand (game-destruction-three grab_* keys)
|
|
3625
|
+
grab_hard: "item_pickup_generic",
|
|
3626
|
+
grab_soft: "item_pickup_leather"
|
|
3627
|
+
};
|
|
3628
|
+
function kitSampleDefaults(sound) {
|
|
3629
|
+
const bus = VOICE_IDS.test(sound.id) ? "voices" : KIT_CATEGORY_BUS[sound.category];
|
|
3630
|
+
if (!bus) return void 0;
|
|
3631
|
+
return { bus, pitch: sound.category === "ambiente" || /_loop/.test(sound.id) ? 0 : 0.04 };
|
|
3632
|
+
}
|
|
3633
|
+
function withAliases(manifest, aliases) {
|
|
3634
|
+
const byId = new Map(manifest.sounds.map((sound) => [sound.id, sound]));
|
|
3635
|
+
const extra = [];
|
|
3636
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
3637
|
+
const sound = byId.get(target);
|
|
3638
|
+
if (sound && !byId.has(alias)) extra.push({ ...sound, id: alias });
|
|
3639
|
+
}
|
|
3640
|
+
return { ...manifest, sounds: [...manifest.sounds, ...extra] };
|
|
3641
|
+
}
|
|
3642
|
+
function kitSampleBank(manifest, options = {}) {
|
|
3643
|
+
return sampleBankFromManifest(withAliases(manifest, options.aliases ?? KIT_SAMPLE_ALIASES), {
|
|
3644
|
+
id: options.id ?? "kit_samples",
|
|
3645
|
+
baseUrl: options.baseUrl ?? "",
|
|
3646
|
+
defaults: kitSampleDefaults
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
var isFileUrl = (url2) => {
|
|
3650
|
+
const page = globalThis.location?.href;
|
|
3651
|
+
try {
|
|
3652
|
+
return new URL(url2, page).protocol === "file:";
|
|
3653
|
+
} catch {
|
|
3654
|
+
return false;
|
|
3655
|
+
}
|
|
3656
|
+
};
|
|
3657
|
+
var fetchJson = async (url2) => {
|
|
3658
|
+
if (isFileUrl(url2)) throw new Error(`${url2}: file:// is not fetched`);
|
|
3659
|
+
const response = await fetch(url2);
|
|
3660
|
+
if (!response.ok) throw new Error(`${url2}: ${response.status}`);
|
|
3661
|
+
return await response.json();
|
|
3662
|
+
};
|
|
3663
|
+
async function loadKitSamples(engine, options = {}) {
|
|
3664
|
+
const baseUrl = options.baseUrl ?? "";
|
|
3665
|
+
let manifest;
|
|
3666
|
+
try {
|
|
3667
|
+
manifest = await (options.fetchManifest ?? fetchJson)(`${baseUrl}manifest.json`);
|
|
3668
|
+
} catch {
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
const bank = kitSampleBank(manifest, options);
|
|
3672
|
+
engine.register(bank);
|
|
3673
|
+
if (options.preload && engine.loadSamples) void engine.loadSamples(options.preload).catch(() => void 0);
|
|
3674
|
+
return bank;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/user-samples.ts
|
|
3678
|
+
var USER_SAMPLES_BANK = "user_samples";
|
|
3679
|
+
var KIT_SAMPLES_BANK = "kit_samples";
|
|
3680
|
+
var USER_SOUND_PREFIX = "user:";
|
|
3681
|
+
function soundSourceOf(event) {
|
|
3682
|
+
if (event.kind === "recipe") return "synth";
|
|
3683
|
+
return event.bank === USER_SAMPLES_BANK ? "user" : "sdk";
|
|
3684
|
+
}
|
|
3685
|
+
function userSampleBank(manifest, options = {}) {
|
|
3686
|
+
const playing = [];
|
|
3687
|
+
const behindKit = [];
|
|
3688
|
+
const sounds = [];
|
|
3689
|
+
for (const picked of manifest.sounds) {
|
|
3690
|
+
const sound = { ...picked, id: splitTier(picked.id).base };
|
|
3691
|
+
sounds.push({ ...sound, id: `${USER_SOUND_PREFIX}${sound.id}` });
|
|
3692
|
+
if (sound.role === "fallback" && options.kitHas?.(sound.id)) {
|
|
3693
|
+
behindKit.push(sound.id);
|
|
3694
|
+
continue;
|
|
3695
|
+
}
|
|
3696
|
+
sounds.push(sound);
|
|
3697
|
+
playing.push(sound.id);
|
|
3698
|
+
}
|
|
3699
|
+
const bank = sampleBankFromManifest(
|
|
3700
|
+
{ sounds },
|
|
3701
|
+
{
|
|
3702
|
+
id: USER_SAMPLES_BANK,
|
|
3703
|
+
baseUrl: options.baseUrl ?? "",
|
|
3704
|
+
defaults: (sound) => kitSampleDefaults({ id: sound.id.replace(USER_SOUND_PREFIX, ""), category: sound.category })
|
|
3705
|
+
}
|
|
3706
|
+
);
|
|
3707
|
+
return { bank, playing, behindKit };
|
|
3708
|
+
}
|
|
3709
|
+
var fetchManifestJson = async (url2) => {
|
|
3710
|
+
const page = globalThis.location?.href;
|
|
3711
|
+
if (new URL(url2, page ?? "http://localhost/").protocol === "file:") throw new Error(`${url2}: file:// is not fetched`);
|
|
3712
|
+
const response = await fetch(url2);
|
|
3713
|
+
if (!response.ok) throw new Error(`${url2}: ${response.status}`);
|
|
3714
|
+
return await response.json();
|
|
3715
|
+
};
|
|
3716
|
+
async function loadUserSamples(engine, options = {}) {
|
|
3717
|
+
const baseUrl = options.baseUrl ?? "./sfx-local/";
|
|
3718
|
+
let manifest;
|
|
3719
|
+
try {
|
|
3720
|
+
manifest = await (options.fetchManifest ?? fetchManifestJson)(`${baseUrl}manifest.json`);
|
|
3721
|
+
} catch {
|
|
3722
|
+
return null;
|
|
3723
|
+
}
|
|
3724
|
+
const kitHas = options.kitHas ?? ((id) => Boolean(engine.registry?.sample(id)));
|
|
3725
|
+
const result = userSampleBank(manifest, { baseUrl, kitHas });
|
|
3726
|
+
engine.register(result.bank);
|
|
3727
|
+
return result;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// src/bench/sound-check-locale.ts
|
|
3731
|
+
var PT = {
|
|
3732
|
+
group: "Som",
|
|
3733
|
+
name: "Painel de som (arquivos e C1\u2013C10)",
|
|
3734
|
+
controls: {
|
|
3735
|
+
category: "1 \xB7 Categoria",
|
|
3736
|
+
sound: "1 \xB7 Som",
|
|
3737
|
+
play: "1 \xB7 \u25B6 Tocar",
|
|
3738
|
+
play_user: "1 \xB7 \u25B6 Tocar o seu",
|
|
3739
|
+
material: "2 \xB7 Material",
|
|
3740
|
+
blow: "2 \xB7 Golpe",
|
|
3741
|
+
strike: "2 \xB7 \u25B6 Tocar golpe",
|
|
3742
|
+
distance: "3 \xB7 Dist\xE2ncia",
|
|
3743
|
+
bearing: "3 \xB7 Dire\xE7\xE3o",
|
|
3744
|
+
environment: "3 \xB7 Lugar",
|
|
3745
|
+
strength: "3 \xB7 For\xE7a",
|
|
3746
|
+
wall: "3 \xB7 Atr\xE1s da parede",
|
|
3747
|
+
burst: "3 \xB7 Rajada 20\xD7",
|
|
3748
|
+
play_a: "4 \xB7 \u25B6 A (atual)",
|
|
3749
|
+
play_b: "4 \xB7 \u25B6 B (novo)",
|
|
3750
|
+
ab_cycle: "4 \xB7 Alternar A/B",
|
|
3751
|
+
blind: "4 \xB7 \xC0s cegas (X)",
|
|
3752
|
+
reveal: "4 \xB7 Revelar X",
|
|
3753
|
+
prefer_a: "4 \xB7 Prefiro A",
|
|
3754
|
+
prefer_b: "4 \xB7 Prefiro B",
|
|
3755
|
+
measure: "5 \xB7 Medir C1\u2013C10",
|
|
3756
|
+
record: "5 \xB7 Gravar 3 s",
|
|
3757
|
+
approve: "5 \xB7 Aprovo (C10)"
|
|
3758
|
+
},
|
|
3759
|
+
hints: {
|
|
3760
|
+
play_user: "Toca a sua grava\xE7\xE3o deste som (dos seus pacotes, fora do git), mesmo quando o SDK tem uma melhor e toca na frente.",
|
|
3761
|
+
sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
|
|
3762
|
+
strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
|
|
3763
|
+
wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
|
|
3764
|
+
burst: "Vinte vezes a cada 60 ms, como uma rajada (C9).",
|
|
3765
|
+
ab_cycle: "A, B, A, B, um por segundo.",
|
|
3766
|
+
blind: "Toca A ou B sem dizer qual; depois revele.",
|
|
3767
|
+
measure: "Mede o som escolhido no motor real (contexto offline): sincronia, som certo, dist\xE2ncia, lado, varia\xE7\xE3o, volume, for\xE7a, cauda e rajada.",
|
|
3768
|
+
record: "Grava 3 s da sa\xEDda com o som tocando e baixa o arquivo (C10).",
|
|
3769
|
+
approve: "Marca o C10 do som escolhido como aprovado pelo seu ouvido."
|
|
3770
|
+
},
|
|
3771
|
+
categories: {
|
|
3772
|
+
armas: "Armas",
|
|
3773
|
+
impactos: "Impactos",
|
|
3774
|
+
"explosoes-fogo": "Explos\xF5es e fogo",
|
|
3775
|
+
personagem: "Personagem",
|
|
3776
|
+
veiculos: "Ve\xEDculos",
|
|
3777
|
+
"itens-portas": "Itens e portas",
|
|
3778
|
+
ambiente: "Ambiente",
|
|
3779
|
+
calibracao: "Calibra\xE7\xE3o (SDK)"
|
|
3780
|
+
},
|
|
3781
|
+
blows: { bullet: "tiro", blunt: "soco/chute", blade: "faca", break: "quebra", drag: "arrasto/queda" },
|
|
3782
|
+
materials: {
|
|
3783
|
+
steel: "a\xE7o",
|
|
3784
|
+
aluminium: "alum\xEDnio",
|
|
3785
|
+
wood: "madeira",
|
|
3786
|
+
plywood: "compensado",
|
|
3787
|
+
mdf: "MDF",
|
|
3788
|
+
glass: "vidro",
|
|
3789
|
+
tempered_glass: "vidro temperado",
|
|
3790
|
+
ceramic: "cer\xE2mica",
|
|
3791
|
+
concrete: "concreto",
|
|
3792
|
+
brick: "tijolo",
|
|
3793
|
+
plaster: "gesso",
|
|
3794
|
+
plastic: "pl\xE1stico",
|
|
3795
|
+
fabric: "tecido",
|
|
3796
|
+
paper: "papel\xE3o",
|
|
3797
|
+
foliage: "folhagem",
|
|
3798
|
+
soil: "terra",
|
|
3799
|
+
flesh: "carne"
|
|
3800
|
+
},
|
|
3801
|
+
environments: { indoor: "interior", outdoor: "exterior" },
|
|
3802
|
+
strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
|
|
3803
|
+
states: { file: "A", synth: "S", missing: "F" },
|
|
3804
|
+
criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
|
|
3805
|
+
metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
|
|
3806
|
+
run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
|
|
3807
|
+
words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
|
|
3808
|
+
sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
|
|
3809
|
+
probe: "Fonte do som",
|
|
3810
|
+
wall: "Parede (oclus\xE3o)",
|
|
3811
|
+
scenarios: { pipeline: "Arquivo \u2192 decodifica \u2192 toca (round-robin, cauda)", criteria: "Crit\xE9rios C1\u2013C9 no som de calibra\xE7\xE3o", loop: "Loop de arquivo: emenda sem buraco (crossfade)", panel: "Painel: categorias, matriz, A/B e parede" }
|
|
3812
|
+
};
|
|
3813
|
+
var EN2 = {
|
|
3814
|
+
...PT,
|
|
3815
|
+
group: "Sound",
|
|
3816
|
+
name: "Sound panel (files and C1\u2013C10)",
|
|
3817
|
+
controls: {
|
|
3818
|
+
category: "1 \xB7 Category",
|
|
3819
|
+
sound: "1 \xB7 Sound",
|
|
3820
|
+
play: "1 \xB7 \u25B6 Play",
|
|
3821
|
+
play_user: "1 \xB7 \u25B6 Play yours",
|
|
3822
|
+
material: "2 \xB7 Material",
|
|
3823
|
+
blow: "2 \xB7 Blow",
|
|
3824
|
+
strike: "2 \xB7 \u25B6 Play blow",
|
|
3825
|
+
distance: "3 \xB7 Distance",
|
|
3826
|
+
bearing: "3 \xB7 Bearing",
|
|
3827
|
+
environment: "3 \xB7 Place",
|
|
3828
|
+
strength: "3 \xB7 Strength",
|
|
3829
|
+
wall: "3 \xB7 Behind the wall",
|
|
3830
|
+
burst: "3 \xB7 Burst 20\xD7",
|
|
3831
|
+
play_a: "4 \xB7 \u25B6 A (current)",
|
|
3832
|
+
play_b: "4 \xB7 \u25B6 B (new)",
|
|
3833
|
+
ab_cycle: "4 \xB7 Alternate A/B",
|
|
3834
|
+
blind: "4 \xB7 Blind (X)",
|
|
3835
|
+
reveal: "4 \xB7 Reveal X",
|
|
3836
|
+
prefer_a: "4 \xB7 Prefer A",
|
|
3837
|
+
prefer_b: "4 \xB7 Prefer B",
|
|
3838
|
+
measure: "5 \xB7 Measure C1\u2013C10",
|
|
3839
|
+
record: "5 \xB7 Record 3 s",
|
|
3840
|
+
approve: "5 \xB7 Approve (C10)"
|
|
3841
|
+
},
|
|
3842
|
+
hints: {
|
|
3843
|
+
play_user: "Plays your own take of this sound (from your packs, kept out of git), even where the SDK has a better one that plays first.",
|
|
3844
|
+
sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
|
|
3845
|
+
strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
|
|
3846
|
+
wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
|
|
3847
|
+
burst: "Twenty times every 60 ms, like a burst (C9).",
|
|
3848
|
+
ab_cycle: "A, B, A, B, one a second.",
|
|
3849
|
+
blind: "Plays A or B without saying which; reveal it after.",
|
|
3850
|
+
measure: "Measures the chosen sound through the real engine (offline context): sync, right sound, distance, side, variation, level, strength, tail and burst.",
|
|
3851
|
+
record: "Records 3 s of the output with the sound playing and downloads it (C10).",
|
|
3852
|
+
approve: "Marks C10 of the chosen sound as approved by your ear."
|
|
3853
|
+
},
|
|
3854
|
+
categories: { armas: "Weapons", impactos: "Impacts", "explosoes-fogo": "Explosions and fire", personagem: "Character", veiculos: "Vehicles", "itens-portas": "Items and doors", ambiente: "Ambience", calibracao: "Calibration (SDK)" },
|
|
3855
|
+
blows: { bullet: "bullet", blunt: "punch/kick", blade: "knife", break: "break", drag: "drag/fall" },
|
|
3856
|
+
materials: {
|
|
3857
|
+
steel: "steel",
|
|
3858
|
+
aluminium: "aluminium",
|
|
3859
|
+
wood: "wood",
|
|
3860
|
+
plywood: "plywood",
|
|
3861
|
+
mdf: "MDF",
|
|
3862
|
+
glass: "glass",
|
|
3863
|
+
tempered_glass: "tempered glass",
|
|
3864
|
+
ceramic: "ceramic",
|
|
3865
|
+
concrete: "concrete",
|
|
3866
|
+
brick: "brick",
|
|
3867
|
+
plaster: "plaster",
|
|
3868
|
+
plastic: "plastic",
|
|
3869
|
+
fabric: "fabric",
|
|
3870
|
+
paper: "cardboard",
|
|
3871
|
+
foliage: "foliage",
|
|
3872
|
+
soil: "soil",
|
|
3873
|
+
flesh: "flesh"
|
|
3874
|
+
},
|
|
3875
|
+
environments: { indoor: "indoor", outdoor: "outdoor" },
|
|
3876
|
+
strengths: { light: "light", medium: "medium", heavy: "heavy" },
|
|
3877
|
+
criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
|
|
3878
|
+
metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
|
|
3879
|
+
run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
|
|
3880
|
+
words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
|
|
3881
|
+
sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
|
|
3882
|
+
probe: "Sound source",
|
|
3883
|
+
wall: "Wall (occlusion)",
|
|
3884
|
+
scenarios: { pipeline: "File \u2192 decode \u2192 play (round-robin, tail)", criteria: "Criteria C1\u2013C9 on the calibration sound", loop: "File loop: seamless joins (crossfade)", panel: "Panel: categories, matrix, A/B and wall" }
|
|
3885
|
+
};
|
|
3886
|
+
var soundCheckTextOf = (locale) => locale.startsWith("en") ? EN2 : PT;
|
|
3887
|
+
|
|
3888
|
+
// src/bench/sound-check.ts
|
|
3889
|
+
var SOUND_STRENGTHS = { light: 0.1, medium: 0.5, heavy: 0.9 };
|
|
3890
|
+
var CALIBRATION_CATEGORY = "calibracao";
|
|
3891
|
+
var CALIBRATION_BANK = { id: "calibration", samples: [CALIBRATION_SOUND, CALIBRATION_LOOP_SOUND] };
|
|
3892
|
+
var DISTANCES2 = [1, 10, 50, 200];
|
|
3893
|
+
var PROBE_ID2 = "sound_check_source";
|
|
3894
|
+
var WALL_ID = "sound_check_wall";
|
|
3895
|
+
var EYE = [0, 1.7, 3];
|
|
3896
|
+
var DRAWN_MAX = 11;
|
|
3897
|
+
var WALL_AT = -2.5;
|
|
3898
|
+
var LOG_LIMIT = 200;
|
|
3899
|
+
function createSoundCheckModule(options = {}) {
|
|
3900
|
+
const locale = options.locale ?? "pt-BR";
|
|
3901
|
+
const text = soundCheckTextOf(locale);
|
|
3902
|
+
const ownBanks = [CALIBRATION_BANK, ...options.banks ?? []];
|
|
3903
|
+
const loader = calibrationLoader(options.loader ?? fetchLoader());
|
|
3904
|
+
const log = [];
|
|
3905
|
+
const catalog = options.catalog ?? [...SFX_CATALOG, ...SFX_IMPACTS];
|
|
3906
|
+
let engine = options.engine ?? createAudioEngine({ banks: [GAME_SFX_BANK, ...ownBanks], samples: { loader } });
|
|
3907
|
+
if (options.engine) for (const bank of ownBanks) engine.register(bank);
|
|
3908
|
+
let host = null;
|
|
3909
|
+
let results = [];
|
|
3910
|
+
let run = "idle";
|
|
3911
|
+
const verdicts = {};
|
|
3912
|
+
const approvals = /* @__PURE__ */ new Set();
|
|
3913
|
+
let blind = null;
|
|
3914
|
+
const chosen = {
|
|
3915
|
+
category: options.sound && options.sound !== CALIBRATION_ID ? catalog.find((entry) => entry.id === options.sound)?.category ?? CALIBRATION_CATEGORY : CALIBRATION_CATEGORY,
|
|
3916
|
+
sound: options.sound ?? CALIBRATION_ID,
|
|
3917
|
+
material: "concrete",
|
|
3918
|
+
blow: "bullet",
|
|
3919
|
+
distance: 10,
|
|
3920
|
+
bearing: 0,
|
|
3921
|
+
environment: "outdoor",
|
|
3922
|
+
strength: "medium",
|
|
3923
|
+
wall: false
|
|
3924
|
+
};
|
|
3925
|
+
const registry = () => engine.registry;
|
|
3926
|
+
const entryOf = (id) => catalog.find((entry) => entry.id === id);
|
|
3927
|
+
const stateOf = (id) => {
|
|
3928
|
+
const entry = entryOf(id);
|
|
3929
|
+
if (entry) return sfxState(entry, registry());
|
|
3930
|
+
return registry().sample(id) ? "file" : registry().sound(id) ? "synth" : "missing";
|
|
3931
|
+
};
|
|
3932
|
+
const pairOf = (id) => {
|
|
3933
|
+
const entry = entryOf(id);
|
|
3934
|
+
const sample = registry().sample(id);
|
|
3935
|
+
const current = entry?.current ?? sample?.fallback ?? (registry().sound(id) ? id : void 0);
|
|
3936
|
+
return { a: current && registry().sound(current) ? current : null, b: sample ? id : null };
|
|
3937
|
+
};
|
|
3938
|
+
const soundsOf = (category) => {
|
|
3939
|
+
if (category === CALIBRATION_CATEGORY) return registry().sampleIds().filter((id) => !entryOf(id));
|
|
3940
|
+
return catalog.filter((entry) => entry.category === category).map((entry) => entry.id);
|
|
3941
|
+
};
|
|
3942
|
+
const sourceOfId = (id) => {
|
|
3943
|
+
const state = stateOf(id);
|
|
3944
|
+
if (state !== "file") return text.states[state];
|
|
3945
|
+
const own = registry().sample(`${USER_SOUND_PREFIX}${id}`) ? " +seu" : "";
|
|
3946
|
+
return `${text.states.file} ${text.sources[soundSourceOf({ kind: "sample", bank: registry().bankOf(id) })]}${own}`;
|
|
3947
|
+
};
|
|
3948
|
+
const label = (id) => {
|
|
3949
|
+
const entry = entryOf(id);
|
|
3950
|
+
return `${id}${entry ? ` \xB7 P${entry.priority}` : ""} \xB7 ${sourceOfId(id)}`;
|
|
3951
|
+
};
|
|
3952
|
+
const soundOptions = () => soundsOf(chosen.category).map((id) => ({ id, name: label(id) }));
|
|
3953
|
+
const listener = () => {
|
|
3954
|
+
const camera = host?.camera;
|
|
3955
|
+
const three = host?.THREE;
|
|
3956
|
+
if (typeof camera?.getWorldDirection !== "function" || typeof three?.Vector3 !== "function" || !camera.position) return { position: EYE, yaw: 0 };
|
|
3957
|
+
const direction = camera.getWorldDirection(new three.Vector3());
|
|
3958
|
+
return { position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-direction.x, -direction.z) };
|
|
3959
|
+
};
|
|
3960
|
+
const offset = (ears, distance) => {
|
|
3961
|
+
const bearing = chosen.bearing * Math.PI / 180;
|
|
3962
|
+
const forward = [-Math.sin(ears.yaw), -Math.cos(ears.yaw)];
|
|
3963
|
+
const right = [Math.cos(ears.yaw), -Math.sin(ears.yaw)];
|
|
3964
|
+
return [ears.position[0] + distance * (forward[0] * Math.cos(bearing) + right[0] * Math.sin(bearing)), ears.position[1], ears.position[2] + distance * (forward[1] * Math.cos(bearing) + right[1] * Math.sin(bearing))];
|
|
3965
|
+
};
|
|
3966
|
+
const playOptions = (extra = {}) => {
|
|
3967
|
+
const ears = listener();
|
|
3968
|
+
engine.setListener(ears);
|
|
3969
|
+
return { position: offset(ears, chosen.distance), environment: chosen.environment, intensity: SOUND_STRENGTHS[chosen.strength], occlusion: chosen.wall ? 1 : 0, ...extra };
|
|
3970
|
+
};
|
|
3971
|
+
const playId = (id, extra = {}) => {
|
|
3972
|
+
if (!id) return null;
|
|
3973
|
+
engine.unlock();
|
|
3974
|
+
return engine.play(id, playOptions(extra));
|
|
3975
|
+
};
|
|
3976
|
+
const preferred = (id) => pairOf(id).b ?? pairOf(id).a ?? (registry().sound(id) ? id : null);
|
|
3977
|
+
const play = () => playId(preferred(chosen.sound));
|
|
3978
|
+
const playB = async (delay = 0) => {
|
|
3979
|
+
const { b } = pairOf(chosen.sound);
|
|
3980
|
+
if (!b) return;
|
|
3981
|
+
if (engine.sampleStatus(b) !== "ready") await engine.loadSamples([b]);
|
|
3982
|
+
playId(b, { delay });
|
|
3983
|
+
};
|
|
3984
|
+
const playUser = async () => {
|
|
3985
|
+
const own = `${USER_SOUND_PREFIX}${chosen.sound}`;
|
|
3986
|
+
if (!registry().sample(own)) return;
|
|
3987
|
+
if (engine.sampleStatus(own) !== "ready") await engine.loadSamples([own]);
|
|
3988
|
+
playId(own);
|
|
3989
|
+
};
|
|
3990
|
+
const playA = (delay = 0) => void playId(pairOf(chosen.sound).a, { delay });
|
|
3991
|
+
const placeScenery = () => {
|
|
3992
|
+
const scene = host?.scene;
|
|
3993
|
+
if (typeof scene?.traverse !== "function") return;
|
|
3994
|
+
const at = offset({ position: EYE, yaw: 0 }, Math.min(chosen.distance, DRAWN_MAX));
|
|
3995
|
+
scene.traverse((object) => {
|
|
3996
|
+
if (object.userData?.[PROBE_ID2] === true) object.position?.set(at[0], 0, at[2]);
|
|
3997
|
+
if (object.userData?.[WALL_ID] === true) object.visible = chosen.wall;
|
|
3998
|
+
});
|
|
3999
|
+
};
|
|
4000
|
+
const measure = async () => {
|
|
4001
|
+
run = "measuring";
|
|
4002
|
+
try {
|
|
4003
|
+
engine.unlock();
|
|
4004
|
+
const context = engine.output()?.context ?? null;
|
|
4005
|
+
const bank = {
|
|
4006
|
+
id: "measured",
|
|
4007
|
+
sounds: registry().soundIds().map((id) => registry().sound(id)).filter((recipe) => recipe !== void 0),
|
|
4008
|
+
loops: registry().loopIds().map((id) => registry().loop(id)).filter((recipe) => recipe !== void 0),
|
|
4009
|
+
samples: registry().sampleIds().map((id) => registry().sample(id)).filter((sample) => sample !== void 0)
|
|
4010
|
+
};
|
|
4011
|
+
results = await measureCriteria({
|
|
4012
|
+
sound: chosen.sound,
|
|
4013
|
+
banks: [bank],
|
|
4014
|
+
loader,
|
|
4015
|
+
...context ? { baseLatency: context.baseLatency ?? 0, outputLatency: context.outputLatency ?? 0 } : {},
|
|
4016
|
+
...options.actions?.[chosen.sound] ? { actions: options.actions[chosen.sound] } : {},
|
|
4017
|
+
...options.loudnessOrder ? { loudnessOrder: options.loudnessOrder } : {}
|
|
4018
|
+
});
|
|
4019
|
+
if (approvals.has(chosen.sound)) results = results.map((result) => result.id === "C10" ? { ...result, pass: true, value: text.words.approved } : result);
|
|
4020
|
+
run = "done";
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
run = "failed";
|
|
4023
|
+
results = [{ id: "C1", pass: false, value: error instanceof Error ? error.message : String(error), target: "" }];
|
|
4024
|
+
}
|
|
4025
|
+
;
|
|
4026
|
+
globalThis.__soundCheck = { sound: chosen.sound, results };
|
|
4027
|
+
return results;
|
|
4028
|
+
};
|
|
4029
|
+
const record = async () => {
|
|
4030
|
+
run = "recording";
|
|
4031
|
+
const blob = await recordOutput(engine, 3, () => {
|
|
4032
|
+
for (let index = 0; index < 4; index++) playId(preferred(chosen.sound), { delay: index * 0.6 });
|
|
4033
|
+
});
|
|
4034
|
+
const documentLike = globalThis.document;
|
|
4035
|
+
if (blob && documentLike && typeof URL.createObjectURL === "function") {
|
|
4036
|
+
const link = documentLike.createElement("a");
|
|
4037
|
+
link.href = URL.createObjectURL(blob);
|
|
4038
|
+
link.download = `${chosen.sound}-${chosen.environment}-${chosen.distance}m.webm`;
|
|
4039
|
+
link.click();
|
|
4040
|
+
run = "recorded";
|
|
4041
|
+
} else run = blob ? "recorded" : "failed";
|
|
4042
|
+
};
|
|
4043
|
+
const approve = () => {
|
|
4044
|
+
approvals.add(chosen.sound);
|
|
4045
|
+
results = results.map((result) => result.id === "C10" ? { ...result, pass: true, value: text.words.approved } : result);
|
|
4046
|
+
host?.log(`${chosen.sound}: C10 ${text.words.approved}`, { sound: chosen.sound }, "sound");
|
|
4047
|
+
};
|
|
4048
|
+
const prefer = (side) => {
|
|
4049
|
+
verdicts[chosen.sound] = side;
|
|
4050
|
+
const pair = pairOf(chosen.sound);
|
|
4051
|
+
host?.log(`${chosen.sound}: ${text.words.prefers} ${side}`, { sound: chosen.sound, prefers: side, a: pair.a, b: pair.b }, "sound");
|
|
4052
|
+
};
|
|
4053
|
+
const controls = () => [
|
|
4054
|
+
{ kind: "choice", id: "category", name: text.controls.category, options: [...SFX_CATEGORIES, CALIBRATION_CATEGORY].map((id) => ({ id, name: text.categories[id] ?? id })), default: chosen.category },
|
|
4055
|
+
{ kind: "choice", id: "sound", name: text.controls.sound, hint: text.hints.sound, options: soundOptions(), default: chosen.sound },
|
|
4056
|
+
{ kind: "action", id: "play", name: text.controls.play },
|
|
4057
|
+
{ kind: "action", id: "play_user", name: text.controls.play_user, hint: text.hints.play_user },
|
|
4058
|
+
{ kind: "choice", id: "material", name: text.controls.material, options: SFX_MATERIALS.map(([id]) => ({ id, name: text.materials[id] })), default: chosen.material },
|
|
4059
|
+
{ kind: "choice", id: "blow", name: text.controls.blow, options: SFX_BLOWS.map((id) => ({ id, name: text.blows[id] })), default: chosen.blow },
|
|
4060
|
+
{ kind: "action", id: "strike", name: text.controls.strike, hint: text.hints.strike },
|
|
4061
|
+
{ kind: "choice", id: "distance", name: text.controls.distance, options: DISTANCES2.map((meters) => ({ id: String(meters), name: `${meters} m` })), default: String(chosen.distance) },
|
|
4062
|
+
{ kind: "number", id: "bearing", name: text.controls.bearing, unit: "\xB0", min: -180, max: 180, step: 15, default: chosen.bearing },
|
|
4063
|
+
{ kind: "choice", id: "environment", name: text.controls.environment, options: SAMPLE_ENVIRONMENTS.map((id) => ({ id, name: text.environments[id] })), default: chosen.environment },
|
|
4064
|
+
{ kind: "choice", id: "strength", name: text.controls.strength, options: Object.keys(SOUND_STRENGTHS).map((id) => ({ id, name: text.strengths[id] })), default: chosen.strength },
|
|
4065
|
+
{ kind: "boolean", id: "wall", name: text.controls.wall, hint: text.hints.wall, default: chosen.wall },
|
|
4066
|
+
{ kind: "action", id: "burst", name: text.controls.burst, hint: text.hints.burst },
|
|
4067
|
+
{ kind: "action", id: "play_a", name: text.controls.play_a },
|
|
4068
|
+
{ kind: "action", id: "play_b", name: text.controls.play_b },
|
|
4069
|
+
{ kind: "action", id: "ab_cycle", name: text.controls.ab_cycle, hint: text.hints.ab_cycle },
|
|
4070
|
+
{ kind: "action", id: "blind", name: text.controls.blind, hint: text.hints.blind },
|
|
4071
|
+
{ kind: "action", id: "reveal", name: text.controls.reveal },
|
|
4072
|
+
{ kind: "action", id: "prefer_a", name: text.controls.prefer_a },
|
|
4073
|
+
{ kind: "action", id: "prefer_b", name: text.controls.prefer_b },
|
|
4074
|
+
{ kind: "action", id: "measure", name: text.controls.measure, hint: text.hints.measure },
|
|
4075
|
+
{ kind: "action", id: "record", name: text.controls.record, hint: text.hints.record },
|
|
4076
|
+
{ kind: "action", id: "approve", name: text.controls.approve, hint: text.hints.approve }
|
|
4077
|
+
];
|
|
4078
|
+
const line = (id) => {
|
|
4079
|
+
const result = results.find((entry) => entry.id === id);
|
|
4080
|
+
if (!result) return text.words.none;
|
|
4081
|
+
return `${result.pass === null ? "\u25CC" : result.pass ? "\u2713" : "\u2717"} ${result.value}`;
|
|
4082
|
+
};
|
|
4083
|
+
const counts = () => {
|
|
4084
|
+
const states = catalog.map((entry) => sfxState(entry, registry()));
|
|
4085
|
+
const count = (state) => states.filter((each) => each === state).length;
|
|
4086
|
+
return `${text.states.file} ${count("file")} \xB7 ${text.states.synth} ${count("synth")} \xB7 ${text.states.missing} ${count("missing")}`;
|
|
4087
|
+
};
|
|
4088
|
+
const strikeId = () => impactSoundId(chosen.material, chosen.blow);
|
|
4089
|
+
const lastLine = () => {
|
|
4090
|
+
const last = log.at(-1);
|
|
4091
|
+
if (!last) return text.words.none;
|
|
4092
|
+
const files = last.layers?.map((layer) => layer.url.split("/").pop()).join("+") ?? "";
|
|
4093
|
+
return `${last.kind}:${last.fallbackOf ? `${last.fallbackOf}\u2192` : ""}${last.id}${last.tier ? `/${last.tier}` : ""} ${files}`.trim();
|
|
4094
|
+
};
|
|
4095
|
+
const sourceLine = () => {
|
|
4096
|
+
const last = log.at(-1);
|
|
4097
|
+
if (!last) return text.words.none;
|
|
4098
|
+
const file = last.layers?.find((layer) => layer.role === "close")?.url.split("/").pop() ?? last.id;
|
|
4099
|
+
return `${text.sources[soundSourceOf(last)]} \xB7 ${last.id.replace(USER_SOUND_PREFIX, "")} \xB7 ${file}`;
|
|
4100
|
+
};
|
|
4101
|
+
const metrics = () => [
|
|
4102
|
+
{ id: "source", name: text.metrics.source, read: sourceLine },
|
|
4103
|
+
{ id: "count", name: text.metrics.count, read: counts },
|
|
4104
|
+
{ id: "chosen", name: text.metrics.chosen, read: () => `${label(chosen.sound)}${registry().sample(chosen.sound) ? ` (${engine.sampleStatus(chosen.sound)})` : ""}` },
|
|
4105
|
+
{ id: "matrix", name: text.metrics.matrix, read: () => `${strikeId()} \xB7 ${text.states[stateOf(strikeId())]}${pairOf(strikeId()).a ? ` \xB7 A=${pairOf(strikeId()).a}` : ""}` },
|
|
4106
|
+
{ id: "ab", name: text.metrics.ab, read: () => `A ${pairOf(chosen.sound).a ?? text.words.none} \xB7 B ${pairOf(chosen.sound).b ?? text.words.none}` },
|
|
4107
|
+
{ id: "blind_state", name: text.metrics.blind, read: () => !blind ? text.words.none : blind.revealed ? `X = ${blind.side}` : text.words.hidden },
|
|
4108
|
+
{ id: "verdicts", name: text.metrics.verdicts, read: () => Object.entries(verdicts).map(([id, side]) => `${id} ${text.words.prefers} ${side}`).join("; ") || text.words.none },
|
|
4109
|
+
{ id: "last", name: text.metrics.last, read: lastLine },
|
|
4110
|
+
{ id: "state", name: text.metrics.state, read: () => text.run[run] },
|
|
4111
|
+
...CRITERIA.map((id) => ({ id: `criterion_${id.toLowerCase()}`, name: text.criteria[id], read: () => line(id) }))
|
|
4112
|
+
];
|
|
4113
|
+
const act = (id) => {
|
|
4114
|
+
if (id === "play") play();
|
|
4115
|
+
else if (id === "play_user") void playUser();
|
|
4116
|
+
else if (id === "strike") playId(preferred(strikeId()));
|
|
4117
|
+
else if (id === "burst") for (let index = 0; index < 20; index++) playId(preferred(chosen.sound), { delay: index * 0.06 });
|
|
4118
|
+
else if (id === "play_a") playA();
|
|
4119
|
+
else if (id === "play_b") void playB();
|
|
4120
|
+
else if (id === "ab_cycle") {
|
|
4121
|
+
playA(0);
|
|
4122
|
+
void playB(1).then(() => {
|
|
4123
|
+
playA(2);
|
|
4124
|
+
return playB(3);
|
|
4125
|
+
});
|
|
4126
|
+
} else if (id === "blind") {
|
|
4127
|
+
const side = Math.random() < 0.5 ? "A" : "B";
|
|
4128
|
+
blind = { side, revealed: false };
|
|
4129
|
+
if (side === "A") playA();
|
|
4130
|
+
else void playB();
|
|
4131
|
+
} else if (id === "reveal" && blind) blind.revealed = true;
|
|
4132
|
+
else if (id === "prefer_a") prefer("A");
|
|
4133
|
+
else if (id === "prefer_b") prefer("B");
|
|
4134
|
+
else if (id === "measure") void measure();
|
|
4135
|
+
else if (id === "record") void record();
|
|
4136
|
+
else if (id === "approve") approve();
|
|
4137
|
+
};
|
|
4138
|
+
const onChange = (change) => {
|
|
4139
|
+
if (change.kind === "action") return act(change.id);
|
|
4140
|
+
if (change.kind !== "control") return;
|
|
4141
|
+
const { id, value } = change;
|
|
4142
|
+
if (id === "category" && typeof value === "string") {
|
|
4143
|
+
chosen.category = value;
|
|
4144
|
+
const first = soundsOf(value)[0];
|
|
4145
|
+
if (first) chosen.sound = first;
|
|
4146
|
+
host?.bench?.patch("sound", { options: soundOptions(), value: chosen.sound });
|
|
4147
|
+
results = [];
|
|
4148
|
+
} else if (id === "sound" && typeof value === "string") {
|
|
4149
|
+
chosen.sound = value;
|
|
4150
|
+
results = [];
|
|
4151
|
+
if (registry().sample(value)) void engine.loadSamples([value]);
|
|
4152
|
+
} else if (id === "material" && typeof value === "string") chosen.material = value;
|
|
4153
|
+
else if (id === "blow" && typeof value === "string") chosen.blow = value;
|
|
4154
|
+
else if (id === "distance" && typeof value === "string") chosen.distance = Number(value);
|
|
4155
|
+
else if (id === "bearing" && typeof value === "number") chosen.bearing = value;
|
|
4156
|
+
else if (id === "environment" && (value === "indoor" || value === "outdoor")) chosen.environment = value;
|
|
4157
|
+
else if (id === "strength" && typeof value === "string" && value in SOUND_STRENGTHS) chosen.strength = value;
|
|
4158
|
+
else if (id === "wall" && typeof value === "boolean") chosen.wall = value;
|
|
4159
|
+
placeScenery();
|
|
4160
|
+
};
|
|
4161
|
+
const items = () => {
|
|
4162
|
+
const marked = (id, name, build) => ({
|
|
4163
|
+
id,
|
|
4164
|
+
name,
|
|
4165
|
+
movable: false,
|
|
4166
|
+
build: (THREE) => {
|
|
4167
|
+
const root = build(THREE);
|
|
4168
|
+
root.userData = root.userData ?? {};
|
|
4169
|
+
root.userData[id] = true;
|
|
4170
|
+
return root;
|
|
4171
|
+
}
|
|
4172
|
+
});
|
|
4173
|
+
return [...options.probe ? [marked(PROBE_ID2, text.probe, options.probe)] : [], ...options.wall ? [marked(WALL_ID, text.wall, options.wall)] : []];
|
|
4174
|
+
};
|
|
4175
|
+
const area = () => ({
|
|
4176
|
+
placements: [
|
|
4177
|
+
...options.probe ? [{ item: PROBE_ID2, position: offset({ position: EYE, yaw: 0 }, Math.min(chosen.distance, DRAWN_MAX)), asleep: true }] : [],
|
|
4178
|
+
...options.wall ? [{ item: WALL_ID, position: [0, 0, WALL_AT], asleep: true }] : []
|
|
4179
|
+
],
|
|
4180
|
+
view: { eye: EYE, target: [0, 1.2, -8] }
|
|
4181
|
+
});
|
|
4182
|
+
const scenarios = () => [
|
|
4183
|
+
{
|
|
4184
|
+
id: "sample_pipeline",
|
|
4185
|
+
name: text.scenarios.pipeline,
|
|
4186
|
+
run: async ({ expect }) => {
|
|
4187
|
+
const report = await engine.loadSamples([CALIBRATION_ID]);
|
|
4188
|
+
expect(report.failed.length === 0 && engine.sampleStatus(CALIBRATION_ID) === "ready", "the calibration files load and decode", { failed: report.failed });
|
|
4189
|
+
const started = [];
|
|
4190
|
+
const offStarted = engine.watchVoices((event) => void started.push(event));
|
|
4191
|
+
const handles = Array.from({ length: 6 }, (_, index) => engine.play(CALIBRATION_ID, { distance: 2, environment: index % 2 === 0 ? "indoor" : "outdoor" }));
|
|
4192
|
+
offStarted();
|
|
4193
|
+
expect(handles.every((handle) => handle?.kind === "sample"), "it plays the files, not the fallback", { kinds: handles.map((handle) => handle?.kind) });
|
|
4194
|
+
const events = started.filter((event) => event.id === CALIBRATION_ID);
|
|
4195
|
+
const takes = events.map((event) => event.layers?.find((layer) => layer.role === "close")?.url);
|
|
4196
|
+
expect(takes.length === 6 && takes.every((take, index) => index === 0 || take !== takes[index - 1]), "never the same take twice in a row", { takes });
|
|
4197
|
+
const tails = events.map((event) => event.layers?.find((layer) => layer.role === "tail")?.url ?? "");
|
|
4198
|
+
expect(tails[0]?.endsWith("tail_in.wav") === true && tails[1]?.endsWith("tail_out.wav") === true, "the tail follows the place", { tails });
|
|
4199
|
+
for (const handle of handles) handle?.stop();
|
|
4200
|
+
}
|
|
4201
|
+
},
|
|
4202
|
+
{
|
|
4203
|
+
id: "sample_criteria",
|
|
4204
|
+
name: text.scenarios.criteria,
|
|
4205
|
+
run: async ({ expect }) => {
|
|
4206
|
+
const previous = chosen.sound;
|
|
4207
|
+
chosen.sound = CALIBRATION_ID;
|
|
4208
|
+
const measured = await measure();
|
|
4209
|
+
chosen.sound = previous;
|
|
4210
|
+
for (const result of measured) if (result.id !== "C10") expect(result.pass === true, `${result.id}: ${result.target}`, { value: result.value, data: result.data });
|
|
4211
|
+
}
|
|
4212
|
+
},
|
|
4213
|
+
{
|
|
4214
|
+
id: "sample_loop",
|
|
4215
|
+
name: text.scenarios.loop,
|
|
4216
|
+
run: async ({ expect }) => {
|
|
4217
|
+
const report = await engine.loadSamples([CALIBRATION_LOOP_ID]);
|
|
4218
|
+
expect(report.failed.length === 0, "the loop takes load and decode", { failed: report.failed });
|
|
4219
|
+
engine.unlock();
|
|
4220
|
+
engine.level("ambience");
|
|
4221
|
+
const loop = engine.loop(CALIBRATION_LOOP_ID, { volume: 0.8 });
|
|
4222
|
+
const wait = (ms2) => new Promise((resolve) => setTimeout(resolve, ms2));
|
|
4223
|
+
const started = performance.now();
|
|
4224
|
+
while (engine.level("ambience") < 0.01 && performance.now() - started < 2e3) await wait(20);
|
|
4225
|
+
expect(performance.now() - started < 2e3, "the loop starts within 2 s", { ms: Math.round(performance.now() - started) });
|
|
4226
|
+
await wait(200);
|
|
4227
|
+
const levels = [];
|
|
4228
|
+
for (let index = 0; index < 60; index++) {
|
|
4229
|
+
levels.push(engine.level("ambience"));
|
|
4230
|
+
await wait(40);
|
|
4231
|
+
}
|
|
4232
|
+
loop.stop();
|
|
4233
|
+
const sorted = [...levels].sort((a, b) => a - b);
|
|
4234
|
+
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
4235
|
+
const lowest = sorted[0] ?? 0;
|
|
4236
|
+
const dip = lowest > 0 && median > 0 ? 20 * Math.log10(lowest / median) : -Infinity;
|
|
4237
|
+
expect(median > 0.01, "the loop sounds", { median });
|
|
4238
|
+
expect(dip >= -6, "no seam: the level never dips more than 6 dB where two takes cross", { dip: Number(dip.toFixed(1)), lowest, median, levels: levels.map((level) => Number(level.toFixed(3))) });
|
|
4239
|
+
await wait(200);
|
|
4240
|
+
expect(engine.level("ambience") < median / 4, "stop silences it", { after: engine.level("ambience") });
|
|
4241
|
+
}
|
|
4242
|
+
},
|
|
4243
|
+
{
|
|
4244
|
+
id: "sound_panel",
|
|
4245
|
+
name: text.scenarios.panel,
|
|
4246
|
+
run: ({ expect }) => {
|
|
4247
|
+
const categories = SFX_CATEGORIES.map((category) => soundsOf(category).length);
|
|
4248
|
+
expect(categories.every((count) => count > 0), "every category lists its sounds", { categories });
|
|
4249
|
+
expect(soundsOf("impactos").length >= SFX_MATERIALS.length * SFX_BLOWS.length, "the matrix has every material \xD7 blow");
|
|
4250
|
+
const pair = pairOf(CALIBRATION_ID);
|
|
4251
|
+
expect(pair.a === "sfx_pistol" && pair.b === CALIBRATION_ID, "A is the synthesized sound and B the file", pair);
|
|
4252
|
+
const clear = engine.planSample(CALIBRATION_ID, { position: [0, 1.6, -10], occlusion: 0, seed: 1 });
|
|
4253
|
+
const walled = engine.planSample(CALIBRATION_ID, { position: [0, 1.6, -10], occlusion: 1, seed: 1 });
|
|
4254
|
+
expect((walled?.cutoff ?? Infinity) < (clear?.cutoff ?? 0) / 5, "behind the wall the sound is muffled", { clear: clear?.cutoff, walled: walled?.cutoff });
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
];
|
|
4258
|
+
return {
|
|
4259
|
+
id: "sound_check_area",
|
|
4260
|
+
name: text.name,
|
|
4261
|
+
group: text.group,
|
|
4262
|
+
ground: { surface: "concrete_slab" },
|
|
4263
|
+
get engine() {
|
|
4264
|
+
return engine;
|
|
4265
|
+
},
|
|
4266
|
+
items: () => items(),
|
|
4267
|
+
area: () => area(),
|
|
4268
|
+
controls: () => controls(),
|
|
4269
|
+
metrics: () => metrics(),
|
|
4270
|
+
scenarios: () => scenarios(),
|
|
4271
|
+
measure,
|
|
4272
|
+
results: () => results,
|
|
4273
|
+
events: () => log,
|
|
4274
|
+
play,
|
|
4275
|
+
pair: () => pairOf(chosen.sound),
|
|
4276
|
+
verdicts: () => ({ ...verdicts }),
|
|
4277
|
+
approvals: () => [...approvals],
|
|
4278
|
+
// the engine (and its context) is made on the first play or measure, not when the room loads the module
|
|
4279
|
+
attach(lent) {
|
|
4280
|
+
host = lent;
|
|
4281
|
+
const shared = options.engine ? void 0 : lent.audio?.engine;
|
|
4282
|
+
if (shared && shared !== engine) {
|
|
4283
|
+
for (const bank of ownBanks) shared.register(bank);
|
|
4284
|
+
shared.addSampleSource(isCalibrationUrl, loader);
|
|
4285
|
+
engine = shared;
|
|
4286
|
+
}
|
|
4287
|
+
const offVoices = engine.watchVoices((event) => {
|
|
4288
|
+
log.push(event);
|
|
4289
|
+
if (log.length > LOG_LIMIT) log.splice(0, log.length - LOG_LIMIT);
|
|
4290
|
+
});
|
|
4291
|
+
lent.bench?.patch("sound", { options: soundOptions() });
|
|
4292
|
+
const off = lent.bench?.onChange(onChange);
|
|
4293
|
+
placeScenery();
|
|
4294
|
+
return () => {
|
|
4295
|
+
off?.();
|
|
4296
|
+
offVoices();
|
|
4297
|
+
host = null;
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
export {
|
|
4304
|
+
createRng,
|
|
4305
|
+
streamSeed,
|
|
4306
|
+
streamFor,
|
|
4307
|
+
drawBetween,
|
|
4308
|
+
BUS_IDS,
|
|
4309
|
+
BUILD_ORDERS,
|
|
4310
|
+
DISTANCE_CURVES,
|
|
4311
|
+
falloffGain,
|
|
4312
|
+
isSilent,
|
|
4313
|
+
cutoffDivisor,
|
|
4314
|
+
DEFAULT_LISTENER,
|
|
4315
|
+
PAN_LIMIT,
|
|
4316
|
+
PAN_MIN_DISTANCE,
|
|
4317
|
+
listenerDistance,
|
|
4318
|
+
panFor,
|
|
4319
|
+
VOICE_GROUPS,
|
|
4320
|
+
BUS_PRIORITY,
|
|
4321
|
+
DEFAULT_MAX_VOICES,
|
|
4322
|
+
busPriority,
|
|
4323
|
+
createVoiceManager,
|
|
4324
|
+
planSound,
|
|
4325
|
+
SAMPLE_ENVIRONMENTS,
|
|
4326
|
+
DEFAULT_SAMPLE_FALLOFF,
|
|
4327
|
+
DEFAULT_CROSSOVER,
|
|
4328
|
+
DEFAULT_SAMPLE_PITCH,
|
|
4329
|
+
DEFAULT_TAIL_GAIN,
|
|
4330
|
+
DEFAULT_SAMPLE_CUTOFF_K,
|
|
4331
|
+
OPEN_CUTOFF,
|
|
4332
|
+
LAYER_FLOOR,
|
|
4333
|
+
pickTake,
|
|
4334
|
+
farMixAt,
|
|
4335
|
+
crossGains,
|
|
4336
|
+
sampleCutoff,
|
|
4337
|
+
sampleUrls,
|
|
4338
|
+
tierFor,
|
|
4339
|
+
planSample,
|
|
4340
|
+
CALIBRATION_ID,
|
|
4341
|
+
encodeWav,
|
|
4342
|
+
CALIBRATION_SOUND,
|
|
4343
|
+
CALIBRATION_LOOP_ID,
|
|
4344
|
+
CALIBRATION_LOOP_SOUND,
|
|
4345
|
+
isCalibrationUrl,
|
|
4346
|
+
calibrationLoader,
|
|
4347
|
+
createSoundRegistry,
|
|
4348
|
+
MIX_PROFILES,
|
|
4349
|
+
DEFAULT_MIX,
|
|
4350
|
+
resolveMix,
|
|
4351
|
+
busGain,
|
|
4352
|
+
fillNoise,
|
|
4353
|
+
fillImpulse,
|
|
4354
|
+
bufferOf,
|
|
4355
|
+
buildChain,
|
|
4356
|
+
globalContext,
|
|
4357
|
+
unlockContext,
|
|
4358
|
+
LOOP_START_ABOVE,
|
|
4359
|
+
LOOP_SMOOTHING,
|
|
4360
|
+
createLoopState,
|
|
4361
|
+
DEFAULT_LOOP_CROSSFADE,
|
|
4362
|
+
loopCrossfade,
|
|
4363
|
+
CROSSFADE_STEPS,
|
|
4364
|
+
equalPowerIn,
|
|
4365
|
+
realize,
|
|
4366
|
+
retunePlan,
|
|
4367
|
+
createSetPicker,
|
|
4368
|
+
fetchLoader,
|
|
4369
|
+
inlineLoader,
|
|
4370
|
+
createSampleStore,
|
|
4371
|
+
realizeSample,
|
|
4372
|
+
sampleLength,
|
|
4373
|
+
limiterCurve2 as limiterCurve,
|
|
4374
|
+
createAudioEngine,
|
|
4375
|
+
dbOf,
|
|
4376
|
+
peakOf,
|
|
4377
|
+
peakIndex,
|
|
4378
|
+
onsetIndex,
|
|
4379
|
+
rmsDb,
|
|
4380
|
+
centroidHz,
|
|
4381
|
+
balanceOf,
|
|
4382
|
+
ringSeconds,
|
|
4383
|
+
countHits,
|
|
4384
|
+
endDropDb,
|
|
4385
|
+
CRITERIA,
|
|
4386
|
+
CRITERIA_TARGETS,
|
|
4387
|
+
measureCriteria,
|
|
4388
|
+
recordOutput,
|
|
4389
|
+
SFX_CATEGORIES,
|
|
4390
|
+
SFX_CATALOG,
|
|
4391
|
+
SFX_MATERIALS,
|
|
4392
|
+
SFX_BLOWS,
|
|
4393
|
+
impactSoundId,
|
|
4394
|
+
SFX_IMPACTS,
|
|
4395
|
+
sfxState,
|
|
4396
|
+
SDK_LICENSES,
|
|
4397
|
+
SFX_LAYERS,
|
|
4398
|
+
SFX_TIERS,
|
|
4399
|
+
parseSfxName,
|
|
4400
|
+
splitTier,
|
|
4401
|
+
sampleBankFromManifest,
|
|
4402
|
+
KIT_CATEGORY_BUS,
|
|
4403
|
+
KIT_SAMPLE_ALIASES,
|
|
4404
|
+
kitSampleDefaults,
|
|
4405
|
+
withAliases,
|
|
4406
|
+
kitSampleBank,
|
|
4407
|
+
loadKitSamples,
|
|
4408
|
+
USER_SAMPLES_BANK,
|
|
4409
|
+
KIT_SAMPLES_BANK,
|
|
4410
|
+
USER_SOUND_PREFIX,
|
|
4411
|
+
soundSourceOf,
|
|
4412
|
+
userSampleBank,
|
|
4413
|
+
loadUserSamples,
|
|
4414
|
+
GAME_SFX_CLASSES,
|
|
4415
|
+
GAME_SFX_FALLBACK,
|
|
4416
|
+
noiseClassRecipe,
|
|
4417
|
+
GAME_SFX_BANK,
|
|
4418
|
+
LIBRARY_SOUNDS,
|
|
4419
|
+
LIBRARY_LOOPS,
|
|
4420
|
+
LIBRARY_BANK,
|
|
4421
|
+
audioBenchTextOf,
|
|
4422
|
+
BENCH_BANK,
|
|
4423
|
+
createAudioTestModule,
|
|
4424
|
+
soundCheckTextOf,
|
|
4425
|
+
SOUND_STRENGTHS,
|
|
4426
|
+
CALIBRATION_CATEGORY,
|
|
4427
|
+
createSoundCheckModule
|
|
4428
|
+
};
|
|
4429
|
+
//# sourceMappingURL=chunk-KBTUANNG.js.map
|