Pārlūkot izejas kodu

Rework JS NativeChannel (#437)

Pavel Alexandrov 7 gadi atpakaļ
vecāks
revīzija
3eff4819e9
2 mainītis faili ar 83 papildinājumiem un 29 dzēšanām
  1. 82 28
      hxd/snd/NativeChannel.hx
  2. 1 1
      hxd/snd/openal/Emulator.hx

+ 82 - 28
hxd/snd/NativeChannel.hx

@@ -94,7 +94,11 @@ class NativeChannel {
 			try {
 				ctx = new js.html.audio.AudioContext();
 			} catch( e : Dynamic ) try {
+				#if (haxe_ver >= 4)
+				ctx = js.Syntax.code('new window.webkitAudioContext()');
+				#else
 				ctx = untyped __js__('new window.webkitAudioContext()');
+				#end
 			} catch( e : Dynamic ) {
 				ctx = null;
 			}
@@ -105,11 +109,16 @@ class NativeChannel {
 		}
 		return ctx;
 	}
-	// Pool implemented to workaround memory leak in chromium browsers:
-	// https://bugs.chromium.org/p/chromium/issues/detail?id=379753
-	static var pool : Map<Int, Array<js.html.audio.ScriptProcessorNode>> = new Map();
+	// Avoid excessive buffer allocation when playing many sounds.
+	// bufferSamples is constant and never change at runtime, so it's safe to use general pool.
+	static var pool : Array<js.html.audio.AudioBuffer> = new Array();
+	static var bufferPool : Array<haxe.io.Float32Array> = new Array();
 	
-	var sproc : js.html.audio.ScriptProcessorNode;
+	var front : js.html.audio.AudioBuffer;
+	var back : js.html.audio.AudioBuffer;
+	var current : js.html.audio.AudioBufferSourceNode;
+	var queued : js.html.audio.AudioBufferSourceNode;
+	var time : Float; // Mandatory for proper buffer sync, otherwise produces gaps in playback due to innacurate timings.
 	var tmpBuffer : haxe.io.Float32Array;
 	#elseif lime_openal
 	var channel : ALChannel;
@@ -125,15 +134,32 @@ class NativeChannel {
 		#elseif js
 		var ctx = getContext();
 		if( ctx == null ) return;
-		var sprocPool = pool.get(bufferSamples);
-		if ( sprocPool != null && sprocPool.length > 0 ) {
-			sproc = sprocPool.pop();
-		} else {
-			sproc = ctx.createScriptProcessor(bufferSamples, 2, 2);
-		}
-		tmpBuffer = new haxe.io.Float32Array(bufferSamples * 2);
-		sproc.connect(ctx.destination);
-		sproc.onaudioprocess = onJsSample;
+		
+		if ( pool.length > 0 ) front = pool.pop();
+		else front = ctx.createBuffer(2, bufferSamples, ctx.sampleRate);
+		if ( pool.length > 0 ) back = pool.pop();
+		else back = ctx.createBuffer(2, bufferSamples, ctx.sampleRate);
+		
+		if ( bufferPool.length > 0 ) tmpBuffer = bufferPool.pop();
+		else tmpBuffer = new haxe.io.Float32Array(bufferSamples * 2);
+		
+		fill(front);
+		fill(back);
+		
+		current = ctx.createBufferSource();
+		current.buffer = front;
+		current.addEventListener("ended", swap);
+		current.connect(ctx.destination);
+		queued = ctx.createBufferSource();
+		queued.buffer = back;
+		queued.addEventListener("ended", swap);
+		queued.connect(ctx.destination);
+		
+		var currTime : Float = ctx.currentTime;
+		current.start(currTime);
+		time = currTime + front.duration;
+		queued.start(time);
+		
 		#elseif lime_openal
 		channel = new ALChannel(bufferSamples, this);
 		#end
@@ -174,18 +200,39 @@ class NativeChannel {
 		js.Browser.document.body.removeEventListener("touchend",stopInput);
 		if( ctx != null ) ctx.resume();
 	}
-
-	function onJsSample( event : js.html.audio.AudioProcessingEvent ) {
+	
+	function swap( event : js.html.Event ) {
+		var tmp = front;
+		front = back;
+		back = tmp;
+		fill(tmp);
+		
+		current.removeEventListener("ended", swap);
+		// current.disconnect(); // Should not be required as it's a one-shot object by design.
+		current = queued;
+		var ctx = getContext();
+		queued = ctx.createBufferSource();
+		queued.buffer = tmp;
+		queued.addEventListener("ended", swap);
+		queued.connect(ctx.destination);
+		
+		time += front.duration;
+		queued.start(time);
+	}
+	
+	inline function fill( buffer : js.html.audio.AudioBuffer ) {
 		onSample(tmpBuffer);
 		// split the channels and copy to output
 		var r = 0;
-		var left = event.outputBuffer.getChannelData(0);
-		var right = event.outputBuffer.getChannelData(1);
-		for( i in 0...bufferSamples ) {
+		var left = buffer.getChannelData(0);
+		var right = buffer.getChannelData(1);
+		for ( i in 0...bufferSamples )
+		{
 			left[i] = tmpBuffer[r++];
 			right[i] = tmpBuffer[r++];
 		}
 	}
+	
 	#end
 
 	function onSample( out : haxe.io.Float32Array ) {
@@ -198,16 +245,23 @@ class NativeChannel {
 			channel = null;
 		}
 		#elseif js
-		if( sproc != null ) {
-			var sprocPool = pool.get(bufferSamples);
-			if (sprocPool == null) {
-				sprocPool = [sproc];
-				pool.set(bufferSamples, sprocPool);
-			} else {
-				sprocPool.push(sproc);
-			}
-			sproc.disconnect();
-			sproc = null;
+		if ( front != null ) {
+			current.disconnect();
+			current.removeEventListener("ended", swap);
+			current = null;
+			
+			queued.removeEventListener("ended", swap);
+			queued.disconnect();
+			queued.stop();
+			queued = null;
+			
+			pool.push(front);
+			front = null;
+			pool.push(back);
+			back = null;
+			
+			bufferPool.push(tmpBuffer);
+			tmpBuffer = null;
 		}
 		#elseif lime_openal
 		if( channel != null ) {

+ 1 - 1
hxd/snd/openal/Emulator.hx

@@ -78,7 +78,7 @@ class Source {
 	// This seems related to some lag in NativeChannel creation and data delivery
 	static inline var STOP_DELAY = #if js 200 #else 0 #end;
 
-	public static var CHANNEL_BUFSIZE = 4096; /* 100 ms latency @44.1Khz */
+	public static var CHANNEL_BUFSIZE = #if js 8192 #else 4096 #end; /* 100 ms latency @44.1Khz */
 
 	static var ID = 0;
 	static var all = new Map<Int,Source>();