Procházet zdrojové kódy

Implement limited spatialization for WebAudio (#712)

Pavel Alexandrov před 5 roky
rodič
revize
fdcb3e02d5

+ 40 - 15
hxd/snd/webaudio/AudioTypes.hx

@@ -16,6 +16,7 @@ class SourceHandle {
 
 
 	public var driver : Driver;
 	public var driver : Driver;
 	public var lowPass : BiquadFilterNode;
 	public var lowPass : BiquadFilterNode;
+	public var panner : PannerNode;
 	public var gain : GainNode;
 	public var gain : GainNode;
 	public var destination : AudioNode;
 	public var destination : AudioNode;
 	public var buffers : Array<BufferPlayback>;
 	public var buffers : Array<BufferPlayback>;
@@ -30,16 +31,19 @@ class SourceHandle {
 	}
 	}
 
 
 	public function updateDestination() {
 	public function updateDestination() {
-		if (lowPass != null) {
+		destination = gain;
+		if ( lowPass != null ) {
+			lowPass.connect(destination);
 			destination = lowPass;
 			destination = lowPass;
-			lowPass.connect(gain);
-		} else {
-			destination = gain;
+		}
+		if ( panner != null ) {
+			panner.connect(destination);
+			destination = panner;
 		}
 		}
 		gain.connect(driver.destination);
 		gain.connect(driver.destination);
 		for (b in buffers) {
 		for (b in buffers) {
 			if ( b.node != null ) {
 			if ( b.node != null ) {
-				b.node.connect(destination);
+				b.restart(this);
 			}
 			}
 		}
 		}
 	}
 	}
@@ -58,7 +62,7 @@ class BufferPlayback {
 
 
 	public var buffer : BufferHandle;
 	public var buffer : BufferHandle;
 	public var node : AudioBufferSourceNode;
 	public var node : AudioBufferSourceNode;
-	public var offset : Float;
+	public var offset : Float; // Buffer offset. Modified when applying effects.
 	public var dirty : Bool; // Playback was started - node no longer usable.
 	public var dirty : Bool; // Playback was started - node no longer usable.
 	public var consumed : Bool; // Node was played completely (ended event fired)
 	public var consumed : Bool; // Node was played completely (ended event fired)
 	public var starts : Float;
 	public var starts : Float;
@@ -68,6 +72,9 @@ class BufferPlayback {
 	
 	
 	static inline var FADE_SAMPLES = 10; // Click prevent at the start.
 	static inline var FADE_SAMPLES = 10; // Click prevent at the start.
 
 
+	var lastSamples:Int;
+	var lastTime:Float;
+
 	public function new()
 	public function new()
 	{
 	{
 
 
@@ -75,8 +82,10 @@ class BufferPlayback {
 
 
 	function get_currentSample ( ):Int {
 	function get_currentSample ( ):Int {
 		if ( consumed ) return buffer.samples;
 		if ( consumed ) return buffer.samples;
-		if ( node == null || !dirty ) return 0;
-		return Math.floor((node.context.currentTime - starts) / ((buffer.inst.duration - offset) / node.playbackRate.value) * buffer.samples);
+		if ( node == null || !dirty || node.context.currentTime < lastTime ) return 0;
+		lastSamples += Math.floor((node.context.currentTime - lastTime) * buffer.inst.sampleRate * node.playbackRate.value);
+		lastTime = node.context.currentTime;
+		return lastSamples;
 	}
 	}
 
 
 	public function set(buf : BufferHandle, grainOffset : Float) {
 	public function set(buf : BufferHandle, grainOffset : Float) {
@@ -115,23 +124,39 @@ class BufferPlayback {
 		node.connect(source.destination);
 		node.connect(source.destination);
 		node.playbackRate.value = source.pitch;
 		node.playbackRate.value = source.pitch;
 		node.start(time, offset);
 		node.start(time, offset);
+		lastSamples = 0;
+		lastTime = time;
 		starts = time;
 		starts = time;
 		return ends = time + (buffer.inst.duration - offset) / source.pitch;
 		return ends = time + (buffer.inst.duration - offset) / source.pitch;
 	}
 	}
 
 
 	public function readjust( time : Float, source : SourceHandle ) {
 	public function readjust( time : Float, source : SourceHandle ) {
 		if (consumed || node == null) return ends;
 		if (consumed || node == null) return ends;
-		node.playbackRate.value = source.pitch;
-		if (dirty) {
-			var elapsed = node.context.currentTime - starts;
-			return ends = starts + elapsed + (buffer.inst.duration - offset - elapsed) / source.pitch;
+		var ctx = source.driver.ctx;
+		var shiftTime = ctx.currentTime;// + 16 / buffer.inst.sampleRate;
+		
+		node.playbackRate.setValueAtTime(source.pitch, shiftTime);
+		var elapsed = shiftTime - starts;
+		if ( elapsed < 0 ) {
+			// Queued node that haven't started yet: Requeue.
+			return start(ctx, source, time == 0 ? shiftTime : time);
 		}
 		}
-		starts = time == 0 ? node.context.currentTime : time;
-		if (source.playing)
-			node.start(starts, offset);
+		// Stretch start position relative to new pitch.
+		starts = shiftTime - (elapsed / source.pitch);
 		return ends = starts + (buffer.inst.duration - offset) / source.pitch;
 		return ends = starts + (buffer.inst.duration - offset) / source.pitch;
 	}
 	}
 
 
+	public function restart( source : SourceHandle ) {
+		if ( consumed || node == null ) return;
+		var ctx = hxd.snd.webaudio.Context.get();
+		if ( ctx.currentTime > starts ) {
+			offset += (ctx.currentTime - starts) * source.pitch;
+			start(ctx, source, ctx.currentTime);
+		} else {
+			start(ctx, source, starts);
+		}
+	}
+
 	public function stop( immediate : Bool = true ) {
 	public function stop( immediate : Bool = true ) {
 		if ( node != null ) {
 		if ( node != null ) {
 			node.removeEventListener("ended", onBufferConsumed);
 			node.removeEventListener("ended", onBufferConsumed);

+ 5 - 3
hxd/snd/webaudio/Driver.hx

@@ -57,7 +57,9 @@ class Driver implements hxd.snd.Driver {
 	}
 	}
 
 
 	public function setListenerParams (position : h3d.Vector, direction : h3d.Vector, up : h3d.Vector, ?velocity : h3d.Vector) : Void {
 	public function setListenerParams (position : h3d.Vector, direction : h3d.Vector, up : h3d.Vector, ?velocity : h3d.Vector) : Void {
-		// Not supported
+		ctx.listener.setPosition(-position.x, position.y, position.z);
+		ctx.listener.setOrientation(-direction.x, direction.y, direction.z, -up.x, up.y, up.z);
+		// TODO: Velocity
 	}
 	}
 
 
 	public function createSource () : SourceHandle {
 	public function createSource () : SourceHandle {
@@ -193,7 +195,7 @@ class Driver implements hxd.snd.Driver {
 		if ( source.playing ) {
 		if ( source.playing ) {
 			if ( source.buffers.length != 1 ) {
 			if ( source.buffers.length != 1 ) {
 				var t = source.buffers[source.buffers.length - 2].ends;
 				var t = source.buffers[source.buffers.length - 2].ends;
-				buf.start(ctx, source, (js.Syntax.code("isFinite({0})", t):Bool) ? t : ctx.currentTime);
+				buf.start(ctx, source, (js.Syntax.code("isFinite({0})", t):Bool) ? hxd.Math.max(t, ctx.currentTime) : ctx.currentTime);
 			} else {
 			} else {
 				buf.start(ctx, source, ctx.currentTime);
 				buf.start(ctx, source, ctx.currentTime);
 			}
 			}
@@ -245,7 +247,7 @@ class Driver implements hxd.snd.Driver {
 	public function getEffectDriver(type : String) : hxd.snd.Driver.EffectDriver<Dynamic> {
 	public function getEffectDriver(type : String) : hxd.snd.Driver.EffectDriver<Dynamic> {
 		return switch(type) {
 		return switch(type) {
 			case "pitch"          : new PitchDriver();
 			case "pitch"          : new PitchDriver();
-			// case "spatialization" : new SpatializationDriver(this);
+			case "spatialization" : new SpatializationDriver();
 			case "lowpass"        : new LowPassDriver();
 			case "lowpass"        : new LowPassDriver();
 			// case "reverb"         : new ReverbDriver(this);
 			// case "reverb"         : new ReverbDriver(this);
 			default               : new hxd.snd.Driver.EffectDriver<Dynamic>();
 			default               : new hxd.snd.Driver.EffectDriver<Dynamic>();

+ 6 - 4
hxd/snd/webaudio/LowPassDriver.hx

@@ -26,11 +26,13 @@ class LowPassDriver extends EffectDriver<LowPass> {
 		return node;
 		return node;
 	}
 	}
 
 
+	override public function bind(e:LowPass, source: SourceHandle) : Void {
+		source.lowPass = get(source.driver.ctx);
+		source.updateDestination();
+		apply(e, source);
+	}
+
 	override function apply(e : LowPass, source : SourceHandle) : Void {
 	override function apply(e : LowPass, source : SourceHandle) : Void {
-		if ( source.lowPass == null ) {
-			source.lowPass = get(source.driver.ctx);
-			source.updateDestination();
-		}
 		var min = 40;
 		var min = 40;
 		var max = source.driver.ctx.sampleRate / 2;
 		var max = source.driver.ctx.sampleRate / 2;
 		var octaves = js.lib.Math.log(max / min) / js.lib.Math.LN2;
 		var octaves = js.lib.Math.log(max / min) / js.lib.Math.LN2;

+ 51 - 0
hxd/snd/webaudio/SpatializationDriver.hx

@@ -0,0 +1,51 @@
+package hxd.snd.webaudio;
+
+#if (js && !useal)
+import js.html.audio.PannerNode;
+import js.html.audio.AudioContext;
+import hxd.snd.effect.Spatialization;
+import hxd.snd.Driver.EffectDriver;
+import hxd.snd.webaudio.AudioTypes;
+
+class SpatializationDriver extends EffectDriver<Spatialization> {
+
+	var pool : Array<PannerNode>;
+
+	public function new() {
+		pool = [];
+		super();
+	}
+
+	function get( ctx : AudioContext ) {
+		if ( pool.length != 0 ) {
+			return pool.pop();
+		}
+		var node = ctx.createPanner();
+		return node;
+	}
+
+	override public function bind(e : Spatialization, source: SourceHandle) : Void {
+		source.panner = get(source.driver.ctx);
+		source.updateDestination();
+		apply(e, source);
+	}
+
+	override function apply(e : Spatialization, source : SourceHandle) : Void {
+		source.panner.setPosition(-e.position.x, e.position.y, e.position.z);
+		source.panner.setOrientation(-e.direction.x, e.direction.y, e.direction.z);
+		// TODO: Velocity
+		source.panner.rolloffFactor = e.rollOffFactor;
+		source.panner.refDistance = e.referenceDistance;
+		var maxDist : Float = e.maxDistance == null ? 3.40282347e38 : e.maxDistance;
+		source.panner.maxDistance = maxDist;
+	}
+
+	override function unbind(e : Spatialization, source : SourceHandle) : Void {
+		pool.push(source.panner);
+		source.panner.disconnect();
+		source.panner = null;
+		if ( source.driver != null )
+			source.updateDestination();
+	}
+}
+#end

+ 24 - 7
samples/Sound.hx

@@ -21,23 +21,26 @@ class Sound extends SampleApp {
 	var music : hxd.snd.Channel;
 	var music : hxd.snd.Channel;
 	var musicPosition : h2d.Text;
 	var musicPosition : h2d.Text;
 	var beeper:Bool = true;
 	var beeper:Bool = true;
+	var pitchFilter : hxd.snd.effect.Pitch;
+	var pitchShift:Bool = false;
+	var pitchSlider : h2d.Slider;
 
 
 	override function init() {
 	override function init() {
 		super.init();
 		super.init();
 
 
 		var res = if( hxd.res.Sound.supportedFormat(Mp3) || hxd.res.Sound.supportedFormat(OggVorbis) ) hxd.Res.music_loop else null;
 		var res = if( hxd.res.Sound.supportedFormat(Mp3) || hxd.res.Sound.supportedFormat(OggVorbis) ) hxd.Res.music_loop else null;
-		var pitch = new hxd.snd.effect.Pitch();
+		pitchFilter = new hxd.snd.effect.Pitch();
 		var lowpass = new hxd.snd.effect.LowPass();
 		var lowpass = new hxd.snd.effect.LowPass();
+		var spatial = new hxd.snd.effect.Spatialization();
 		if( res != null ) {
 		if( res != null ) {
 			trace("Playing "+res);
 			trace("Playing "+res);
 			music = res.play(true);
 			music = res.play(true);
 			//music.queueSound(...);
 			//music.queueSound(...);
 			music.onEnd = function() trace("LOOP");
 			music.onEnd = function() trace("LOOP");
 			// Use effect processing on the channel
 			// Use effect processing on the channel
-			music.addEffect(pitch);
-			#if hlopenal
+			music.addEffect(pitchFilter);
 			music.addEffect(lowpass);
 			music.addEffect(lowpass);
-			#end
+			music.addEffect(spatial);
 		}
 		}
 
 
 		slider = new h2d.Slider(300, 10);
 		slider = new h2d.Slider(300, 10);
@@ -71,10 +74,18 @@ class Sound extends SampleApp {
 			tf.textAlign = Right;
 			tf.textAlign = Right;
 			f.addChild(slider);
 			f.addChild(slider);
 			f.addChild(musicPosition);
 			f.addChild(musicPosition);
-			addSlider("Pitch val", function() { return pitch.value; }, function(v) { pitch.value = v; }, 0, 2);
-			#if hlopenal
+			pitchSlider = addSlider("Pitch val", function() { return pitchFilter.value; }, function(v) { pitchFilter.value = v; }, 0, 2);
+			addCheck("Pitch shift", function() { return pitchShift; }, function (v) { pitchShift = v; });
 			addSlider("Lowpass gain", function() { return lowpass.gainHF; }, function(v) { lowpass.gainHF = v; }, 0, 1);
 			addSlider("Lowpass gain", function() { return lowpass.gainHF; }, function(v) { lowpass.gainHF = v; }, 0, 1);
-			#end
+			addText("Spatialization");
+			addSlider("X", function() { return spatial.position.x; }, function(v) { spatial.position.x = v; }, -10, 10);
+			addSlider("Y", function() { return spatial.position.y; }, function(v) { spatial.position.y = v; }, -10, 10);
+			addSlider("Z", function() { return spatial.position.z; }, function(v) { spatial.position.z = v; }, -10, 10);
+			addText("Spatialization Listener");
+			var listener = hxd.snd.Manager.get().listener;
+			addSlider("X", function() { return listener.position.x; }, function (v) { listener.position.x = v; }, -10, 10);
+			addSlider("Y", function() { return listener.position.y; }, function (v) { listener.position.y = v; }, -10, 10);
+			addSlider("Z", function() { return listener.position.z; }, function (v) { listener.position.z = v; }, -10, 10);
 		}
 		}
 	}
 	}
 
 
@@ -89,6 +100,12 @@ class Sound extends SampleApp {
 				engine.backgroundColor = 0;
 				engine.backgroundColor = 0;
 		}
 		}
 
 
+		if ( pitchShift ) {
+			pitchFilter.value = Math.max(Math.cos(hxd.Timer.lastTimeStamp / 4) + 1, 0.1);
+			pitchSlider.value = pitchFilter.value;
+			pitchSlider.onChange();
+		}
+
 		if( music != null ) {
 		if( music != null ) {
 			slider.value = music.position / music.duration;
 			slider.value = music.position / music.duration;
 			musicPosition.text = hxd.Math.fmt(music.position) + "/" + hxd.Math.fmt(music.duration);
 			musicPosition.text = hxd.Math.fmt(music.position) + "/" + hxd.Math.fmt(music.duration);