Преглед на файлове

new sound API : works in JS/CPP/Flash, mp3 support still missing in CPP, JS requires WebAudioAPI

Nicolas Cannasse преди 10 години
родител
ревизия
0409ff23b2
променени са 11 файла, в които са добавени 202 реда и са изтрити 806 реда
  1. 3 245
      hxd/res/Sound.hx
  2. 4 2
      hxd/snd/Channel.hx
  3. 14 0
      hxd/snd/Data.hx
  4. 81 0
      hxd/snd/Mp3Data.hx
  5. 0 364
      hxd/snd/MusicWorker.hx
  6. 11 2
      hxd/snd/NativeChannel.hx
  7. 0 78
      hxd/snd/SoundChannel.hx
  8. 0 73
      hxd/snd/SoundData.hx
  9. 2 2
      hxd/snd/WavData.hx
  10. 83 37
      hxd/snd/Worker.hx
  11. 4 3
      samples/sound/Sound.hx

+ 3 - 245
hxd/res/Sound.hx

@@ -11,8 +11,9 @@ class Sound extends Resource {
 		var bytes = entry.getBytes();
 		var bytes = entry.getBytes();
 		switch( bytes.get(0) ) {
 		switch( bytes.get(0) ) {
 		case 'R'.code: // RIFF (wav)
 		case 'R'.code: // RIFF (wav)
-			var s = new format.wav.Reader(new haxe.io.BytesInput(bytes)).read();
-			data = new hxd.snd.WavData(s);
+			data = new hxd.snd.WavData(bytes);
+		case 255, 'I'.code: // MP3 (or ID3)
+			data = new hxd.snd.Mp3Data(bytes);
 		default:
 		default:
 		}
 		}
 		if( data == null )
 		if( data == null )
@@ -24,8 +25,6 @@ class Sound extends Resource {
 		data = null;
 		data = null;
 	}
 	}
 
 
-#if newSoundAPI
-
 	public function play( loop = false ) {
 	public function play( loop = false ) {
 		if( defaultWorker == null ) {
 		if( defaultWorker == null ) {
 			// don't use a native worker since we haven't setup it in our main()
 			// don't use a native worker since we haven't setup it in our main()
@@ -48,245 +47,4 @@ class Sound extends Resource {
 		return defaultWorker.start();
 		return defaultWorker.start();
 	}
 	}
 
 
-#else
-
-	public static function startWorker() {
-		return false;
-	}
-
-	public static var BUFFER_SIZE = 4096;
-
-	public static dynamic function getGlobalVolume( s : Sound ) {
-		return 1.0;
-	}
-
-	public var volume(default, set) = 1.0;
-	public var loop : Bool;
-
-	#if flash
-	var snd : flash.media.Sound;
-	var channel : flash.media.SoundChannel;
-
-	var mp3Data : flash.media.Sound;
-	var wavHeader : format.wav.Data.WAVEHeader;
-	var playingBytes : haxe.io.Bytes;
-	var bytesPosition : Int;
-	var mp3SampleCount : Int;
-	var playDelay : Float = 0;
-	#end
-
-	public function getPosition() : Float {
-		#if flash
-		return channel.position;
-		#else
-		return 0.;
-		#end
-	}
-
-	public function play() {
-		playAt(0);
-	}
-
-	public function isPlaying() {
-		#if flash
-		return channel != null || playDelay > haxe.Timer.stamp();
-		#else
-		return false;
-		#end
-	}
-
-	#if flash
-	function onWavSample( e : flash.events.SampleDataEvent ) {
-		var input = new haxe.io.BytesInput(playingBytes,bytesPosition);
-		var out = e.data;
-		var size = BUFFER_SIZE;
-		out.position = 0;
-		do {
-			var write = size - Std.int(out.position/8);
-			try {
-				switch( [wavHeader.channels, wavHeader.bitsPerSample] ) {
-				case [2, 16]:
-					for( i in 0...write ) {
-						out.writeFloat(input.readInt16() / 32767);
-						out.writeFloat(input.readInt16() / 32767);
-					}
-				case [1, 16]:
-					for( i in 0...write ) {
-						var f = input.readInt16() / 32767;
-						out.writeFloat(f);
-						out.writeFloat(f);
-					}
-				case [1, 8]:
-					for( i in 0...write ) {
-						var f = input.readByte() / 255;
-						out.writeFloat(f);
-						out.writeFloat(f);
-					}
-				case [2, 8]:
-					for( i in 0...write ) {
-						out.writeFloat(input.readByte() / 255);
-						out.writeFloat(input.readByte() / 255);
-					}
-				default:
-				}
-				break;
-			} catch( e : haxe.io.Eof ) {
-				if( loop )
-					input.position = 0;
-				else if( channel != null && out.position == 0 ) {
-					haxe.Timer.delay(channel.stop, 150); // wait until the buffer is played
-					channel = null;
-				} else if( out.position > 0 ) {
-					playDelay = haxe.Timer.stamp() + (out.position / 8) / 44000;
-				}
-			}
-		} while( Std.int(out.position) < size * 8 && loop );
-		// pad with zeroes
-		while( Std.int(out.position) < size * 8 ) {
-			out.writeFloat(0);
-			out.writeFloat(0);
-		}
-		bytesPosition = input.position;
-	}
-
-	function onMp3Sample(e:flash.events.SampleDataEvent) {
-		var out = e.data;
-		out.position = 0;
-
-		var MAGIC_DELAY = 2257;
-		var position = bytesPosition;
-		while( true ) {
-			var size = BUFFER_SIZE - ((out.position >> 3) : Int);
-			if( size == 0 ) break;
-			if( position + size >= mp3SampleCount ) {
-				var read = mp3SampleCount - position;
-				mp3Data.extract(out, read, position + MAGIC_DELAY);
-				position = 0;
-			} else {
-				mp3Data.extract(out, size, position + MAGIC_DELAY);
-				position += size;
-			}
-		}
-		bytesPosition = position;
-	}
-
-	function initSound() {
-		if( snd == null )
-			watch(onReload);
-		snd = new flash.media.Sound();
-		var bytes = entry.getBytes();
-		switch( bytes.get(0) ) {
-		case 'R'.code: // RIFF (wav)
-			var s = new format.wav.Reader(new haxe.io.BytesInput(bytes)).read();
-
-			if( s.header.channels > 2 || (s.header.bitsPerSample != 8 && s.header.bitsPerSample != 16) )
-				throw "Unsupported " + s.header.bitsPerSample + "x" + s.header.channels;
-
-			if( s.header.samplingRate != 44100 ) {
-				// resample
-				var out = new haxe.io.BytesOutput();
-				var rpos = 0.;
-				var data = s.data;
-				var max = data.length >> (s.header.bitsPerSample >> 4);
-				var delta = s.header.samplingRate / 44100;
-				var scale = (1 << s.header.bitsPerSample) - 1;
-				while( rpos < max ) {
-					var ipos = Std.int(rpos);
-					var npos = ipos + 1;
-					if( npos >= max ) npos = max - 1;
-					var v1, v2;
-					if( s.header.bitsPerSample == 8 ) {
-						v1 = data.get(ipos) / 255;
-						v2 = data.get(npos) / 255;
-					} else {
-						v1 = (data.get(ipos<<1) | (data.get((ipos<<1) + 1)<<8)) / 65535;
-						v2 = (data.get(npos<<1) | (data.get((npos<<1) + 1)<<8)) / 65535;
-					}
-					var v = Std.int(hxd.Math.lerp(v1, v2, rpos - ipos) * scale);
-					if( s.header.bitsPerSample == 8 )
-						out.writeByte(v);
-					else
-						out.writeUInt16(v);
-					rpos += delta;
-				}
-				s.data = out.getBytes();
-			}
-
-			wavHeader = s.header;
-			playingBytes = s.data;
-			snd.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, onWavSample);
-
-		case 255, 'I'.code: // MP3 (or ID3)
-
-			snd.loadCompressedDataFromByteArray(bytes.getData(), bytes.length);
-			if( loop ) {
-
-				var mp = new format.mp3.Reader(new haxe.io.BytesInput(bytes)).read();
-				var samples = mp.sampleCount;
-				var frame = mp.frames[0].data.toString();
-				// http://gabriel.mp3-tech.org/mp3infotag.html
-				var lame = frame.indexOf("LAME", 32 + 120);
-				if( lame >= 0 ) {
-					var startEnd = (frame.charCodeAt(lame + 21) << 16) | (frame.charCodeAt(lame + 22) << 8) | frame.charCodeAt(lame + 23);
-					var start = startEnd >> 12;
-					var end = startEnd & ((1 << 12) - 1);
-					samples -= start + end + 1152; // first frame is empty
-				}
-
-
-				mp3Data = snd;
-				mp3SampleCount = samples;
-				snd = new flash.media.Sound();
-				snd.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, onMp3Sample);
-			}
-
-		default:
-			throw "Unsupported sound format " + entry.path;
-		}
-		hxd.impl.Tmp.saveBytes(bytes);
-	}
-
-	function onReload() {
-		stop();
-		initSound();
-	}
-
-	#end
-
-	public function playAt( startPosition : Float ) {
-		#if flash
-		if( snd == null ) initSound();
-
-		// can't mix two wavs
-		if( wavHeader != null && channel != null )
-			return;
-		bytesPosition = 0;
-		channel = snd.play(startPosition, loop?0x7FFFFFFF:0);
-		if( !loop ) {
-			var chan = channel;
-			if( chan != null ) channel.addEventListener(flash.events.Event.SOUND_COMPLETE, function(e) { chan.stop(); if( chan == channel ) channel = null; } );
-		}
-		volume = volume;
-		#end
-	}
-
-	public function stop() {
-		#if flash
-		if( channel != null ) {
-			channel.stop();
-			channel = null;
-		}
-		#end
-	}
-
-	function set_volume( v : Float ) {
-		volume = v;
-		#if flash
-		if( channel != null )
-			channel.soundTransform = new flash.media.SoundTransform(v * getGlobalVolume(this));
-		#end
-		return v;
-	}
-#end
-
 }
 }

+ 4 - 2
hxd/snd/Channel.hx

@@ -2,6 +2,7 @@ package hxd.snd;
 
 
 class Channel {
 class Channel {
 	var w : Worker;
 	var w : Worker;
+	var channel : Worker.NativeChannelData;
 	var id : Int;
 	var id : Int;
 	var snd : Data;
 	var snd : Data;
 	var position : Int;
 	var position : Int;
@@ -18,8 +19,9 @@ class Channel {
 	public var volume(default, set) : Float;
 	public var volume(default, set) : Float;
 	public var currentTime(get, set) : Float;
 	public var currentTime(get, set) : Float;
 
 
-	function new(w, res, id, loop, v, t:Float) {
+	function new(w, chan, res, id, loop, v, t:Float) {
 		this.res = res;
 		this.res = res;
+		this.channel = chan;
 		this.vol = volume = v;
 		this.vol = volume = v;
 		this.id = id;
 		this.id = id;
 		this.loop = loop;
 		this.loop = loop;
@@ -68,7 +70,7 @@ class Channel {
 	public function stop() {
 	public function stop() {
 		if( w != null ) {
 		if( w != null ) {
 			w.send(Stop(id));
 			w.send(Stop(id));
-			w.channels.remove(this);
+			if( channel != null ) channel.channels.remove(this);
 			w.cmap.remove(id);
 			w.cmap.remove(id);
 			w = null;
 			w = null;
 		}
 		}

+ 14 - 0
hxd/snd/Data.hx

@@ -3,8 +3,22 @@ package hxd.snd;
 class Data {
 class Data {
 
 
 	public var samples(default, null) : Int;
 	public var samples(default, null) : Int;
+
+	/**
+		Decode sound samples as stereo 32 bit floats 44.1Khz
+		If you decode while no data is available, blank samples will be written.
+	**/
 	public function decode( out : haxe.io.Bytes, outPos : Int, sampleStart : Int, sampleCount : Int ) : Void {
 	public function decode( out : haxe.io.Bytes, outPos : Int, sampleStart : Int, sampleCount : Int ) : Void {
 		throw "not implemented";
 		throw "not implemented";
 	}
 	}
 
 
+	/**
+		Some platforms might require some data to be loaded before we can start decoding.
+		Use load() and wait for onEnd to make sure that the sound data and the correct number of samples is available.
+		onEnd() might be called back immediately if the data is already available.
+	**/
+	public function load( onEnd : Void -> Void ) {
+		onEnd();
+	}
+
 }
 }

+ 81 - 0
hxd/snd/Mp3Data.hx

@@ -0,0 +1,81 @@
+package hxd.snd;
+
+class Mp3Data extends Data {
+
+	#if flash
+	var snd : flash.media.Sound;
+	#elseif js
+	var buffer : haxe.io.Bytes;
+	var onEnd : Void -> Void;
+	#end
+
+	public function new( bytes : haxe.io.Bytes ) {
+		var mp = new format.mp3.Reader(new haxe.io.BytesInput(bytes)).read();
+		samples = mp.sampleCount;
+		// Sadly mp3 is not meant to perfectly loop, let's try to extract the real number of samples that were actually encoded with LAME
+		// http://gabriel.mp3-tech.org/mp3infotag.html
+		var frame = mp.frames[0].data;
+		var lame = -1;
+		for( i in 32 + 120...frame.length - 24 )
+			if( frame.get(i) == "L".code && frame.get(i + 1) == "A".code && frame.get(i + 2) == "M".code && frame.get(i + 3) == "E".code ) {
+				lame = i;
+				break;
+			}
+		if( lame >= 0 ) {
+			var startEnd = (frame.get(lame + 21) << 16) | (frame.get(lame + 22) << 8) | frame.get(lame + 23);
+			var start = startEnd >> 12;
+			var end = startEnd & ((1 << 12) - 1);
+			samples -= start + end + 1152; // first frame is empty
+		}
+		#if flash
+		snd = new flash.media.Sound();
+		bytes.getData().position = 0;
+		snd.loadCompressedDataFromByteArray(bytes.getData(), bytes.length);
+		#elseif js
+		var ctx = @:privateAccess NativeChannel.getContext();
+		ctx.decodeAudioData(bytes.getData(), processBuffer);
+		#end
+	}
+
+
+	#if js
+	override public function load(onEnd:Void->Void) {
+		if( buffer != null ) onEnd() else this.onEnd = onEnd;
+	}
+
+	function processBuffer( buf : js.html.audio.AudioBuffer ) {
+		var left = buf.getChannelData(0);
+		var right = buf.numberOfChannels < 2 ? left : buf.getChannelData(1);
+		var join = new js.html.Float32Array(left.length * 2);
+		var w = 0;
+		for( i in 0...buf.length ) {
+			join[w++] = left[i];
+			join[w++] = right[i];
+		}
+		samples = buf.length; // actual decoded samples
+ 		buffer = haxe.io.Bytes.ofData(join.buffer);
+		if( onEnd != null ) {
+			onEnd();
+			onEnd = null;
+		}
+	}
+	#end
+
+	override public function decode(out:haxe.io.Bytes, outPos:Int, sampleStart:Int, sampleCount:Int) {
+		#if flash
+		var b = out.getData();
+		b.position = outPos;
+		snd.extract(b, sampleCount, sampleStart + 2257 /* MAGIC_DELAY, silence added at mp3 start */ );
+		#elseif js
+		if( buffer == null ) {
+			// not yet available : fill with blanks
+			out.fill(outPos, sampleCount * 8, 0);
+		} else {
+			out.blit(outPos, buffer, sampleStart * 8, sampleCount * 8);
+		}
+		#else
+		throw "MP3 decoding is not available for this platform";
+		#end
+	}
+
+}

+ 0 - 364
hxd/snd/MusicWorker.hx

@@ -1,364 +0,0 @@
-package hxd.snd;
-
-enum MusicMessage {
-	Play( path : String, volume : Float, time : Float );
-	SetGlobalVolume( volume : Float );
-	SetVolume( id : Int, volume : Float );
-	Fade( id : Int, uid : Int, volume : Float, time : Float );
-	Stop( id : Int );
-	Queue( id : Int, path : Null<String> );
-	Loop( id : Int, b : Bool );
-	EndLoop( id : Int );
-	SetTime( id : Int, t : Float );
-	FadeEnd( id : Int, uid : Int );
-	Active( b : Bool );
-}
-
-class Channel {
-	var w : MusicWorker;
-	var id : Int;
-	var snd : SoundData;
-	var samples : Int;
-	var position : Int;
-	var vol : Float;
-	var volumeTarget : Float;
-	var volumeSpeed : Float;
-	var playTime : Float;
-	var onFadeEnd : Void -> Void;
-	var fadeUID : Int = 0;
-	var next : Channel;
-
-	public var res(default, null) : hxd.res.Sound;
-	public var loop(default, set) : Bool;
-	public var volume(default, set) : Float;
-	public var currentTime(get, set) : Float;
-
-	function new(w, res, id, v, t:Float) {
-		this.res = res;
-		this.vol = volume = v;
-		this.id = id;
-		loop = true;
-		volumeSpeed = 0;
-		playTime = haxe.Timer.stamp() - t;
-		this.w = w;
-	}
-
-	public function queueNext( r : hxd.res.Sound ) {
-		if( w != null ) {
-			if( r != null ) w.channelID++;
-			w.send(Queue(id, r == null ? null : r.entry.path));
-		}
-	}
-
-	function get_currentTime() {
-		return haxe.Timer.stamp() - playTime;
-	}
-
-	function set_currentTime(v:Float) {
-		playTime = haxe.Timer.stamp() - v;
-		if( w != null ) {
-			throw "assert";
-			w.send(SetTime(id, v));
-			return v;
-		}
-		if( samples > 0 ) {
-			position = Std.int(v * 44100) % samples;
-			if( position < 0 ) position += samples;
-		}
-		return v;
-	}
-
-	public function fadeTo( volume : Float, time : Float = 1., ?onEnd : Void -> Void ) {
-		if( this.volume == volume ) {
-			if( onEnd != null ) haxe.Timer.delay(onEnd, 1+Math.ceil(time * 1000));
-			return;
-		}
-		var old = w;
-		w = null;
-		this.volume = volume;
-		w = old;
-		onFadeEnd = onEnd;
-		if( w != null ) w.send(Fade(id, onEnd == null ? 0 : ++fadeUID, volume, time));
-	}
-
-	public function stop() {
-		if( w != null ) {
-			w.send(Stop(id));
-			w.channels.remove(this);
-			w.cmap.remove(id);
-			w = null;
-		}
-	}
-
-	function set_loop(b) {
-		if( loop == b )
-			return b;
-		if( w != null ) w.send(Loop(id, b));
-		return loop = b;
-	}
-
-	function set_volume(v) {
-		if( volume == v )
-			return v;
-		volume = v;
-		if( w != null ) w.send(SetVolume(id, v));
-		return v;
-	}
-
-	public dynamic function onEnd() {
-	}
-
-}
-
-@:access(hxd.snd.Channel)
-@:allow(hxd.snd.Channel)
-class MusicWorker extends hxd.Worker<MusicMessage> {
-
-	public static var NATIVE_MUSIC = false;
-	public static var volume(default, set) = 1.0;
-
-	var channelID = 1;
-	var channels : Array<Channel> = [];
-	var cmap : Map<Int,Channel> = new Map();
-	var snd : SoundData;
-	var channel : SoundChannel;
-	var tmpBuf : haxe.io.Bytes;
-	var out : haxe.ds.Vector<Float>;
-	var current : Channel;
-	var prevPos : Float;
-	var globalVolume : Float = 1.;
-	var waitTimer : haxe.Timer;
-	static inline var BUFFER_SIZE = 4096;
-
-	public function new() {
-		super(MusicMessage);
-	}
-
-	override function clone() {
-		return new MusicWorker();
-	}
-
-	function makeChannel( res, volume : Float, time : Float ) {
-		var c = new Channel(isWorker ? null : this, res, channelID++, volume, time);
-		channels.push(c);
-		cmap.set(c.id, c);
-		return c;
-	}
-
-	override function handleMessage( msg : MusicMessage ) {
-
-		switch( msg ) {
-		case Play(path, volume, time):
-			var c = makeChannel(null, volume, time);
-			var bytes = hxd.Res.loader.load(path).entry.getBytes();
-			c.snd = new SoundData();
-			c.snd.loadMP3(bytes);
-
-			if( NATIVE_MUSIC ) {
-				if( channel != null ) channel.stop();
-				channel = c.snd.playNative(time, true);
-				channel.volume = globalVolume;
-				channel.onEnd = function() send(EndLoop(c.id));
-				current = c;
-				return;
-			}
-
-			var mp = new format.mp3.Reader(new haxe.io.BytesInput(bytes)).read();
-			c.samples = mp.sampleCount;
-			var frame = mp.frames[0].data;
-			// http://gabriel.mp3-tech.org/mp3infotag.html
-			var pos = -1;
-			for( i in 32 + 120...frame.length - 24 )
-				if( frame.get(i) == "L".code && frame.get(i + 1) == "A".code && frame.get(i + 2) == "M".code && frame.get(i + 3) == "E".code ) {
-					pos = i;
-					break;
-				}
-			if( pos >= 0 ) {
-				var startEnd = (frame.get(pos + 21) << 16) | (frame.get(pos + 22) << 8) | frame.get(pos + 23);
-				var start = startEnd >> 12;
-				var end = startEnd & ((1 << 12) - 1);
-				c.samples -= start + end + 1152; // first frame is empty
-			}
-			c.currentTime = time; // update position
-
-		case SetVolume(id, volume):
-			var c = cmap.get(id);
-			if( c == null ) return;
-			c.vol = volume;
-			c.volumeSpeed = 0;
-		case Stop(id):
-			var c = cmap.get(id);
-			if( c == null ) return;
-			channels.remove(c);
-			cmap.remove(c.id);
-			if( NATIVE_MUSIC && c == current ) {
-				channel.stop();
-				channel = null;
-				current = null;
-			}
-		case Fade(id, uid, vol, time):
-			var c = cmap.get(id);
-			if( c == null ) return;
-			c.volumeTarget = vol;
-			c.fadeUID = uid;
-			c.volumeSpeed = (vol - c.vol) / (time * 88200);
-		case Queue(id, path):
-			var c = cmap.get(id);
-			if( c == null ) return;
-
-			if( NATIVE_MUSIC ) {
-				if( current != c || channel == null ) return;
-				var alloc = channelID++;
-				channel.onEnd = function() {
-					send(EndLoop(c.id));
-					if( path != null ) {
-						var old = channelID;
-						channelID = alloc;
-						handleMessage(Play(path, c.volume, 0));
-						channelID = old;
-						var c2 = channels[channels.length - 1];
-						send(Stop(c.id));
-						// rebind c2 as c
-						cmap.remove(c2.id);
-						c2.id = c.id;
-						cmap.set(c2.id, c2);
-					}
-				};
-				return;
-			}
-
-			handleMessage(Play(path, 0, 0));
-			var c2 = channels[channels.length - 1];
-			c.next = c2;
-
-		case Loop(id, b):
-			var c = cmap.get(id);
-			if( c == null ) return;
-			c.loop = b;
-		case EndLoop(id):
-			var c = cmap.get(id);
-			if( c != null ) c.onEnd();
-		case FadeEnd(id,uid):
-			var c = cmap.get(id);
-			if( c != null && c.fadeUID == uid ) c.onFadeEnd();
-		case SetTime(id, v):
-			var c = cmap.get(id);
-			if( c != null ) c.currentTime = v;
-		case Active(b):
-			if( b ) {
-				if( channel != null ) return;
-				channel = NATIVE_MUSIC ? (current == null ? null : current.snd.playNative(prevPos,true)) : snd.playStream(sampleData);
-				handleMessage(SetGlobalVolume(globalVolume));
-			} else {
-				if( channel == null ) return;
-				prevPos = NATIVE_MUSIC ? channel.position : 0;
-				channel.stop();
-				channel = null;
-			}
-		case SetGlobalVolume(v):
-			globalVolume = v;
-			if( channel != null ) channel.volume = v;
-		}
-	}
-
-	override function setupMain() {
-		// make sure that the sounds system is initialized
-		// https://bugbase.adobe.com/index.cfm?event=bug&id=3842828
-		var s = new SoundData();
-		s.playStream(function() return new haxe.ds.Vector<Float>(BUFFER_SIZE * 2)).stop();
-
-		if( hxd.System.isAndroid )
-			initActivate();
-		if( volume != 1 )
-			send(SetGlobalVolume(volume));
-	}
-
-	function initActivate() {
-		#if air3
-		// note : on some devices (Wiko) theses events are not fired inside workers, so catch them only in main thread
-		flash.desktop.NativeApplication.nativeApplication.addEventListener(flash.events.Event.ACTIVATE, function(_:Dynamic) send(Active(true)));
-		flash.desktop.NativeApplication.nativeApplication.addEventListener(flash.events.Event.DEACTIVATE, function(_:Dynamic) send(Active(false)));
-		#end
-	}
-
-	override function setupWorker() {
-		tmpBuf = haxe.io.Bytes.alloc(BUFFER_SIZE * 4 * 2);
-		out = new haxe.ds.Vector(BUFFER_SIZE * 2);
-		if( !NATIVE_MUSIC ) {
-			snd = new SoundData();
-			channel = snd.playStream(sampleData);
-		}
-	}
-
-	function sampleData() {
-		for( i in 0...BUFFER_SIZE*2 )
-			out[i] = 0;
-		for( c in channels ) {
-
-			if( c.vol <= 0 && c.volumeSpeed <= 0 ) continue;
-			if( !c.loop && c.position == c.samples ) continue;
-
-			var w = 0;
-			while( true ) {
-				var size = BUFFER_SIZE - (w >> 1);
-				if( size == 0 ) break;
-				if( c.position + size >= c.samples ) {
-					size = c.samples - c.position;
-					c.snd.extract(tmpBuf, 0, c.position, size);
-					c.position = 0;
-				} else {
-					c.snd.extract(tmpBuf, 0, c.position, size);
-					c.position += size;
-				}
-				#if flash
-				flash.Memory.select(tmpBuf.getData());
-				#end
-				for( i in 0...size * 2 ) {
-					out[w++] += #if flash flash.Memory.getFloat #else tmpBuf.getFloat #end(i << 2) * c.vol;
-					if( c.volumeSpeed != 0 ) {
-						c.vol += c.volumeSpeed;
-						if( (c.volumeSpeed > 0) == (c.vol > c.volumeTarget) ) {
-							c.vol = c.volumeTarget;
-							c.volumeSpeed = 0;
-							if( c.fadeUID > 0 ) send(FadeEnd(c.id, c.fadeUID));
-						}
-					}
-				}
-				if( c.position == 0 ) {
-					send(EndLoop(c.id));
-					if( c.next != null ) {
-						c.snd = c.next.snd;
-						c.samples = c.next.samples;
-						handleMessage(Stop(c.next.id));
-						c.next = null;
-					} else if( !c.loop ) {
-						c.position = c.samples;
-						break;
-					}
-				}
-			}
-		}
-		return out;
-	}
-
-	static function set_volume( v : Float ) {
-		if( volume == v )
-			return v;
-		if( inst != null )
-			inst.send(SetGlobalVolume(v));
-		return volume = v;
-	}
-
-	public static function play( music : hxd.res.Sound, volume = 1., time = 0. ) {
-		inst.send(Play(music.entry.path, volume, time));
-		return inst.makeChannel(music, volume, time);
-	}
-
-	static var inst : MusicWorker;
-
-	public static function init() {
-		inst = new MusicWorker();
-		return inst.start();
-	}
-
-}

+ 11 - 2
hxd/snd/NativeChannel.hx

@@ -22,6 +22,16 @@ class NativeChannel {
 	var channel : flash.media.SoundChannel;
 	var channel : flash.media.SoundChannel;
 	#elseif js
 	#elseif js
 	static var ctx : js.html.audio.AudioContext;
 	static var ctx : js.html.audio.AudioContext;
+	static function getContext() {
+		if( ctx == null ) {
+			try {
+				ctx = new js.html.audio.AudioContext();
+			} catch( e : Dynamic ) {
+				throw "Web Audio API not available for this browser";
+			}
+		}
+		return ctx;
+	}
 	var sproc : js.html.audio.ScriptProcessorNode;
 	var sproc : js.html.audio.ScriptProcessorNode;
 	var tmpBuffer : haxe.io.Float32Array;
 	var tmpBuffer : haxe.io.Float32Array;
 	#elseif hxsdl
 	#elseif hxsdl
@@ -36,8 +46,7 @@ class NativeChannel {
 		snd.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, onFlashSample);
 		snd.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, onFlashSample);
 		channel = snd.play(0, 0x7FFFFFFF);
 		channel = snd.play(0, 0x7FFFFFFF);
 		#elseif js
 		#elseif js
-		if( ctx == null )
-			ctx = new js.html.audio.AudioContext();
+		var ctx = getContext();
 		sproc = ctx.createScriptProcessor(bufferSamples, 2, 2);
 		sproc = ctx.createScriptProcessor(bufferSamples, 2, 2);
 		tmpBuffer = new haxe.io.Float32Array(bufferSamples * 2);
 		tmpBuffer = new haxe.io.Float32Array(bufferSamples * 2);
 		sproc.connect(ctx.destination);
 		sproc.connect(ctx.destination);

+ 0 - 78
hxd/snd/SoundChannel.hx

@@ -1,78 +0,0 @@
-package hxd.snd;
-
-class SoundChannel {
-
-	var snd : SoundData;
-	#if flash
-	var channel : flash.media.SoundChannel;
-	var endTimer : haxe.Timer;
-	#end
-
-	public var loop(default, null) : Bool;
-	public var volume(default, set) : Float;
-	public var position(get, never) : Float;
-	public var playing(default, null) : Bool;
-
-	function new(snd, loop) {
-		this.snd = snd;
-		this.loop = loop;
-		volume = 1;
-		playing = true;
-	}
-
-	function init( startTime : Float ) {
-		#if flash
-		startTime = (startTime * 1000) % @:privateAccess snd.snd.length;
-		channel = @:privateAccess snd.snd.play(startTime, startTime == 0 && loop ? 0x7FFFFFFF : 1);
-		if( !loop && channel != null ) {
-			channel.addEventListener(flash.events.Event.SOUND_COMPLETE, function(_) { playing = false; onEnd(); });
-		} else if( loop && startTime != 0 && channel != null ) {
-			channel.addEventListener(flash.events.Event.SOUND_COMPLETE, function(_) { stop(); init(0); playing = true; onEnd(); } );
-		} else {
-			var t = @:privateAccess (snd.snd.length - (channel == null ? 0 : channel.position));
-			endTimer = new haxe.Timer(Std.int(t));
-			endTimer.run = function() {
-				if( channel == null ) stop();
-				onEnd();
-			};
-		}
-		#end
-	}
-
-	function get_position() {
-		#if flash
-		if( channel != null )
-			return (channel.position / 1000) % @:privateAccess snd.snd.length;
-		#end
-		return 0.;
-	}
-
-	function set_volume(v) {
-		#if flash
-		if( channel != null ) {
-			var st = channel.soundTransform;
-			st.volume = v;
-			channel.soundTransform = st;
-		}
-		#end
-		return volume = v;
-	}
-
-	public function stop() {
-		playing = false;
-		#if flash
-		if( channel != null ) {
-			channel.stop();
-			channel = null;
-		}
-		if( endTimer != null ) {
-			endTimer.stop();
-			endTimer = null;
-		}
-		#end
-	}
-
-	public dynamic function onEnd() {
-	}
-
-}

+ 0 - 73
hxd/snd/SoundData.hx

@@ -1,73 +0,0 @@
-package hxd.snd;
-
-@:access(hxd.snd.SoundChannel)
-class SoundData {
-
-	#if flash
-	var snd : flash.media.Sound;
-	var onStreamData : Void -> haxe.ds.Vector<Float>;
-	#end
-
-	public function new() {
-		#if flash
-		snd = new flash.media.Sound();
-		#end
-	}
-
-	public function loadMP3( data : haxe.io.Bytes ) {
-		#if flash
-		snd.loadCompressedDataFromByteArray(data.getData(), data.length);
-		#else
-		throw "Not implemented";
-		#end
-	}
-
-	public function extract( bytesOutput : haxe.io.Bytes, position : Int, startSample : Int, sampleCount : Int ) {
-		#if flash
-		var b = bytesOutput.getData();
-		b.position = position;
-		snd.extract(b, sampleCount, startSample + 2257);
-		#else
-		throw "Not implemented";
-		#end
-	}
-
-	public function playNative( startTime : Float = 0., loop = false ) : SoundChannel {
-		var c = new SoundChannel(this, loop);
-		c.init(startTime);
-		return c;
-	}
-
-	public function loadURL( url : String ) {
-		#if flash
-		snd.load(new flash.net.URLRequest(url));
-		#else
-		throw "Not implemented";
-		#end
-	}
-
-	#if flash
-	function onFlashSample( e : flash.events.SampleDataEvent ) {
-		var buf = onStreamData();
-		var bytes = e.data;
-		bytes.position = 0;
-		for( i in 0...buf.length )
-			bytes.writeFloat(buf[i]);
-	}
-	#end
-
-	public function playStream( onData : Void -> haxe.ds.Vector<Float> ) : SoundChannel {
-		#if flash
-		if( onStreamData == null )
-			snd.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, onFlashSample);
-		onStreamData = onData;
-		var c = new SoundChannel(this, true);
-		c.channel = snd.play(0, 0x7FFFFFFF);
-		return c;
-		#else
-		throw "Not implemented";
-		return null;
-		#end
-	}
-
-}

+ 2 - 2
hxd/snd/WavData.hx

@@ -5,8 +5,8 @@ class WavData extends hxd.snd.Data {
 
 
 	var rawData : haxe.io.Bytes;
 	var rawData : haxe.io.Bytes;
 
 
-	public function new(d) {
-		init(d);
+	public function new(bytes) {
+		init(new format.wav.Reader(new haxe.io.BytesInput(bytes)).read());
 	}
 	}
 
 
 	function init(d:format.wav.Data.WAVE) {
 	function init(d:format.wav.Data.WAVE) {

+ 83 - 37
hxd/snd/Worker.hx

@@ -16,12 +16,25 @@ private enum Message {
 
 
 private class WorkerChannel extends NativeChannel {
 private class WorkerChannel extends NativeChannel {
 	var w : Worker;
 	var w : Worker;
-	public function new(w) {
+	var c : NativeChannelData;
+	public function new(w,c) {
 		this.w = w;
 		this.w = w;
+		this.c = c;
 		super(w.bufferSamples);
 		super(w.bufferSamples);
 	}
 	}
 	override function onSample(out:haxe.io.Float32Array) {
 	override function onSample(out:haxe.io.Float32Array) {
-		@:privateAccess w.sampleData(out);
+		@:privateAccess w.sampleData(out, c);
+	}
+}
+
+class NativeChannelData {
+	public var next = 0.;
+	public var tmpBuf : haxe.io.Bytes;
+	public var channel : NativeChannel;
+	public var channels : Array<Channel>;
+	public function new(w:Worker) {
+		channels = [];
+		tmpBuf = haxe.io.Bytes.alloc(w.bufferSamples * 4 * 2);
 	}
 	}
 }
 }
 
 
@@ -37,15 +50,12 @@ class Worker extends hxd.Worker<Message> {
 
 
 	var channelID = 1;
 	var channelID = 1;
 	var cmap : Map<Int,Channel> = new Map();
 	var cmap : Map<Int,Channel> = new Map();
-	var snd : SoundData;
-	var channels : Array<Channel>;
-	var tmpBuf : haxe.io.Bytes;
-	var channel : NativeChannel;
+	var channels : Array<NativeChannelData>;
 
 
-	public function new( bufferSamples = 4096 ) {
+	public function new( nativeChannels = 4, bufferSamples = 4096 ) {
 		super(Message);
 		super(Message);
 		this.bufferSamples = bufferSamples;
 		this.bufferSamples = bufferSamples;
-		this.channels = [];
+		this.channels = [for( i in 0...nativeChannels ) new NativeChannelData(this)];
 	}
 	}
 
 
 	override function clone() {
 	override function clone() {
@@ -53,23 +63,54 @@ class Worker extends hxd.Worker<Message> {
 	}
 	}
 
 
 	function allocChannel( res, loop : Bool, volume : Float, time : Float ) {
 	function allocChannel( res, loop : Bool, volume : Float, time : Float ) {
-		var c = new Channel(isWorker ? null : this, res, channelID++, loop, volume, time);
-		channels.push(c);
+		var chan = isWorker ? selectChannel() : null;
+		var c = new Channel(isWorker ? null : this, chan, res, channelID++, loop, volume, time);
+		if( chan != null ) chan.channels.push(c);
 		cmap.set(c.id, c);
 		cmap.set(c.id, c);
 		return c;
 		return c;
 	}
 	}
 
 
+	function cleanChannels() {
+		for( c in channels )
+			if( c.channels.length == 0 && c.channel != null ) {
+				c.channel.stop();
+				c.channel = null;
+			}
+	}
+
+	function selectChannel() {
+		cleanChannels();
+		var free = -1, best : NativeChannelData = null;
+		// select best one to play
+		for( i in 0...channels.length ) {
+			var c = channels[i];
+			if( c.channel == null ) {
+				if( free < 0 ) free = i;
+				continue;
+			}
+			if( best == null || best.next > c.next ) best = c;
+		}
+		if( free >= 0 ) {
+			best = channels[free];
+			best.next = 0;
+		}
+		return best;
+	}
+
 	override function handleMessage( msg : Message ) {
 	override function handleMessage( msg : Message ) {
 		switch( msg ) {
 		switch( msg ) {
 		case Play(path, loop, volume, time):
 		case Play(path, loop, volume, time):
-			var c = allocChannel(null, loop, volume, time);
-			if( c == null )
-				return;
-
-			c.res = hxd.Res.loader.load(path).toSound();
-			c.snd = c.res.getData();
-			c.currentTime = time;
-
+			var res = hxd.Res.loader.load(path).toSound();
+			var snd = res.getData();
+			snd.load(function() {
+				var c = allocChannel(null, loop, volume, time);
+				if( c == null )
+					return;
+				c.res = res;
+				c.snd = snd;
+				c.currentTime = time;
+				if( c.channel.channel == null ) c.channel.channel = new WorkerChannel(this, c.channel);
+			});
 		case SetVolume(id, volume):
 		case SetVolume(id, volume):
 			var c = cmap.get(id);
 			var c = cmap.get(id);
 			if( c == null ) return;
 			if( c == null ) return;
@@ -78,7 +119,7 @@ class Worker extends hxd.Worker<Message> {
 		case Stop(id):
 		case Stop(id):
 			var c = cmap.get(id);
 			var c = cmap.get(id);
 			if( c == null ) return;
 			if( c == null ) return;
-			channels.remove(c);
+			c.channel.channels.remove(c);
 			cmap.remove(c.id);
 			cmap.remove(c.id);
 		case Fade(id, uid, vol, time):
 		case Fade(id, uid, vol, time):
 			var c = cmap.get(id);
 			var c = cmap.get(id);
@@ -108,12 +149,15 @@ class Worker extends hxd.Worker<Message> {
 			if( c != null ) c.currentTime = v;
 			if( c != null ) c.currentTime = v;
 		case Active(b):
 		case Active(b):
 			if( b ) {
 			if( b ) {
-				if( channel != null ) return;
-				channel = new WorkerChannel(this);
+				for( c in channels )
+					if( c != null && c.channel == null && c.channels.length > 0 )
+						c.channel = new WorkerChannel(this, c);
 			} else {
 			} else {
-				if( channel == null ) return;
-				channel.stop();
-				channel = null;
+				for( c in channels )
+					if( c.channel != null ) {
+						c.channel.stop();
+						c.channel = null;
+					}
 			}
 			}
 		case SetGlobalVolume(v):
 		case SetGlobalVolume(v):
 			volume = v;
 			volume = v;
@@ -124,8 +168,10 @@ class Worker extends hxd.Worker<Message> {
 		#if flash
 		#if flash
 		// make sure that the sounds system is initialized
 		// make sure that the sounds system is initialized
 		// https://bugbase.adobe.com/index.cfm?event=bug&id=3842828
 		// https://bugbase.adobe.com/index.cfm?event=bug&id=3842828
-		var s = new SoundData();
-		s.playStream(function() return new haxe.ds.Vector<Float>(bufferSamples * 2)).stop();
+		var s = new flash.media.Sound();
+		s.addEventListener(flash.events.SampleDataEvent.SAMPLE_DATA, function(e:flash.events.SampleDataEvent) { for( i in 0...bufferSamples * 2) e.data.writeFloat(0); } );
+		var chan = s.play();
+		if( chan != null ) chan.stop();
 		#end
 		#end
 		if( hxd.System.isAndroid )
 		if( hxd.System.isAndroid )
 			initActivate();
 			initActivate();
@@ -140,22 +186,21 @@ class Worker extends hxd.Worker<Message> {
 	}
 	}
 
 
 	override function setupWorker() {
 	override function setupWorker() {
-		tmpBuf = haxe.io.Bytes.alloc(bufferSamples * 4 * 2);
 		#if hxsdl
 		#if hxsdl
 		// we might create our worker before initializing SDL, let's make sure it's done.
 		// we might create our worker before initializing SDL, let's make sure it's done.
 		sdl.Sdl.init();
 		sdl.Sdl.init();
 		#end
 		#end
-		channel = new WorkerChannel(this);
 	}
 	}
 
 
-	function sampleData( out : haxe.io.Float32Array ) {
+	function sampleData( out : haxe.io.Float32Array, chan : NativeChannelData ) {
+		chan.next = haxe.Timer.stamp() + bufferSamples / 44100;
+		var cid = 0;
+		var cmax = chan.channels.length;
 		for( i in 0...bufferSamples*2 )
 		for( i in 0...bufferSamples*2 )
 			out[i] = 0;
 			out[i] = 0;
-		var cid = 0;
-		var cmax = channels.length;
 
 
 		while( cid < cmax ) {
 		while( cid < cmax ) {
-			var c = channels[cid++];
+			var c = chan.channels[cid++];
 
 
 			var w = 0;
 			var w = 0;
 			var snd = c.snd;
 			var snd = c.snd;
@@ -166,20 +211,21 @@ class Worker extends hxd.Worker<Message> {
 				var play = vol > 0 || c.volumeSpeed > 0;
 				var play = vol > 0 || c.volumeSpeed > 0;
 				if( c.position + size >= snd.samples ) {
 				if( c.position + size >= snd.samples ) {
 					size = snd.samples - c.position;
 					size = snd.samples - c.position;
-					if( play ) snd.decode(tmpBuf, 0, c.position, size);
+					if( play ) snd.decode(chan.tmpBuf, 0, c.position, size);
 					c.position = 0;
 					c.position = 0;
 				} else {
 				} else {
-					if( play ) snd.decode(tmpBuf, 0, c.position, size);
+					if( play ) snd.decode(chan.tmpBuf, 0, c.position, size);
 					c.position += size;
 					c.position += size;
 				}
 				}
 				if( !play ) {
 				if( !play ) {
 					w += size * 2;
 					w += size * 2;
 				} else {
 				} else {
 					#if flash
 					#if flash
-					flash.Memory.select(tmpBuf.getData());
+					flash.Memory.select(chan.tmpBuf.getData());
 					#end
 					#end
 					for( i in 0...size * 2 ) {
 					for( i in 0...size * 2 ) {
-						out[w++] += #if flash flash.Memory.getFloat #else tmpBuf.getFloat #end(i << 2) * vol;
+						var v = #if flash flash.Memory.getFloat #else chan.tmpBuf.getFloat #end(i << 2) * vol;
+						out[w++] += v;
 						if( c.volumeSpeed != 0 ) {
 						if( c.volumeSpeed != 0 ) {
 							c.vol += c.volumeSpeed;
 							c.vol += c.volumeSpeed;
 							if( (c.volumeSpeed > 0) == (c.vol > c.volumeTarget) ) {
 							if( (c.volumeSpeed > 0) == (c.vol > c.volumeTarget) ) {
@@ -193,7 +239,7 @@ class Worker extends hxd.Worker<Message> {
 				}
 				}
 				if( c.position == 0 ) {
 				if( c.position == 0 ) {
 					if( !c.loop ) {
 					if( !c.loop ) {
-						channels.remove(c);
+						chan.channels.remove(c);
 						cmap.remove(c.id);
 						cmap.remove(c.id);
 						cid--;
 						cid--;
 						cmax--;
 						cmax--;

+ 4 - 3
samples/sound/Sound.hx

@@ -16,7 +16,6 @@ class NoiseChannel extends hxd.snd.NativeChannel {
 
 
 class Sound extends hxd.App {
 class Sound extends hxd.App {
 
 
-	var chan : hxd.snd.SoundChannel;
 	var time = 0.;
 	var time = 0.;
 	static var music : hxd.snd.Worker;
 	static var music : hxd.snd.Worker;
 
 
@@ -24,8 +23,10 @@ class Sound extends hxd.App {
 		//var c = new NoiseChannel();
 		//var c = new NoiseChannel();
 		//haxe.Timer.delay(c.stop, 1000);
 		//haxe.Timer.delay(c.stop, 1000);
 
 
-//		var c = hxd.Res.music_loop.play(true);
-//		c.onEnd = function() trace("LOOP");
+		#if !cpp
+		var c = hxd.Res.music_loop.play(true);
+		c.onEnd = function() trace("LOOP");
+		#end
 	}
 	}
 
 
 	override function update(dt:Float) {
 	override function update(dt:Float) {