/**
* 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|