浏览代码

Fixed mp3 sampling, added NativeChannel hl support

Pavel Alexandrov 5 年之前
父节点
当前提交
d0ff33912c
共有 4 个文件被更改,包括 199 次插入41 次删除
  1. 38 29
      hxd/snd/Mp3Data.hx
  2. 111 0
      hxd/snd/NativeChannel.hx
  3. 1 0
      hxd/snd/openal/Driver.hx
  4. 49 12
      samples/Sound.hx

+ 38 - 29
hxd/snd/Mp3Data.hx

@@ -16,12 +16,13 @@ class Mp3Data extends Data {
 	#elseif hl
 	var bytes : haxe.io.Bytes;
 	var frameOffsets:Array<Int>;
-	var frameSamples:Array<Int>; // Sample offset of a frame.
+	// Sample count in one frame. After some searching, I couldn't find any mp3 files that contained frames with different Layers (hence - different samples/frame).
+	// Simplifies seeking quite a bit if we assume that we won't find any mp3 files with different sampling per frame.
+	var samplesPerFrame:Int;
 	var reader : Mp3File;
 	var frame : haxe.io.Bytes;
 	var currentFrame : Int;
 	var currentSample : Int;
-	var frameEnd : Int;
 	#end
 
 	public function new( bytes : haxe.io.Bytes ) {
@@ -76,19 +77,19 @@ class Mp3Data extends Data {
 		this.bytes = bytes;
 		// Re-reading MP3 to find offsets to each frame in file for seeking.
 		frameOffsets = new Array();
-		frameSamples = new Array();
+		samplesPerFrame = format.mp3.Tools.getSampleCountHdr(header);
 		var byteInput = new haxe.io.BytesInput(bytes);
 		var reader = new format.mp3.Reader(byteInput);
 		var ft;
 		var totalSamples : Int = 0;
 		while ( (ft = reader.seekFrame()) == FT_MP3 ) {
+			var headerPos = byteInput.position - 2; // 0xfff sync
 			var header = reader.readFrameHeader();
 			if ( header != null && !format.mp3.Tools.isInvalidFrameHeader(header) ) {
 				// TODO: Layer I support. Layer I frames contain only 384 frames.
 				var len = format.mp3.Tools.getSampleDataSizeHdr(header);
 				if ( byteInput.length - byteInput.position >= len ) {
-					frameOffsets.push(byteInput.position - 4);
-					frameSamples.push(totalSamples);
+					frameOffsets.push(headerPos);
 					totalSamples += format.mp3.Tools.getSampleCountHdr(header);
 					byteInput.position += len;
 				}
@@ -97,7 +98,6 @@ class Mp3Data extends Data {
 
 		this.frame = haxe.io.Bytes.alloc(1152*channels*4); // 2 channels, F32.
 		this.currentSample = -1;
-		this.frameEnd = -1;
 		this.currentFrame = -1;
 		this.reader = mp3_open(bytes, bytes.length);
 
@@ -166,31 +166,41 @@ class Mp3Data extends Data {
 		}
 		#elseif hl
 		if ( currentSample != sampleStart ) {
-			var i = frameSamples.length - 1;
-			while ( i >= 0 ) {
-				if ( frameSamples[i] <= sampleStart ) {
-					if ( this.currentFrame != i ) {
-						seekFrame(i);
-						this.currentSample = sampleStart;
-					}
-					break;
-				}
-				i--;
+			var targetFrame = Math.floor(sampleStart / samplesPerFrame);
+			if ( targetFrame != currentFrame ) {
+				seekFrame(Math.floor(sampleStart / samplesPerFrame));
 			}
+			this.currentSample = sampleStart;
+		}
+		var frameStart = currentFrame * samplesPerFrame;
+		var writeSamples:Int, offset:Int;
+		// Fill out remaining of the frame.
+		if ( currentSample != frameStart ) {
+			writeSamples = samplesPerFrame - (currentSample - frameStart);
+			offset = (currentSample - frameStart) * 4 * channels;
+			out.blit(outPos, frame, offset, frame.length - offset);
+			outPos += frame.length - offset;
+			
+			sampleCount -= writeSamples;
+			currentSample += writeSamples;
+			seekFrame(currentFrame + 1);
 		}
-		var targetSample = sampleStart + sampleCount;
-		while (frameEnd <= targetSample) {
-			var frameStart = frameSamples[currentFrame];
-			var offset = (frameEnd - currentSample) * 4 * channels;
-			out.blit(outPos, frame, (currentSample - frameStart) * 4 * channels, offset);
-			currentSample = frameEnd;
+		writeSamples = samplesPerFrame;
+		offset = samplesPerFrame * 4 * channels;
+		// Fill out frames that fit inside buffer
+		while ( sampleCount > writeSamples ) {
+			out.blit(outPos, frame, 0, frame.length);
 			outPos += offset;
+
+			sampleCount -= writeSamples;
+			currentSample += writeSamples;
 			seekFrame(currentFrame + 1);
 		}
-		if ( currentSample < targetSample ) {
-			var frameStart = frameSamples[currentFrame];
-			out.blit(outPos, frame, (currentSample - frameStart) * 4 * channels, (targetSample - currentSample) * channels * 4);
-			currentSample = targetSample;
+		// Fill beginning of the frame to the remainder of the buffer.
+		if ( sampleCount > 0 ) {
+			writeSamples = sampleCount;
+			out.blit(outPos, frame, 0, sampleCount * 4 * channels);
+			currentSample += sampleCount;
 		}
 		#else
 		throw "MP3 decoding is not available for this platform";
@@ -199,10 +209,9 @@ class Mp3Data extends Data {
 
 	#if hl
 
-	function seekFrame(to:Int) {
+	inline function seekFrame( to : Int ) {
 		currentFrame = to;
-		var samples : Int = mp3_decode_frame(reader, bytes, bytes.length, frameOffsets[to], frame, frame.length, 0);
-		frameEnd = frameSamples[to] + samples;
+		mp3_decode_frame(reader, bytes, bytes.length, frameOffsets[to], frame, frame.length, 0);
 	}
 	
 	@:hlNative("fmt", "mp3_open") static function mp3_open( bytes : hl.Bytes, size : Int ) : Mp3File {

+ 111 - 0
hxd/snd/NativeChannel.hx

@@ -1,5 +1,107 @@
 package hxd.snd;
 
+#if hlopenal
+
+import openal.AL;
+import hxd.snd.Manager;
+import hxd.snd.Driver;
+
+@:access(hxd.snd.Manager)
+private class ALChannel {
+
+	static var nativeUpdate : haxe.MainLoop.MainEvent;
+	static var nativeChannels : Array<ALChannel>;
+	
+	static function updateChannels() {
+		var i = 0;
+		// Should ensure ordering if it was removed during update?
+		for ( chn in nativeChannels ) chn.onUpdate();
+	}
+
+	var manager : Manager;
+	var update : haxe.MainLoop.MainEvent;
+	var native : NativeChannel;
+	var samples : Int;
+
+	var driver : Driver;
+	var buffers : Array<BufferHandle>;
+	var bufPos : Int;
+	var src : SourceHandle;
+
+	var fbuf : haxe.io.Bytes;
+	var ibuf : haxe.io.Bytes;
+
+	public function new(samples, native) {
+		if ( nativeUpdate == null ) {
+			nativeUpdate = haxe.MainLoop.add(updateChannels);
+			#if (haxe_ver >= 4) nativeUpdate.isBlocking = false; #end
+			nativeChannels = [];
+		}
+		this.native = native;
+		this.samples = samples;
+
+		this.manager = Manager.get();
+		this.driver = manager.driver;
+
+		buffers = [driver.createBuffer(), driver.createBuffer()];
+		src = driver.createSource();
+		bufPos = 0;
+		
+		// AL.sourcef(src,AL.PITCH,1.0);
+		// AL.sourcef(src,AL.GAIN,1.0);
+		fbuf = haxe.io.Bytes.alloc( samples<<3 );
+		ibuf = haxe.io.Bytes.alloc( samples<<2 );
+
+		for ( b in buffers )
+			onSample(b);
+		forcePlay();
+		nativeChannels.push(this);
+	}
+
+	public function stop() {
+		if ( src != null ) {
+			nativeChannels.remove(this);
+			driver.stopSource(src);
+			driver.destroySource(src);
+			for (buf in buffers)
+				driver.destroyBuffer(buf);
+			src = null;
+			buffers = null;
+		}
+	}
+
+	@:noDebug function onSample( buf : BufferHandle ) {
+		@:privateAccess native.onSample(haxe.io.Float32Array.fromBytes(fbuf));
+
+		// Convert Float32 to Int16
+		for ( i in 0...samples << 1 ) {
+			var v = Std.int(fbuf.getFloat(i << 2) * 0x7FFF);
+			ibuf.set( i<<1, v );
+			ibuf.set( (i<<1) + 1, v>>>8 );
+		}
+		driver.setBufferData(buf, ibuf, ibuf.length, I16, 2, Manager.STREAM_BUFFER_SAMPLE_COUNT);
+		driver.queueBuffer(src, buf, 0, false);
+	}
+
+	inline function forcePlay() {
+		if (!src.playing) driver.playSource(src);
+	}
+
+	function onUpdate(){
+		var cnt = driver.getProcessedBuffers(src);
+		while (cnt > 0)
+		{
+			cnt--;
+			var buf = buffers[bufPos];
+			driver.unqueueBuffer(src, buf);
+			onSample(buf);
+			forcePlay();
+			if (++bufPos == buffers.length) bufPos = 0;
+		}
+	}
+}
+
+#end
 class NativeChannel {
 
 	#if flash
@@ -38,6 +140,8 @@ class NativeChannel {
 	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 hlopenal
+	var channel : ALChannel;
 	#end
 	public var bufferSamples(default, null) : Int;
 
@@ -76,6 +180,8 @@ class NativeChannel {
 		time = currTime + front.duration;
 		queued.start(time);
 		
+		#elseif hlopenal
+		channel = new ALChannel(bufferSamples, this);
 		#end
 	}
 
@@ -177,6 +283,11 @@ class NativeChannel {
 			bufferPool.push(tmpBuffer);
 			tmpBuffer = null;
 		}
+		#elseif hlopenal
+		if( channel != null ) {
+			channel.stop();
+			channel = null;
+		}
 		#end
 	}
 

+ 1 - 0
hxd/snd/openal/Driver.hx

@@ -92,6 +92,7 @@ class Driver implements hxd.snd.Driver {
 
 	public function playSource(source : SourceHandle) : Void {
 		AL.sourcePlay(source.inst);
+		source.sampleOffset = 0;
 		source.playing = true;
 	}
 

+ 49 - 12
samples/Sound.hx

@@ -14,41 +14,78 @@ class NoiseChannel extends hxd.snd.NativeChannel {
 }
 
 
-class Sound extends hxd.App {
+class Sound extends SampleApp {
 
 	var time = 0.;
 	var slider : h2d.Slider;
 	var music : hxd.snd.Channel;
+	var musicPosition : h2d.Text;
+	var beeper:Bool = true;
 
 	override function init() {
+		super.init();
+
 		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();
 		if( res != null ) {
 			trace("Playing "+res);
 			music = res.play(true);
 			//music.queueSound(...);
 			music.onEnd = function() trace("LOOP");
+			// Use effect processing on the channel
+			music.addEffect(pitch);
 		}
 
-		slider = new h2d.Slider(300, 10, s2d);
-		slider.x = 150;
-		slider.y = 80;
-		if( music == null ) slider.remove();
+		slider = new h2d.Slider(300, 10);
 		slider.onChange = function() {
 			music.position = slider.value * music.duration;
 		};
+		musicPosition = new h2d.Text(getFont());
+
+		// slider.x = 150;
+		// slider.y = 80;
+		// if( music == null ) slider.remove();
+		// slider.onChange = function() {
+		// 	music.position = slider.value * music.duration;
+		// };
+		// musicPosition.setPosition(460, 80);
+		
+		addSlider("Global vol", function() { return hxd.snd.Manager.get().masterVolume; }, function(v) { hxd.snd.Manager.get().masterVolume = v; });
+		addCheck("Beeper", function() { return beeper; }, function(v) { beeper = v; });
+		addButton("Play noise", function() {
+			var c = new NoiseChannel();
+			haxe.Timer.delay(c.stop, 1000);
+		});
+		if ( music != null ) {
+			addCheck("Music mute", function() { return music.mute; }, function(v) { music.mute = v; });
+			addSlider("Music vol", function() { return music.volume; }, function(v) { music.volume = v; });
+			var f = new h2d.Flow(fui);
+			f.horizontalSpacing = 5;
+			var tf = new h2d.Text(getFont(), f);
+			tf.text = "Music pos";
+			tf.maxWidth = 70;
+			tf.textAlign = Right;
+			f.addChild(slider);
+			f.addChild(musicPosition);
+
+			addSlider("Pitch val", function() { return pitch.value; }, function(v) { pitch.value = v; }, 0, 2);
+		}
 	}
 
 	override function update(dt:Float) {
-		time += dt;
-		if( time > 1 ) {
-			time--;
-			hxd.Res.sound_fx.play();
-			engine.backgroundColor = 0xFFFF0000;
-		} else
-			engine.backgroundColor = 0;
+		if ( beeper ) {
+			time += dt;
+			if( time > 1 ) {
+				time--;
+				hxd.Res.sound_fx.play();
+				engine.backgroundColor = 0xFFFF0000;
+			} else
+				engine.backgroundColor = 0;
+		}
 
 		if( music != null ) {
 			slider.value = music.position / music.duration;
+			musicPosition.text = hxd.Math.fmt(music.position) + "/" + hxd.Math.fmt(music.duration);
 			if( hxd.Key.isPressed(hxd.Key.M) ) {
 				music.mute = !music.mute;
 			}