/** * Copyright (C) 2014-2016 Triumph LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ "use strict"; /** * Sound effects internal API. * @name sfx * @namespace * @exports exports as sfx */ b4w.module["__sfx"] = function(exports, require) { var m_cfg = require("__config"); var m_print = require("__print"); var m_quat = require("__quat"); var m_time = require("__time"); var m_tsr = require("__tsr"); var m_util = require("__util"); var m_vec3 = require("__vec3"); var cfg_def = m_cfg.defaults; var cfg_sfx = m_cfg.sfx; var DOPPLER_SMOOTH_PERIOD = 0.3; var SPKSTATE_UNDEFINED = 10; var SPKSTATE_PLAY = 20; var SPKSTATE_STOP = 30; var SPKSTATE_PAUSE = 40; var SPKSTATE_FINISH = 50; var SCHED_PARAM_LOOPS = 5; var SCHED_PARAM_ANTICIPATE_TIME = 3.0; var _vec3_tmp = new Float32Array(3); var _vec3_tmp2 = new Float32Array(3); var _vec3_tmp3 = new Float32Array(3); var _quat_tmp = m_quat.create(); // permanent vars var _supported_audio = []; var _supported_video = []; var _wa = null; // per-loaded-scene vars var _active_scene = null; var _speaker_objects = []; var _seed_tmp = [1]; var _playlist = null; // audio source types exports.AST_NONE = 10; exports.AST_ARRAY_BUFFER = 20; exports.AST_HTML_ELEMENT = 30; exports.create_sfx = function() { var sfx = { uuid: -1, filepath: "", behavior: "NONE", muted: false, volume: 1, pitch: 1, attenuation: 1, dist_ref: 1, dist_max: 10000, cone_angle_inner: 360, cone_angle_outer: 360, cone_volume_outer: 1, autoplay: false, cyclic: false, loop: false, // buffer time loop_start: 0, loop_end: 0, delay: 0, delay_random: 0, volume_random: 0, pitch_random: 0, fade_in: 0, fade_out: 0, start_time: 0, pause_time: 0, buf_offset: 0, duration: 0, vp_rand_end_time: 0, base_seed: 1, src: null, // initial state state: SPKSTATE_UNDEFINED, last_position: new Float32Array(3), velocity: new Float32Array(3), enable_doppler: false, last_doppler_shift: 1, // for BACKGROUND_MUSIC bgm_start_timeout: -1, bgm_stop_timeout: -1, duck_time: 0, // nodes proc_chain_in: null, source_node: null, source_node2: null, panner_node: null, filter_node: null, gain_node: null, fade_gain_node: null, rand_gain_node: null, update_counter: 0 } return sfx; } /** * Initialize sound effects module */ exports.init = function() { // NOTE: DOM Exception 5 if not found var audio = document.createElement("audio"); var video = document.createElement("video"); // do not detect codecs here, simply follow the rules: // ogg - vorbis // mp3 - mp3 // mp4 - aac if (audio.canPlayType) { if (audio.canPlayType("audio/ogg") != "") { _supported_audio.push("ogg"); _supported_audio.push("ogv"); _supported_audio.push("oga"); } if (audio.canPlayType("audio/mpeg") != "") _supported_audio.push("mp3"); if (audio.canPlayType("audio/mp4") != "") { _supported_audio.push("mp4"); _supported_audio.push("m4v"); _supported_audio.push("m4a"); } if (audio.canPlayType("audio/webm") != "") _supported_audio.push("webm"); } if (video.canPlayType) { if (video.canPlayType("video/ogg") != "") { _supported_video.push("ogv"); _supported_video.push("ogg"); _supported_video.push("oga"); } if (video.canPlayType("video/mp4") != "") { _supported_video.push("m4v"); _supported_video.push("mp4"); } if (video.canPlayType("video/webm") != "") _supported_video.push("webm"); if (video.canPlayType("video/mpeg") != "") _supported_video.push("mp3"); } } exports.attach_scene_sfx = function(scene) { // NOTE: register context once and reuse for all loaded scenes to prevent // out-of-resources error due to Chromium context leaks if (cfg_sfx.webaudio && !_wa) { _wa = create_wa_context(); if (_wa) m_print.log("%cINIT WEBAUDIO: " + _wa.sampleRate + "Hz", "color: #00a"); } if (_wa) { var scene_sfx = { listener_last_eye : new Float32Array(3), listener_velocity : new Float32Array(3), update_counter: 0 }; var gnode = _wa.createGain(); var fade_gnode = _wa.createGain(); scene_sfx.gain_node = gnode; scene_sfx.fade_gain_node = fade_gnode; gnode.connect(fade_gnode); fade_gnode.connect(_wa.destination); if (scene["b4w_enable_dynamic_compressor"]) { var compressor = _wa.createDynamicsCompressor(); var dcs = scene["b4w_dynamic_compressor_settings"]; compressor.threshold.value = dcs["threshold"]; compressor.knee.value = dcs["knee"]; compressor.ratio.value = dcs["ratio"]; compressor.attack.value = dcs["attack"]; compressor.release.value = dcs["release"]; compressor.connect(gnode); scene_sfx.compressor_node = compressor; scene_sfx.proc_chain_in = compressor; } else { scene_sfx.compressor_node = null; scene_sfx.proc_chain_in = gnode; } switch (scene["audio_distance_model"]) { case "INVERSE": case "INVERSE_CLAMPED": scene_sfx.distance_model = "inverse"; break; case "LINEAR": case "LINEAR_CLAMPED": scene_sfx.distance_model = "linear"; break; case "EXPONENT": case "EXPONENT_CLAMPED": scene_sfx.distance_model = "exponential"; break; case "NONE": scene_sfx.distance_model = "none"; break; default: m_util.panic("Wrong audio distance model"); } scene_sfx.doppler_factor = scene["audio_doppler_factor"]; scene_sfx.speed_of_sound = scene["audio_doppler_speed"]; scene_sfx.muted = false; scene_sfx.volume = scene["audio_volume"]; gnode.gain.value = calc_gain(scene_sfx); scene_sfx.duck_time = 0; } else var scene_sfx = null; scene._sfx = scene_sfx; } function create_wa_context() { var AudioContext = window.AudioContext || window.webkitAudioContext; if (AudioContext) { try { var ctx = new AudioContext(); } catch (e) { m_print.error("Unable to initialize AudioContext: \"" + e + "\". The audio is disabled."); return null; } // simple WebAudio version check if (ctx.createGain) { return ctx; } else { m_print.warn("deprecated WebAudio implementation"); return null; } } else { m_print.warn("WebAudio is not supported"); return null; } } exports.set_active_scene = function(scene) { _active_scene = scene; } exports.detect_audio_container = function(extension) { if (!extension) var extension = "ogg"; // only one fallback required in most cases // requested hint is supported if (_supported_audio.indexOf(extension) > -1) return extension; else if (_supported_audio.indexOf("m4a") > -1) return "m4a"; else if (_supported_audio.indexOf("oga") > -1) return "oga"; else return ""; } exports.detect_video_container = function(extension) { if (!extension) var extension = "webm"; // only one fallback required in most cases // requested hint is supported if (_supported_video.indexOf(extension) > -1) return extension; else if (_supported_video.indexOf("m4v") > -1) return "m4v"; else if (_supported_video.indexOf("webm") > -1) return "webm"; else return ""; } /** * Update speaker object from bpy_object, adding some properties */ exports.update_object = function(bpy_obj, obj) { var speaker = bpy_obj["data"]; var sfx = obj.sfx; sfx.uuid = bpy_obj["data"]["sound"]["uuid"]; sfx.filepath = bpy_obj["data"]["sound"]["filepath"]; switch (speaker["b4w_behavior"]) { case "POSITIONAL": case "BACKGROUND_SOUND": sfx.behavior = _wa ? speaker["b4w_behavior"] : "NONE"; break; case "BACKGROUND_MUSIC": sfx.behavior = _wa ? (check_media_element_node() && !cfg_def.chrome_html_bkg_music_hack ? "BACKGROUND_MUSIC" : "BACKGROUND_SOUND") : "NONE"; break; default: m_util.panic("Wrong speaker behavior"); break; } // NOTE: temporary compatibility actions: allow speakers without sound if (!speaker["sound"]) sfx.behavior = "NONE"; sfx.enable_doppler = speaker["b4w_enable_doppler"]; sfx.muted = speaker["muted"]; sfx.volume = speaker["volume"]; sfx.pitch = speaker["pitch"]; sfx.attenuation = speaker["attenuation"]; sfx.dist_ref = speaker["distance_reference"]; sfx.dist_max = speaker["distance_max"] || 10000; // spec def sfx.cone_angle_inner = speaker["cone_angle_inner"]; sfx.cone_angle_outer = speaker["cone_angle_outer"]; sfx.cone_volume_outer = speaker["cone_volume_outer"]; sfx.autoplay = speaker["b4w_auto_play"]; sfx.cyclic = speaker["b4w_cyclic_play"]; sfx.loop = speaker["b4w_loop"]; sfx.loop_start = speaker["b4w_loop_start"]; sfx.loop_end = speaker["b4w_loop_end"]; sfx.delay = speaker["b4w_delay"]; sfx.delay_random = speaker["b4w_delay_random"]; sfx.volume_random = speaker["b4w_volume_random"]; sfx.pitch_random = speaker["b4w_pitch_random"]; sfx.fade_in = speaker["b4w_fade_in"]; sfx.fade_out = speaker["b4w_fade_out"]; _speaker_objects.push(obj); } function check_media_element_node() { if (window.MediaElementAudioSourceNode) { return true; } else { m_print.warn("MediaElementAudioSourceNode not found"); return false; } } /** * Returns audio source type for given object (AST_*) * @param {Object3D} obj Object 3D */ exports.source_type = function(obj) { if (obj.type != "SPEAKER") m_util.panic("Wrong object type"); switch (obj.sfx.behavior) { case "POSITIONAL": return exports.AST_ARRAY_BUFFER; case "BACKGROUND_SOUND": return exports.AST_ARRAY_BUFFER; case "BACKGROUND_MUSIC": return exports.AST_HTML_ELEMENT; case "NONE": return exports.AST_NONE; default: m_util.panic("Wrong speaker behavior"); } } /** * Updates speaker object with loaded sound data * @param {Object3D} obj Object 3D * @param {ArrayBuffer|