diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx new file mode 100644 index 00000000..4884a926 --- /dev/null +++ b/source/flixel/sound/FlxSound.hx @@ -0,0 +1,955 @@ +package flixel.sound; + +import lime.media.AudioBuffer; +import lime.media.AudioSource; +import lime.media.openal.AL; +#if lime_vorbis +import lime.media.vorbis.VorbisFile; +#end + +import openfl.Assets; +import openfl.events.IEventDispatcher; +import openfl.events.Event; +import openfl.media.Sound; +import openfl.media.SoundChannel; +import openfl.media.SoundTransform; +import openfl.media.SoundMixer; +import openfl.net.URLRequest; +import openfl.utils.AssetType; +#if flash11 +import openfl.utils.ByteArray; +#end + +import flixel.math.FlxMath; +import flixel.math.FlxPoint; +import flixel.system.FlxAssets.FlxSoundAsset; +import flixel.tweens.FlxTween; +import flixel.util.FlxSignal; +import flixel.util.FlxStringUtil; +import flixel.FlxBasic; +import flixel.FlxObject; +import flixel.FlxG; + +@:access(flixel.FlxGame) +class FlxSound extends FlxBasic { + /** + * The x position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. + */ + public var x:Float; + + /** + * The y position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. + */ + public var y:Float; + + /** + * Whether or not this sound should be automatically destroyed when you switch states. + */ + public var persist:Bool; + + /** + * Whether or not the sound is currently playing. + */ + public var playing(get, never):Bool; + + /** + * Set volume to a value between 0 and 1 to change how this sound is. + */ + public var volume(get, set):Float; + + /** + * Whether to make this sound muted or not. + */ + public var muted(default, set):Bool; + + #if FLX_PITCH + /** + * Set pitch, which also alters the playback speed. Default is 1. + * @since 5.0.0 + */ + public var pitch(get, set):Float; + + /** + * Alters the pitch of the sound depends on the current FlxG.timeScale. Default is true. + * @since raltyMod + */ + public var timeScaleBased:Bool; + #end + + /** + * Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this. + */ + public var pan(get, set):Float; + + /** + * The position in runtime of the music playback in milliseconds. + * If set while paused, changes only come into effect after a `resume()` call. + */ + public var time(get, set):Float; + + /** + * The offset for this FlxSound. + * Useful for just generally offsetting this sound without affecting time. + * @since raltyMod + */ + public var offset(get, set):Float; + + /** + * The length of the sound in milliseconds. + * @since 4.2.0 + */ + public var length(get, never):Float; + + /** + * The latency of the sound in milliseconds. + * @since raltyMod + */ + //public var latency(get, never):Float; + + /** + * Whether or not this sound should loop. + */ + public var looped(default, set):Bool; + + /** + * In case of looping, the point (in milliseconds) from where to restart the sound when it loops back + * @since 4.1.0 + */ + public var loopTime(default, set):Float; + + /** + * At which point to stop playing the sound, in milliseconds. + * If not set / `null`, the sound completes normally. + * @since 4.2.0 + */ + public var endTime(default, set):Null; + + /** + * The sound's "target" (for proximity and panning). + * @since raltyMod + */ + public var target:Null; + + /** + * The maximum effective radius of this sound (for proximity and panning). + * @since raltyMod + */ + public var radius:Float; + + /** + * Whether the proximity alters the pan or not + * @since raltyMod + */ + public var proximityPan:Bool; + + /** + * Stores for how much channels are in the loaded sound. + * @since raltyMod + */ + public var channels(get, never):Int; + + /** + * Whether or not this sound is stereo instead of mono. + * @since raltyMod + */ + public var stereo(get, never):Bool; + + /** + * Whether or not this sound is loaded yet. + * @since raltyMod + */ + public var loaded(get, never):Bool; + + /** + * Whether or not this sound is streamed, only vorbis though. + * @since raltyMod + */ + public var streamed(get, never):Bool; + + /** + * Stores the average wave amplitude of both stereo channels. + * @since raltyMod + */ + public var amplitude(get, never):Float; + + /** + * Just the amplitude of the left stereo channel + * @since raltyMod + */ + public var amplitudeLeft(get, never):Float; + + /** + * Just the amplitude of the right stereo channel + * @since raltyMod + */ + public var amplitudeRight(get, never):Float; + + /** + * The ID3 song name. Defaults to null. Currently only works for MP3 streamed sounds. + */ + public var name(default, null):String; + + /** + * The ID3 artist name. Defaults to null. Currently only works for MP3 streamed sounds. + */ + public var artist(default, null):String; + + /** + * Whether to call `destroy()` when the sound has finished playing. + */ + public var autoDestroy:Bool; + + /** + * Signal that is dispatched on sound complete. + */ + public final onFinish:FlxSignal; + + /** + * Tracker for sound complete callback. If assigned, will be called + * each time when sound reaches its end. + */ + //@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.") + public var onComplete:Void->Void; + + /** + * The sound group this sound belongs to + */ + public var group(default, set):FlxSoundGroup; + + /** + * Stores the sound lime AudioBuffer if exists. + * @since raltyMod + */ + public var buffer(get, never):AudioBuffer; + + #if lime_vorbis + /** + * Stores the sound VorbisFile if exists. + * @since raltyMod + */ + public var vorbis(get, never):VorbisFile; + #end + + /** + * The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`) + * @since 4.1.0 + */ + public var fadeTween:FlxTween; + + @:allow(flixel.system.frontEnds.SoundFrontEnd.load) var _sound:Sound; + var _transform:SoundTransform; + var _channel:SoundChannel; + var _source:AudioSource; + var _paused:Bool; + var _volume:Float; + var _volumeAdjust:Float; + var _time:Float; + var _offset:Float; + var _timeInterpolation:Float; + var _lastTime:Null; // FlxG.game.getTicks(), in MS + var _length:Float; + #if FLX_PITCH + var _pitch:Float; + var _timeScaleAdjust:Float; + var _realPitch:Float; + #end + var _amplitudeLeft:Float; + var _amplitudeRight:Float; + var _amplitudeUpdate:Bool; + var _amplitudeTime:Float; + var _alreadyPaused:Null; + + public function new() { + super(); + onFinish = new FlxSignal(); + revive(); + } + + /** + * Resets this FlxSound properties. + * + * @param clean Whether if this FlxSound also needs to be cleaned up too. + */ + public function reset():Void { + if (_source != null) stop(); + x = y = 0; + + muted = false; + looped = false; + loopTime = 0; + endTime = null; + autoDestroy = false; + visible = false; + target = null; + radius = 0; + proximityPan = true; + onComplete = null; + timeScaleBased = true; + + _alreadyPaused = null; + _cameras = null; + _lastTime = null; + _paused = false; + _length = _time = 0; + _volume = _volumeAdjust = 1; + _amplitudeLeft = _amplitudeRight = 0; + _amplitudeUpdate = true; + #if FLX_PITCH _pitch = _realPitch = _timeScaleAdjust = 1; #end + } + + /** + * Internal cleanup function for cleaning up this FlxSound. + * @since raltyMod + */ + function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess { + active = false; + _lastTime = null; + + if (_channel != null) { + _channel.removeEventListener(Event.SOUND_COMPLETE, stopped); + } + + if (destroySound) { + onFinish.removeAll(); + + if (group != null) group.remove(this); + + if (_channel != null) { + _channel.stop(); + _channel = null; + } + if (_source != null) _source.dispose(); + _source = null; + _sound = null; + + reset(); + } + else if (_channel != null && _channel.__isValid) { + if (resetPosition) { + _source.stop(); + + _time = 0; + _paused = false; + } + else if (!_paused) { + get_time(); + _source.pause(); + } + } + } + + /** + * Handles fade out, fade in, panning, proximity, and amplitude operations each frame. + */ + override function update(elapsed:Float):Void { + if (!playing) return; + + var timeScaleTarget = timeScaleBased ? FlxG.timeScale : 1.0; + if (_timeScaleAdjust != timeScaleTarget) { + _timeScaleAdjust = timeScaleTarget; + pitch = _pitch; + if (_channel == null) return; + } + + _amplitudeUpdate = true; + + var radialMultiplier = 1.0; + + // Distance-based volume control (TODO for Ralty: REDO THIS) + if (target != null) { + var targetPosition = target.getPosition(); + radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / radius; + targetPosition.put(); + radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1); + + if (proximityPan && _transform != null) { + var d:Float = (x - target.x) / radius; + _transform.pan = FlxMath.bound(d, -1, 1); + } + } + + _volumeAdjust = radialMultiplier; + updateTransform(); + } + + override function revive() { + reset(); + super.revive(); + } + + override function kill() { + super.kill(); + reset(); + } + + override function destroy() { + super.destroy(); + cleanup(true); + } + + /** + * One of the main setup functions for sounds, this function loads a sound from an embedded MP3. + * + * @param embeddedSound An embedded Class object representing an MP3 file. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @param onComplete Called when the sound finished playing + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadEmbedded(embeddedSound:FlxSoundAsset, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound { + if (!exists || embeddedSound == null) return this; + cleanup(true); + + if ((embeddedSound is Sound)) _sound = embeddedSound; + else if ((embeddedSound is Class)) _sound = Type.createInstance(embeddedSound, []); + else if ((embeddedSound is String)) { + if (Assets.exists(embeddedSound, AssetType.MUSIC) && this == FlxG.sound.music) + _sound = Assets.getMusic(embeddedSound); + else if (Assets.exists(embeddedSound, AssetType.SOUND)) + _sound = Assets.getSound(embeddedSound); + else + FlxG.log.error('Could not find a Sound asset with an ID of \'$embeddedSound\'.'); + } + + return init(looped, autoDestroy, onComplete); + } + + /** + * One of the main setup functions for sounds, this function loads a sound from a URL. + * + * @param soundURL A string representing the URL of the MP3 file you want to play. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @param onComplete Called when the sound finished playing + * @param onLoad Called when the sound finished loading. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadStream(soundURL:String, looped = false, autoDestroy = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound { + if (!exists) return this; + cleanup(true); + + _sound = new Sound(); + _sound.addEventListener(Event.ID3, gotID3); + var loadCallback:Event->Void = null; + loadCallback = function(e:Event) + { + (e.target : IEventDispatcher).removeEventListener(e.type, loadCallback); + // Check if the sound was destroyed before calling. Weak ref doesn't guarantee GC. + if (_sound == e.target) + { + _length = _sound.length; + if (onLoad != null) + onLoad(); + } + } + // Use a weak reference so this can be garbage collected if destroyed before loading. + _sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true); + _sound.load(new URLRequest(soundURL)); + + return init(looped, autoDestroy, onComplete); + } + + #if flash11 + /** + * One of the main setup functions for sounds, this function loads a sound from a ByteArray. + * + * @param bytes A ByteArray object. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadByteArray(bytes:ByteArray, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound { + if (!exists) return this; + cleanup(true); + + _sound = new Sound(); + _sound.addEventListener(Event.ID3, gotID3); + _sound.loadCompressedDataFromByteArray(bytes, bytes.length); + + return init(looped, autoDestroy, onComplete); + } + #end + + function init(?looped:Null, ?autoDestroy:Null, ?onComplete:NullVoid>):FlxSound { + if (looped != null) this.looped = looped; + if (autoDestroy != null) this.autoDestroy = autoDestroy; + if (onComplete != null) this.onComplete = onComplete; // DEPRECATED + + if (_sound != null) makeChannel(); + else _length = 0; + + endTime = null; + + return this; + } + + /** + * Call inbetween init function if a sound asset does exists. + * @since raltyMod + */ + function makeChannel() @:privateAccess { + if (_source != null) _source.dispose(); + + if (_channel == null) (_source = (_channel = new SoundChannel(null)).__source = new AudioSource(_sound.__buffer)).gain = 0; + else { + (_source = new AudioSource(_sound.__buffer)).gain = 0; + _channel.__dispose(); + _channel.__source = _source; + SoundMixer.__registerSoundChannel(_channel); + } + if (_transform == null) _transform = new SoundTransform(); + _transform.pan = 0; + _length = _source.length; + + _source.onComplete.add(_channel.source_onComplete); + _source.onLoop.add(_channel.source_onLoop); + _channel.__soundTransform = _transform; + _channel.__isValid = true; + } + + /** + * Call after adjusting the volume to update the sound channel's settings. + */ + @:allow(flixel.sound.FlxSoundGroup) + function updateTransform() { + if (_transform == null) { + _transform = new SoundTransform(); + pan = pan; + } + _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || muted) ? 0 : 1) * FlxG.sound.volume * #end + (group != null ? group.volume : 1) * _volume * _volumeAdjust; + + if (_channel != null) _channel.soundTransform = _transform; + } + + /** + * Call this function if you want this sound's volume to change + * based on distance from a particular FlxObject. + * + * @param X The X position of the sound. + * @param Y The Y position of the sound. + * @param TargetObject The object you want to track. + * @param Radius The maximum distance this sound can travel. + * @param Pan Whether panning should be used in addition to the volume changes. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function proximity(x = 0.0, y = 0.0, ?targetObject:FlxObject, ?radius:Float, pan = true):FlxSound { + setPosition(x, y); + if (targetObject != null) this.target = targetObject; + if (radius != null) this.radius = radius; + proximityPan = pan; + + return this; + } + + /** + * Helper function to set the coordinates of this object. + * Sound positioning is used in conjunction with proximity/panning. + * + * @param x The new x position + * @param y The new y position + */ + public inline function setPosition(x = 0.0, y = 0.0):Void { + this.x = x; + this.y = y; + } + + /** + * Call this function to play the sound - also works on paused sounds. + * + * @param forceRestart Whether to start the sound over or not. + * Default value is false, meaning if the sound is already playing or was + * paused when you call play(), it will continue playing from its current + * position, NOT start again from the beginning. + * @param startTime At which point to start playing the sound, in milliseconds. + * @param endTime At which point to stop playing the sound, in milliseconds. + * If not set / `null`, the sound completes normally. + */ + public function play(forceRestart = false, startTime = 0.0, ?endTime:Null):FlxSound { + if (!exists) return this; + + if (forceRestart) cleanup(false, true); + else if (playing) return this; + + if (endTime != null) this.endTime = endTime; + if (_paused) resume(); + else startSound(startTime); + + return this; + } + + /** + * Unpause a sound. Only works on sounds that have been paused. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function resume():FlxSound { + if (_paused) startSound(_time); + return this; + } + + /** + * Call this function to pause this sound. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function pause():FlxSound { + if (!playing) return this; + + cleanup(false, false); + _paused = true; + return this; + } + + /** + * Call this function to stop this sound. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function stop():FlxSound { + cleanup(autoDestroy, true); + return this; + } + + /** + * Helper function that tweens this sound's volume. + * + * @param duration The amount of time the fade-out operation should take. + * @param to The volume to tween to, 0 by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void):FlxSound { + if (fadeTween != null) fadeTween.cancel(); + fadeTween = FlxTween.num(volume, to, duration, {onComplete: onComplete}, volumeTween); + + return this; + } + + /** + * Helper function that tweens this sound's volume. + * If the sound wasn't playing at all, it'll play before the tween starts. + * + * @param duration The amount of time the fade-in operation should take. + * @param from The volume to tween from, 0 by default. + * @param to The volume to tween to, 1 by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void):FlxSound { + if (!playing) play(); + if (fadeTween != null) fadeTween.cancel(); + fadeTween = FlxTween.num(from, to, duration, {onComplete: onComplete}, volumeTween); + + return this; + } + + function volumeTween(f:Float) volume = f; + + /** + * Returns the currently selected "real" volume of the sound (takes fades and proximity into account). + * + * @return The adjusted volume of the sound. + */ + public inline function getActualVolume():Float + return _volume * _volumeAdjust; + + #if FLX_PITCH + /** + * Returns the currently selected "real" pitch of the sound. + * + * @return The adjusted pitch of the sound. + */ + public inline function getActualPitch():Float + return _realPitch; + #end + + /** + * Returns the actual time coming from the internal, can be used for detecting sync error. + * + * @return The actual time of the sound. + */ + public inline function getActualTime():Float { + get_time(); + return _time; + } + + /** + * An internal helper function used to attempt to start playing + * the sound and populate the _channel variable. + */ + var _tries = 0; + function startSound(StartTime:Float) @:privateAccess { + if (_sound == null) return; + + _paused = false; + _time = StartTime; + _lastTime = FlxG.game.getTicks(); + if (_channel == null || !_channel.__isValid || _source.__backend == null || _source.__backend.disposed #if lime_openal || _source.__backend.handle == null #end) + makeChannel(); + + if (_channel != null) { + #if FLX_PITCH + _timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0; + _realPitch = -1.0; + pitch = _pitch; + #end + + // TODO: fix the buffer loud beep for streaming sounds... + #if lime_openal + _source.__backend.setGain(0); + #end + + _channel.soundTransform = _transform; + _channel.__lastPeakTime = 0; + _channel.__leftPeak = 0; + _channel.__rightPeak = 0; + _channel.addEventListener(Event.SOUND_COMPLETE, stopped); + _source.__backend.playing = true; + _source.offset = 0; + _source.currentTime = _time; + + #if lime_openal + if (_source.__backend.handle == null) return destroy(); + + var s = false; + try {s = AL.getSourcei(_source.__backend.handle, AL.SOURCE_STATE) == AL.PLAYING;} catch(e) {} + if (s) updateTransform(); + else { + if (++_tries > 10) return; + makeChannel(); + return startSound(StartTime); + } + + _tries = 0; + #end + + looped = looped; + loopTime = loopTime; + endTime = endTime; + _amplitudeTime = -1; + + active = true; + } + else { + exists = false; + active = false; + } + } + + function stopped(?_) { + onFinish.dispatch(); + + if (onComplete != null) onComplete(); + + if (looped) { + cleanup(false); + play(false, loopTime - _offset, endTime - _offset); + } + else cleanup(autoDestroy); + } + + /** + * Internal event handler for ID3 info (i.e. fetching the song name). + */ + function gotID3(_) { + name = _sound.id3.songName; + artist = _sound.id3.artist; + _sound.removeEventListener(Event.ID3, gotID3); + } + + #if FLX_SOUND_SYSTEM + @:allow(flixel.system.frontEnds.SoundFrontEnd) + function onFocus() if (!_alreadyPaused) { + resume(); + _alreadyPaused = null; + } + + @:allow(flixel.system.frontEnds.SoundFrontEnd) + function onFocusLost() if (_alreadyPaused == null && !(_alreadyPaused = _paused)) pause(); + #end + + function set_group(value:FlxSoundGroup):FlxSoundGroup { + if (value != null) value.add(this); + else group.remove(this); + return group; + } + + inline function get_playing():Bool @:privateAccess + return _channel != null && _channel.__isValid && _source.playing; + + inline function get_volume():Float + return _volume; + + inline function set_volume(Volume:Float):Float { + _volume = FlxMath.bound(Volume, 0, 4); + updateTransform(); + return _volume; + } + + inline function set_muted(Muted:Bool):Bool { + muted = Muted; + updateTransform(); + return muted; + } + + inline function get_loaded():Bool + return buffer != null; + + inline function get_streamed():Bool + return #if lime_vorbis vorbis != null #else false #end; + + inline function get_buffer():AudioBuffer + @:privateAccess return _sound != null ? _sound.__buffer : null; + + #if lime_vorbis + inline function get_vorbis():VorbisFile + @:privateAccess return _sound != null ? _sound.__buffer.__srcVorbisFile : null; + #end + + function update_amplitude():Void @:privateAccess { + if (_channel == null || _time == _amplitudeTime || !_amplitudeUpdate) return; + _channel.__updatePeaks(); + + _amplitudeUpdate = false; + _amplitudeLeft = _channel.__leftPeak; + _amplitudeRight = _channel.__rightPeak; + _amplitudeTime = _time; + } + + inline function get_amplitudeLeft():Float { + update_amplitude(); + return _amplitudeLeft; + } + + inline function get_amplitudeRight():Float { + update_amplitude(); + return _amplitudeRight; + } + + inline function get_amplitude():Float { + update_amplitude(); + return if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft; + } + + inline function get_channels():Int + @:privateAccess return (buffer != null) ? buffer.channels : 0; + + inline function get_stereo():Bool + return channels > 1; + + #if FLX_PITCH + inline function get_pitch():Float + return _pitch; + + function set_pitch(v:Float):Float { + var adjusted:Float = FlxMath.bound(v * _timeScaleAdjust, 0); + if (_channel != null && _realPitch != adjusted) { + if ((_channel.pitch = adjusted) > 0 && _realPitch <= 0) { + _realPitch = adjusted; + time = _time; + } + else { + get_time(); + _realPitch = adjusted; + } + } + return _pitch = FlxMath.bound(v, 0); + } + #end + + function set_looped(v:Bool):Bool { + if (playing) { + if (v) _channel.loops = 999; + else _channel.loops = 0; + } + return looped = v; + } + + function set_loopTime(v:Float):Float { + if (playing) _channel.loopTime = v; + return loopTime = v; + } + + function set_endTime(v:Null):Null { + if (playing) { + if (v != null && v > 0 && v < length) _channel.endTime = v; + else _channel.endTime = null; + } + return endTime = v; + } + + inline function get_pan():Float return _transform.pan; + inline function set_pan(pan:Float):Float return _transform.pan = pan; + + inline function getFakeTime():Float { + if (@:privateAccess _channel.__isValid && _source.playing && _realPitch > 0 && _lastTime != null) + return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation; + else + return _time; + } + function get_time():Float { + if (_channel == null) return _time; + + final pos = _channel.position - _offset; + if (!playing || _realPitch <= 0) { + _lastTime = null; + return _time = pos; + } + + final fakeTime = getFakeTime(); + if (pos != _time) { + _lastTime = FlxG.game.getTicks(); + if ((_timeInterpolation = 1 - Math.min(fakeTime - pos, 1000) * 0.001) < 1 && _timeInterpolation > .9) + return _time = fakeTime; + else { + _timeInterpolation = 1; + return _time = pos; + } + } + else + return fakeTime; + } + + function set_time(time:Float):Float @:privateAccess { + time = FlxMath.bound(time, 0, length - 1); + if (_channel != null && _realPitch > 0) { + if (!_channel.__isValid) { + cleanup(false, true); + startSound(time); + } + else if (playing) { + _source.offset = 0; + _source.currentTime = time + _offset; + } + } + + _lastTime = null; + return _time = time; + } + + function get_offset():Float return _offset; + function set_offset(offset:Float):Float { + if (offset == _offset) return offset; + _offset = offset; + time = time; + return offset; + } + + inline function get_length():Float return _length - _offset; + + /*function get_latency():Float { + if (_channel != null) return _source.latency; + return 0; + }*/ + + override function toString():String { + return FlxStringUtil.getDebugString([ + LabelValuePair.weak("playing", playing), + LabelValuePair.weak("time", _time), + LabelValuePair.weak("length", length), + LabelValuePair.weak("volume", volume), + LabelValuePair.weak("pitch", pitch) + ]); + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx index aaf47774..7ffcf4d8 100644 --- a/source/funkin/backend/system/Conductor.hx +++ b/source/funkin/backend/system/Conductor.hx @@ -160,11 +160,11 @@ class Conductor private static function __updateSongPos(elapsed:Float) { if (FlxG.sound.music == null || !FlxG.sound.music.playing) { - lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.time - songOffset : -songOffset; + lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.getActualTime() - songOffset : -songOffset; return; } - if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) { + if (lastSongPos != (lastSongPos = FlxG.sound.music.getActualTime() - songOffset)) { // update conductor songPosition = lastSongPos; } else { diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index b72003c0..f88ef235 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -812,10 +812,12 @@ class CoolUtil * @param music Music */ public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) { - if (frontEnd.music != null) - @:privateAccess frontEnd.destroySound(frontEnd.music); + if (frontEnd.music == music) return; + + if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music); frontEnd.list.remove(music); - frontEnd.music = music; + frontEnd.defaultMusicGroup.add(frontEnd.music = music); + music.persist = true; } /** diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index f5f6bc5b..d81c9444 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -509,7 +509,7 @@ class PlayState extends MusicBeatState @:noCompletion @:dox(hide) private var _endSongCalled:Bool = false; @:dox(hide) - var __vocalOffsetViolation:Float = 0; + var __vocalSyncTimer:Float = 0; private function get_accuracy():Float { if (accuracyPressedNotes <= 0) return -1; @@ -999,21 +999,11 @@ class PlayState extends MusicBeatState inst.onComplete = endSong; - if (!paused) { - FlxG.sound.setMusic(inst); - FlxG.sound.music.play(); - } - vocals.play(); + var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; + inst.time = vocals.time = time; - vocals.pause(); - inst.pause(); for (strumLine in strumLines.members) { - strumLine.vocals.play(); - strumLine.vocals.pause(); - } - inst.time = vocals.time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; - for (strumLine in strumLines.members) { - strumLine.vocals.time = vocals.time; + strumLine.vocals.time = time; strumLine.vocals.play(); } vocals.play(); @@ -1028,15 +1018,14 @@ class PlayState extends MusicBeatState scripts.call("destroy"); for(g in __cachedGraphics) g.useCount--; - @:privateAccess - for (strumLine in strumLines.members) - FlxG.sound.destroySound(strumLine.vocals); - super.destroy(); - scripts = FlxDestroyUtil.destroy(scripts); @:privateAccess { - FlxG.sound.destroySound(inst); + for (strumLine in strumLines.members) FlxG.sound.destroySound(strumLine.vocals); + if (FlxG.sound.music != inst) FlxG.sound.destroySound(inst); FlxG.sound.destroySound(vocals); } + scripts = FlxDestroyUtil.destroy(scripts); + + super.destroy(); WindowUtils.resetAffixes(); SaveWarning.reset(); @@ -1079,15 +1068,14 @@ class PlayState extends MusicBeatState curSong = songData.meta.name.toLowerCase(); curSongID = curSong.replace(" ", "-"); - inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty)); + FlxG.sound.setMusic(inst = FlxG.sound.load(Assets.getMusic(Paths.inst(SONG.meta.name, difficulty)))); if (SONG.meta.needsVoices != false && Assets.exists(Paths.voices(SONG.meta.name, difficulty))) // null or true vocals = FlxG.sound.load(Paths.voices(SONG.meta.name, difficulty)); else vocals = new FlxSound(); - inst.group = FlxG.sound.defaultMusicGroup; - vocals.group = FlxG.sound.defaultMusicGroup; - inst.persist = vocals.persist = false; + vocals.group = FlxG.sound.defaultMusicGroup; + vocals.persist = false; generatedMusic = true; } @@ -1191,14 +1179,15 @@ class PlayState extends MusicBeatState vocals.pause(); for (strumLine in strumLines.members) strumLine.vocals.pause(); - FlxG.sound.music.play(); - Conductor.songPosition = FlxG.sound.music.time; - vocals.time = Conductor.songPosition + Conductor.songOffset; + var time = Conductor.songPosition + Conductor.songOffset; + inst.time = vocals.time = time; for (strumLine in strumLines.members) { - strumLine.vocals.time = vocals.time; + strumLine.vocals.time = time; strumLine.vocals.play(); } vocals.play(); + inst.play(); + gameAndCharsCall("onVocalsResync"); } @@ -1333,22 +1322,19 @@ class PlayState extends MusicBeatState if (Conductor.songPosition >= 0) startSong(); } - } else if (FlxG.sound.music != null) { - var instTime = FlxG.sound.music.time; - var isOffsync:Bool = vocals.time != instTime; - if(!isOffsync) { - for(strumLine in strumLines.members) { - if(strumLine.vocals.time != instTime) { - isOffsync = true; - break; - } + } + else if (FlxG.sound.music != null && (__vocalSyncTimer -= elapsed) < 0) { + __vocalSyncTimer = 1; + + var instTime = FlxG.sound.music.getActualTime(); + var isOffsync:Bool = vocals.loaded && Math.abs(instTime - vocals.getActualTime()) > 30; + if (!isOffsync) { + for (strumLine in strumLines.members) { + if ((isOffsync = strumLine.vocals.loaded && Math.abs(instTime - strumLine.vocals.getActualTime()) > 30)) break; } } - __vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (isOffsync ? elapsed : -elapsed / 2)); - if (__vocalOffsetViolation > Flags.VOCAL_OFFSET_VIOLATION_THRESHOLD) { - resyncVocals(); - __vocalOffsetViolation = 0; - } + + if (isOffsync) resyncVocals(); } while(events.length > 0 && events.last().time <= Conductor.songPosition) diff --git a/source/funkin/menus/PauseSubState.hx b/source/funkin/menus/PauseSubState.hx index e8ad62af..47b1a9a6 100644 --- a/source/funkin/menus/PauseSubState.hx +++ b/source/funkin/menus/PauseSubState.hx @@ -65,7 +65,7 @@ class PauseSubState extends MusicBeatSubstate menuItems = event.options; - pauseMusic = FlxG.sound.load(Paths.music(event.music), 0, true); + pauseMusic = FlxG.sound.load(Assets.getMusic(Paths.music(event.music)), 0, true); pauseMusic.persist = false; pauseMusic.group = FlxG.sound.defaultMusicGroup; pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2))); diff --git a/source/lime/_internal/backend/html5/HTML5AudioSource.hx b/source/lime/_internal/backend/html5/HTML5AudioSource.hx index bfca4198..7fd31c4e 100644 --- a/source/lime/_internal/backend/html5/HTML5AudioSource.hx +++ b/source/lime/_internal/backend/html5/HTML5AudioSource.hx @@ -2,6 +2,7 @@ package lime._internal.backend.html5; import lime.math.Vector4; import lime.media.AudioSource; +import lime.media.AudioManager; @:access(lime.media.AudioBuffer) class HTML5AudioSource @@ -9,7 +10,7 @@ class HTML5AudioSource private var completed:Bool; private var gain:Float; private var id:Int; - private var length:Float; + private var length:Null; private var loops:Int; private var parent:AudioSource; private var playing:Bool; @@ -24,7 +25,9 @@ class HTML5AudioSource position = new Vector4(); } - public function dispose():Void {} + public function dispose():Void { + stop(); + } public function init():Void {} @@ -98,8 +101,9 @@ class HTML5AudioSource { loops--; stop(); - // currentTime = 0; + if (loopTime != null && loopTime > 0) setCurrentTime(loopTime); play(); + parent.onLoop.dispatch(); return; } else if (parent.buffer != null && parent.buffer.__srcHowl != null) @@ -128,7 +132,7 @@ class HTML5AudioSource } else if (parent.buffer != null && parent.buffer.__srcHowl != null) { - var time = (parent.buffer.__srcHowl.seek(id) * 1000) - parent.offset; + var time = parent.buffer.__srcHowl.seek(id) * 1000.0 - parent.offset; if (time < 0) return 0; return time; } @@ -137,6 +141,20 @@ class HTML5AudioSource return 0; } + public function getLatency():Float + { + var ctx = AudioManager.context.web; + if (ctx != null) + { + var baseLatency:Float = untyped ctx.baseLatency != null ? untyped ctx.baseLatency : 0; + var outputLatency:Float = untyped ctx.outputLatency != null ? untyped ctx.outputLatency : 0; + + return (baseLatency + outputLatency) * 1000; + } + + return 0; + } + public function setCurrentTime(value:Float):Float { #if lime_howlerjs @@ -172,7 +190,7 @@ class HTML5AudioSource return gain = value; } - public function getLength():Float + public function getLength():Null { if (length != 0) { @@ -182,14 +200,14 @@ class HTML5AudioSource #if lime_howlerjs if (parent.buffer != null && parent.buffer.__srcHowl != null) { - return parent.buffer.__srcHowl.duration() * 1000; + return parent.buffer.__srcHowl.duration() * 1000.0; } #end return 0; } - public function setLength(value:Float):Float + public function setLength(value:Null):Null { return length = value; } @@ -204,6 +222,14 @@ class HTML5AudioSource return loops = value; } + public function getLoopTime():Float { + return loopTime; + } + + public function setLoopTime(value:Float):Float { + return loopTime = value; + } + public function getPitch():Float { #if lime_howlerjs @@ -218,9 +244,11 @@ class HTML5AudioSource #if lime_howlerjs parent.buffer.__srcHowl.rate(value); #end + return getPitch(); } + public function getPosition():Vector4 { #if lime_howlerjs @@ -250,4 +278,4 @@ class HTML5AudioSource return position; } -} \ No newline at end of file +} diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index e7cbd821..dcd84632 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,236 +1,193 @@ package lime._internal.backend.native; -import haxe.Int64; import haxe.Timer; -import lime.math.Vector4; +import haxe.Int64; + import lime.media.openal.AL; import lime.media.openal.ALBuffer; import lime.media.openal.ALSource; import lime.media.vorbis.VorbisFile; -import lime.media.AudioManager; + +import lime.math.Vector4; +import lime.media.AudioBuffer; import lime.media.AudioSource; -import lime.utils.UInt8Array; +import lime.utils.ArrayBufferView; #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end @:access(lime.media.AudioBuffer) -class NativeAudioSource -{ +@:access(lime.utils.ArrayBufferView) +@:access(lime.media.vorbis.VorbisFile) +class NativeAudioSource { public static var initBuffers:Array = []; - private static var STREAM_BUFFER_SIZE = 48000; - #if (native_audio_buffers && !macro) - private static var STREAM_NUM_BUFFERS = Std.parseInt(haxe.macro.Compiler.getDefine("native_audio_buffers")); - #else - private static var STREAM_NUM_BUFFERS = 3; - #end - private static var STREAM_TIMER_FREQUENCY = 100; + private static final STREAM_BUFFER_SIZE:Int = 0x8000; + private static final STREAM_NUM_BUFFERS:Int = 5; + private static final STREAM_TIMER_FREQUENCY:Int = 100; + + //#if (lime >= "8.3.0") + //private static var hasALSoftLatencyExt:Null; + //#end private var buffers:Array; + private var bufferDatas:Array; private var bufferTimeBlocks:Array; - private var completed:Bool; - private var dataLength:Int; - private var format:Int; - private var handle:ALSource; + private var bufferLoops:Int; + private var queuedBuffers:Int; + private var requestBuffers:Int; + private var length:Null; - private var loops:Int; - private var parent:AudioSource; + private var loopTime:Null; private var playing:Bool; + private var loops:Int; private var position:Vector4; - private var samples:Float; + + private var bitsPerSample:Int; + private var format:Int; + private var dataLength:Int64; + private var samples:Int64; + private var completed:Bool; private var stream:Bool; - private var streamTimer:Timer; + + private var handle:ALSource; + private var parent:AudioSource; private var timer:Timer; + private var streamTimer:Timer; + private var disposed:Bool; + private var safeEnd:Bool; - private var pcmTotal:Int64; - private var sampleRate:Int; - - var disposed:Bool = false; - - public function new(parent:AudioSource) - { + public function new(parent:AudioSource) { this.parent = parent; - position = new Vector4(); } - public function dispose():Void - { - if (handle != null) - { - stop(); + public function dispose():Void { + disposed = true; + stop(); + + if (handle != null) { AL.sourcei(handle, AL.BUFFER, null); AL.deleteSource(handle); - if (buffers != null) - { - for (buffer in buffers) - { - AL.deleteBuffer(buffer); - } - buffers = null; - } - handle = null; } - disposed = true; + handle = null; + + if (buffers != null) AL.deleteBuffers(buffers); + buffers = null; + initBuffers.remove(this); } - public function init():Void - { - dataLength = 0; - format = 0; + public function init():Void { + //#if (lime >= "8.3.0") + //if (hasALSoftLatencyExt == null) hasALSoftLatencyExt = AL.isExtensionPresent("AL_SOFT_source_latency"); + //#end - if (parent.buffer.channels == 1) - { - if (parent.buffer.bitsPerSample == 8) - { - format = AL.FORMAT_MONO8; - } - else if (parent.buffer.bitsPerSample == 16) - { - format = AL.FORMAT_MONO16; - } - } - else if (parent.buffer.channels == 2) - { - if (parent.buffer.bitsPerSample == 8) - { - format = AL.FORMAT_STEREO8; - } - else if (parent.buffer.bitsPerSample == 16) - { - format = AL.FORMAT_STEREO16; - } - } + if (handle != null) return; - if (parent.buffer.__srcVorbisFile != null) - { - stream = true; + var buffer = parent.buffer; + if (disposed = (handle = AL.createSource()) == null) return; - var vorbisFile = parent.buffer.__srcVorbisFile; - pcmTotal = vorbisFile.pcmTotal(); - var info = vorbisFile.info(); - sampleRate = info.rate; - var _dataLength = pcmTotal * Int64.ofInt(parent.buffer.channels) * (Int64.ofInt(parent.buffer.bitsPerSample) / Int64.ofInt(8)); - dataLength = Int64.toInt(_dataLength); + AL.sourcef(handle, AL.MAX_GAIN, 10); + bufferLoops = 0; - buffers = new Array(); - bufferTimeBlocks = new Array(); + bitsPerSample = buffer.bitsPerSample; + if (buffer.channels > 1) format = bitsPerSample == 8 ? AL.FORMAT_STEREO8 : AL.FORMAT_STEREO16; + else format = bitsPerSample == 8 ? AL.FORMAT_MONO8 : AL.FORMAT_MONO16; - for (i in 0...STREAM_NUM_BUFFERS) - { + var vorbisFile = buffer.__srcVorbisFile; + if (stream = vorbisFile != null) { + dataLength = (samples = vorbisFile.pcmTotal()) * buffer.channels * (Int64.ofInt(bitsPerSample) / 8); + buffers = []; + bufferDatas = []; + bufferTimeBlocks = []; + + var constructor = bitsPerSample == 8 ? Int8 : Int16; + for (i in 0...STREAM_NUM_BUFFERS) { buffers.push(AL.createBuffer()); + bufferDatas.push(new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)); bufferTimeBlocks.push(0); } - - handle = AL.createSource(); - - samples = Int64.toInt(pcmTotal); } - else - { - if (parent.buffer.__srcBuffer == null) - { - parent.buffer.__srcBuffer = AL.createBuffer(); + else { + samples = ((dataLength = buffer.data.length) * 8) / (buffer.channels * bitsPerSample); - if (parent.buffer.__srcBuffer != null) - { - AL.bufferData(parent.buffer.__srcBuffer, format, parent.buffer.data, parent.buffer.data.length, parent.buffer.sampleRate); - } - } + if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null) + AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate); - dataLength = parent.buffer.data.length; - - handle = AL.createSource(); - - if (handle != null) - { - AL.sourcei(handle, AL.BUFFER, parent.buffer.__srcBuffer); - } - - samples = Int64.toInt((Int64.make(0, dataLength) * Int64.ofInt(8)) / Int64.ofInt(parent.buffer.channels * parent.buffer.bitsPerSample)); + AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer); } initBuffers.push(this); + + if (dataLength == 0) { + trace('NativeAudioSource Bug! dataLength is 0'); + dispose(); + } } - public function play():Void - { - if (disposed) { - trace("TRIED TO PLAY DISPOSED?? WTFFF"); - trace(haxe.CallStack.toString(haxe.CallStack.callStack())); - } - if (playing || handle == null) - { - return; - } + public function play():Void { + if (playing || disposed) return; playing = true; - - if (stream) - { - setCurrentTime(getCurrentTime()); - - streamTimer = new Timer(STREAM_TIMER_FREQUENCY); - streamTimer.run = streamTimer_onRun; - } - else - { - var time = completed ? 0 : getCurrentTime(); - - setCurrentTime(time); - } + if (completed) setCurrentTime(0); + else setCurrentTime(getCurrentTime()); } - public function pause():Void - { + public function pause():Void { + if (!(disposed = handle == null)) AL.sourcePause(handle); + playing = false; - - if (handle == null) return; - AL.sourcePause(handle); - - if (streamTimer != null) - { - streamTimer.stop(); - } - - if (timer != null) - { - timer.stop(); - } + stopStreamTimer(); + stopTimer(); } - private function readVorbisFileBuffer(vorbisFile:VorbisFile, length:Int):UInt8Array - { - #if lime_vorbis - var buffer = new UInt8Array(length); - var read = 0, total = 0, readMax; - - for (i in 0...STREAM_NUM_BUFFERS-1) - { - bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; + public function stop():Void { + if (!(disposed = handle == null)) { + if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.STOPPED) AL.sourceStop(handle); + if (stream) AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED) + AL.getSourcei(handle, AL.BUFFERS_PROCESSED)); } - bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell(); - while (total < length) - { - readMax = 4096; + requestBuffers = queuedBuffers = bufferLoops = 0; + playing = false; + stopStreamTimer(); + stopTimer(); + } - if (readMax > length - total) - { - readMax = length - total; + private function complete():Void { + stop(); + + completed = true; + parent.onComplete.dispatch(); + } + + private function readVorbisFileBuffer(vorbisFile:VorbisFile, max:Int):ArrayBufferView { + #if lime_vorbis + var id = STREAM_NUM_BUFFERS - requestBuffers, read = STREAM_NUM_BUFFERS - 1, total = 0, readMax; + var buffer = bufferDatas[id]; + + for (i in id...read) { + bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; + bufferDatas[i] = bufferDatas[i + 1]; + } + bufferTimeBlocks[read] = vorbisFile.timeTell(); + bufferDatas[read] = buffer; + queuedBuffers = requestBuffers; + + while (total < STREAM_BUFFER_SIZE) { + if ((readMax = 4096) > (read = max - total)) readMax = read; + if (vorbisFile.handle == null) break; + if (readMax > 0 && (read = vorbisFile.read(buffer.buffer, total, readMax)) > 0) total += read; + else if (safeEnd = (loops > bufferLoops)) { + if (readMax == 4096) continue; + bufferLoops++; vorbisFile.timeSeek((loopTime != null ? Math.max(0, loopTime / 1000) : 0) + parent.offset / 1000); + if ((max = (dataLength - (vorbisFile.pcmTell() * (Int64.ofInt(bitsPerSample) / 8) * parent.buffer.channels)).low) > STREAM_BUFFER_SIZE) + max = STREAM_BUFFER_SIZE; } - - read = vorbisFile.read(buffer.buffer, total, readMax); - - if (read > 0) - { - total += read; - } - else - { + else { + buffer.buffer.fill(total, STREAM_BUFFER_SIZE - total - 1, 0); + resetTimer((getLength() - getCurrentTime()) / getPitch()); break; } } @@ -241,344 +198,281 @@ class NativeAudioSource #end } - private function refillBuffers(buffers:Array = null):Void - { + private function fillBuffers(buffers:Array):Void { #if lime_vorbis - var vorbisFile = null; - var position = 0; + if (parent == null || parent.buffer == null) return dispose(); + if (handle == null || buffers.length < 1) return; - if (buffers == null) - { - var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + final buffer = parent.buffer, vorbisFile = buffer.__srcVorbisFile; + final actualDataRate = (Int64.ofInt(bitsPerSample) / 8) * buffer.channels; + var position = vorbisFile.pcmTell() * actualDataRate, length = getLengthSamples() * actualDataRate, trackLoops = bufferLoops; + if (position >= length && safeEnd) return; - if (buffersProcessed > 0) - { - vorbisFile = parent.buffer.__srcVorbisFile; - position = Int64.toInt(vorbisFile.pcmTell()); + var sampleRate = buffer.sampleRate, numBuffers = 0, data, size:Int64; + for (buffer in buffers) { + if ((size = length - position) > STREAM_BUFFER_SIZE) size = Int64.ofInt(STREAM_BUFFER_SIZE); + data = readVorbisFileBuffer(vorbisFile, Int64.toInt(size)); - if (position < dataLength) - { - buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed); - } + if (disposed) return; + AL.bufferData(buffer, format, data, STREAM_BUFFER_SIZE, sampleRate); + numBuffers++; + + if (safeEnd) break; + else if (bufferLoops != trackLoops) { + position = vorbisFile.pcmTell() * actualDataRate; + trackLoops = bufferLoops; } } + AL.sourceQueueBuffers(handle, numBuffers, buffers); - if (buffers != null) - { - if (vorbisFile == null) - { - vorbisFile = parent.buffer.__srcVorbisFile; - position = Int64.toInt(vorbisFile.pcmTell()); - } - - var numBuffers = 0; - var data; - - for (buffer in buffers) - { - if (dataLength - position >= STREAM_BUFFER_SIZE) - { - data = readVorbisFileBuffer(vorbisFile, STREAM_BUFFER_SIZE); - AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); - position += STREAM_BUFFER_SIZE; - numBuffers++; - } - else if (position < dataLength) - { - data = readVorbisFileBuffer(vorbisFile, dataLength - position); - AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); - numBuffers++; - break; - } - } - - AL.sourceQueueBuffers(handle, numBuffers, buffers); - - // OpenAL can unexpectedly stop playback if the buffers run out - // of data, which typically happens if an operation (such as - // resizing a window) freezes the main thread. - // If AL is supposed to be playing but isn't, restart it here. - if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) - { - AL.sourcePlay(handle); + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + if (bufferLoops > 0) { // TODO: remove this once buffer loop stutters fixed + setCurrentTime(getCurrentTime()); + return; } + AL.sourcePlay(handle); + resetTimer(Std.int((getLength() - getCurrentTime()) / getPitch())); } #end } - public function stop():Void - { - if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) - { - AL.sourceStop(handle); + private function streamRun():Void { + #if lime_vorbis + if (disposed = (handle == null || buffers == null)) return dispose(); + + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile == null) return dispose(); + + try { + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + if (processed > 0 && playing) { + fillBuffers(AL.sourceUnqueueBuffers(handle, processed)); + if ((!safeEnd || loops > 0) && queuedBuffers < STREAM_NUM_BUFFERS && playing) { + /*if (requestBuffers >= STREAM_NUM_BUFFERS) { + AL.sourceUnqueueBuffers(handle, queuedBuffers); + setCurrentTime((bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers] + + (STREAM_BUFFER_SIZE / ((bitsPerSample / 8) * parent.buffer.channels) / parent.buffer.sampleRate) + ) * 1000 - parent.offset); + } + else*/fillBuffers([buffers[++requestBuffers - 1]]); + } + } } - - playing = false; - - if (streamTimer != null) - { - streamTimer.stop(); - } - - if (timer != null) - { - timer.stop(); - } - - setCurrentTime(0); + catch(e) trace(e); + #end } - // Event Handlers - private function streamTimer_onRun():Void - { - refillBuffers(); + // Timers + inline function stopStreamTimer():Void if (streamTimer != null) streamTimer.stop(); + + private function resetStreamTimer():Void { + stopStreamTimer(); + + #if lime_vorbis + streamTimer = new Timer(STREAM_TIMER_FREQUENCY); + streamTimer.run = streamRun; + #end } - private function timer_onRun():Void - { - if (loops > 0) - { - playing = false; - loops--; - setCurrentTime(0); - play(); + inline function stopTimer():Void if (timer != null) timer.stop(); + + private function resetTimer(timeRemaining:Float):Void { + stopTimer(); + + /*if (timeRemaining <= 26) { + timer_onRun(); + return; + }*/ + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + + private function timer_onRun():Void { + if (!safeEnd && bufferLoops <= 0) { + #if lime_vorbis + var ranOut = false; + if (stream) { + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile == null) return dispose(); + ranOut = vorbisFile.pcmTell() >= getLengthSamples() || queuedBuffers < 3; + } + + if (!ranOut) + #end + { + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); + if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { + resetTimer(timeRemaining); + return; + } + } + } + safeEnd = false; + + if (loops <= 0) { + complete(); return; } - else - { - stop(); - } - completed = true; - parent.onComplete.dispatch(); + if (bufferLoops <= 0) { + loops--; + var loopTime = loopTime ?? 0; + if (stream || loopTime > 0) setCurrentTime(loopTime); + else if (!stream) { + setLoops(loops); + if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) + setCurrentTime(0); + } + } + else { + loops -= bufferLoops; + bufferLoops = 0; + } + parent.onLoop.dispatch(); } // Get & Set Methods - public function getCurrentTime():Float - { - if (completed) - { - return getLength(); - } - else if (handle != null) - { - if (stream) - { - var time = (Std.int(bufferTimeBlocks[0] * 1000) + Std.int(AL.getSourcef(handle, AL.SEC_OFFSET) * 1000)) - parent.offset; - if (time < 0) return 0; - return time; - } - else - { - var offset:Int = AL.getSourcei(handle, AL.BYTE_OFFSET); - var ratio = (offset / dataLength); - var totalSeconds = samples / parent.buffer.sampleRate; - - var time = totalSeconds * ratio * 1000 - parent.offset; - - // var time = Std.int (AL.getSourcef (handle, AL.SEC_OFFSET) * 1000) - parent.offset; - if (time < 0) return 0; - return time; + public function getCurrentTime():Float { + if (completed) return getLength(); + else if (!disposed) { + var time = AL.getSourcef(handle, AL.SEC_OFFSET); + if (stream) time += bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers]; + time = time * 1000 - parent.offset; + if (loops > 0 && time > getLength()) { + var start = loopTime != null ? Math.max(0, loopTime + parent.offset) : parent.offset; + return ((time - start) % (getLength() - start)) + start; } + if (time > 0) return time; } - return 0; } - public function setCurrentTime(value:Float):Float - { - // `setCurrentTime()` has side effects and is never safe to skip. - /* if (value == getCurrentTime()) - { - return value; - } */ + public function setCurrentTime(value:Float):Float { + if (disposed = (handle == null)) return value; - if (handle != null) - { - if (stream) - { - AL.sourceStop(handle); + var total = getRealLength(); + var time = Math.max(0, Math.min(total, value + parent.offset)), ratio = time / total; - parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000); - AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS); - refillBuffers(buffers); + if (stream) { + // TODO: smooth setCurrentTime for stream (dont refill buffers again) - if (playing) AL.sourcePlay(handle); - } - else if (parent.buffer != null) - { - AL.sourceRewind(handle); - - // AL.sourcef (handle, AL.SEC_OFFSET, (value + parent.offset) / 1000); - - var secondOffset = (value + parent.offset) / 1000; - var totalSeconds = samples / parent.buffer.sampleRate; - - if (secondOffset < 0) secondOffset = 0; - else if (secondOffset > totalSeconds) secondOffset = totalSeconds; - - var ratio = (secondOffset / totalSeconds); - var totalOffset = Std.int(dataLength * ratio); - - AL.sourcei(handle, AL.BYTE_OFFSET, totalOffset); - if (playing) AL.sourcePlay(handle); + AL.sourceStop(handle); + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + + #if lime_vorbis + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile != null) { + // var chunk = Std.int(Math.floor(getFloat(samples) * ratio / STREAM_BUFFER_SIZE) * STREAM_BUFFER_SIZE); + vorbisFile.pcmSeek(Int64.fromFloat(getFloat(samples) * ratio)); + fillBuffers(buffers.slice(0, requestBuffers = queuedBuffers = 3)); + // AL.sourcei(handle, AL.SAMPLE_OFFSET, Std.int((samples * ratio) - chunk)); + if (playing) resetStreamTimer(); } + #end + } + else { + AL.sourceRewind(handle); + AL.sourcei(handle, AL.BYTE_OFFSET, Int64.fromFloat(getFloat(dataLength) * ratio)); } - if (playing) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (getLength() - value) / getPitch(); - - if (timeRemaining > 0) - { - completed = false; - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } - else - { - playing = false; - completed = true; + if (playing) { + var timeRemaining = (getLength() - time) / getPitch(); + if (completed = timeRemaining <= 0) complete(); + else { + AL.sourcePlay(handle); + resetTimer(timeRemaining); } } return value; } - public function getGain():Float - { - if (handle != null) - { - return AL.getSourcef(handle, AL.GAIN); - } - else - { - return 1; - } + inline private function getLengthSamples():Int64 { + return if (length == null) samples; + else Int64.fromFloat((length + parent.offset) / 1000 * parent.buffer.sampleRate); + } + + inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >>> 0); + + inline private function getRealLength():Float return getFloat(samples) / parent.buffer.sampleRate * 1000; + + public function getLength():Null { + return if (length == null) getRealLength() - parent.offset; + else length - parent.offset; } - public function setGain(value:Float):Float - { - if (handle != null) - { - AL.sourcef(handle, AL.GAIN, value); - } + public function setLength(value:Null):Null { + if (value == length) return value; + length = value; + if (playing) { + var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); + if (timeRemaining > 0) resetTimer(timeRemaining); + } return value; } - public function getLength():Float - { - if (length != null) - { - return length; + public function getPitch():Float { + return if (disposed) 1; + else AL.getSourcef(handle, AL.PITCH); + } + + public function setPitch(value:Float):Float { + if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value; + if (playing) { + var timeRemaining = (getLength() - getCurrentTime()) / value; + if (timeRemaining > 0) resetTimer(timeRemaining); } - - return (samples / parent.buffer.sampleRate * 1000) - parent.offset; + AL.sourcef(handle, AL.PITCH, value); + return value; } - public function setLength(value:Float):Float - { - if (playing && length != value) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (value - getCurrentTime()) / getPitch(); - - if (timeRemaining > 0) - { - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } - } - - return length = value; + public function getGain():Float { + if (disposed) return 1; + return AL.getSourcef(handle, AL.GAIN); } - public function getLoops():Int - { - return loops; + public function setGain(value:Float):Float { + if (!disposed) AL.sourcef(handle, AL.GAIN, value); + return value; } - public function setLoops(value:Int):Int - { + inline public function getLoops():Int return loops; + + inline public function setLoops(value:Int):Int { + if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE); return loops = value; } - public function getPitch():Float - { - if (handle != null) - { - return AL.getSourcef(handle, AL.PITCH); - } - else - { - return 1; - } + inline public function getLoopTime():Float return loopTime; + + inline public function setLoopTime(value:Float):Float { + if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE); + return loopTime = value; } - public function setPitch(value:Float):Float - { - if (playing && value != getPitch()) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (getLength() - getCurrentTime()) / value; - - if (timeRemaining > 0) - { - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } + public function getLatency():Float { + /*#if (lime >= "8.3.0") + if (hasALSoftLatencyExt) { + final offsets = AL.getSourcedvSOFT(handle, AL.SEC_OFFSET_LATENCY_SOFT, 2); + if (offsets != null) return offsets[1] * 1000; } - - if (handle != null) - { - AL.sourcef(handle, AL.PITCH, value); - } - - return value; + #end*/ + return 0; } - public function getPosition():Vector4 - { - if (handle != null) - { - #if !emscripten - var value = AL.getSource3f(handle, AL.POSITION); - position.x = value[0]; - position.y = value[1]; - position.z = value[2]; - #end - } + public function getPosition():Vector4 return position; - return position; - } - - public function setPosition(value:Vector4):Vector4 - { + public function setPosition(value:Vector4):Vector4 { position.x = value.x; position.y = value.y; position.z = value.z; position.w = value.w; - if (handle != null) - { + if (!disposed) { AL.distanceModel(AL.NONE); AL.source3f(handle, AL.POSITION, position.x, position.y, position.z); } - return position; } -} +} \ No newline at end of file diff --git a/source/lime/media/AudioBuffer.hx b/source/lime/media/AudioBuffer.hx new file mode 100644 index 00000000..87215835 --- /dev/null +++ b/source/lime/media/AudioBuffer.hx @@ -0,0 +1,494 @@ +package lime.media; + +import haxe.io.Bytes; +import haxe.io.Path; +import lime._internal.backend.native.NativeCFFI; +import lime._internal.format.Base64; +import lime.app.Future; +import lime.app.Promise; +import lime.media.openal.AL; +import lime.media.openal.ALBuffer; +import lime.media.vorbis.VorbisFile; +import lime.net.HTTPRequest; +import lime.utils.Log; +import lime.utils.UInt8Array; +#if lime_howlerjs +import lime.media.howlerjs.Howl; +#end +#if (js && html5) +import js.html.Audio; +#elseif flash +import flash.media.Sound; +import flash.net.URLRequest; +#end + +@:access(lime._internal.backend.native.NativeCFFI) +@:access(lime.utils.Assets) +#if hl +@:keep +#end +#if !lime_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end + +/** + The `AudioBuffer` class represents a buffer of audio data that can be played back using an `AudioSource`. + It supports a variety of audio formats and platforms, providing a consistent API for loading and managing audio data. + + Depending on the platform, the audio backend may differ, but the class provides a unified interface for accessing + audio data, whether it's stored in memory, loaded from a file, or streamed. + + @see lime.media.AudioSource +**/ +class AudioBuffer +{ + /** + The number of bits per sample in the audio data. + **/ + public var bitsPerSample:Int; + + /** + The number of audio channels (e.g., 1 for mono, 2 for stereo). + **/ + public var channels:Int; + + /** + The raw audio data stored as a `UInt8Array`. + **/ + public var data:UInt8Array; + + /** + The sample rate of the audio data, in Hz. + **/ + public var sampleRate:Int; + + /** + The source of the audio data. This can be an `Audio`, `Sound`, `Howl`, or other platform-specific object. + **/ + public var src(get, set):Dynamic; + + @:noCompletion private var __srcAudio:#if (js && html5) Audio #else Dynamic #end; + @:noCompletion private var __srcBuffer:#if lime_cffi ALBuffer #else Dynamic #end; + @:noCompletion private var __srcCustom:Dynamic; + @:noCompletion private var __srcHowl:#if lime_howlerjs Howl #else Dynamic #end; + @:noCompletion private var __srcSound:#if flash Sound #else Dynamic #end; + @:noCompletion private var __srcVorbisFile:#if lime_vorbis VorbisFile #else Dynamic #end; + + #if commonjs + private static function __init__() + { + var p = untyped AudioBuffer.prototype; + untyped Object.defineProperties(p, + { + "src": {get: p.get_src, set: p.set_src} + }); + } + #end + + /** + Creates a new, empty `AudioBuffer` instance. + **/ + public function new() {} + + /** + Disposes of the resources used by this `AudioBuffer`, such as unloading any associated audio data. + **/ + public function dispose():Void + { + #if (js && html5 && lime_howlerjs) + if (__srcHowl != null) __srcHowl.unload(); + __srcHowl = null; + #end + #if lime_cffi + if (__srcBuffer != null) AL.deleteBuffer(__srcBuffer); + __srcBuffer = null; + #end + #if lime_vorbis + if (__srcVorbisFile != null) { + __srcVorbisFile.clear(); + @:privateAccess __srcVorbisFile.handle = null; + } + __srcVorbisFile = null; + #end + } + + /** + Creates an `AudioBuffer` from a Base64-encoded string. + + @param base64String The Base64-encoded audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + public static function fromBase64(base64String:String):AudioBuffer + { + if (base64String == null) return null; + + #if (js && html5 && lime_howlerjs) + // if base64String doesn't contain codec data, add it. + if (base64String.indexOf(",") == -1) + { + base64String = "data:" + __getCodec(Base64.decode(base64String)) + ";base64," + base64String; + } + + var audioBuffer = new AudioBuffer(); + audioBuffer.src = new Howl({src: [base64String], html5: true, preload: false}); + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + // if base64String contains codec data, strip it then decode it. + var base64StringSplit = base64String.split(","); + var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1]; + var bytes:Bytes = Base64.decode(base64StringNoEncoding); + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer); + #else + // if base64String contains codec data, strip it then decode it. + var base64StringSplit = base64String.split(","); + var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1]; + var bytes:Bytes = Base64.decode(base64StringNoEncoding); + var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + return audioBuffer; + } + #end + #end + + return null; + } + + /** + Creates an `AudioBuffer` from a `Bytes` object. + + @param bytes The `Bytes` object containing the audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + public static function fromBytes(bytes:Bytes):AudioBuffer + { + if (bytes == null) return null; + + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], html5: true, preload: false}); + + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer); + #else + var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + return audioBuffer; + } + #end + #end + + return null; + } + + /** + Creates an `AudioBuffer` from a file. + + @param path The file path to the audio data. + @return An `AudioBuffer` instance with the audio data loaded from the file. + **/ + public static function fromFile(path:String #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer + { + if (path == null) return null; + + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + + #if force_html5_audio + audioBuffer.__srcHowl = new Howl({src: [path], html5: true, preload: false}); + #else + audioBuffer.__srcHowl = new Howl({src: [path], html5: howlHtml5, preload: false}); + #end + + return audioBuffer; + #elseif flash + switch (Path.extension(path)) + { + case "ogg", "wav": + return null; + default: + } + + var audioBuffer = new AudioBuffer(); + audioBuffer.__srcSound = new Sound(new URLRequest(path)); + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + //audioBuffer = NativeCFFI.lime_audio_load_file(path, audioBuffer); + //if (audioBuffer != null) audioBuffer.initBuffer(); + //return audioBuffer; + return NativeCFFI.lime_audio_load_file(path, audioBuffer); + #else + var data:Dynamic = NativeCFFI.lime_audio_load_file(path, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + //audioBuffer.initBuffer(); + return audioBuffer; + } + + return null; + #end + #else + return null; + #end + } + + /** + Creates an `AudioBuffer` from an array of file paths. + + @param paths An array of file paths to search for audio data. + @return An `AudioBuffer` instance with the audio data loaded from the first valid file found. + **/ + public static function fromFiles(paths:Array #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer + { + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + + #if force_html5_audio + audioBuffer.__srcHowl = new Howl({src: paths, html5: true, preload: false}); + #else + audioBuffer.__srcHowl = new Howl({src: paths, html5: howlHtml5, preload: false}); + #end + + return audioBuffer; + #else + var buffer = null; + + for (path in paths) + { + buffer = AudioBuffer.fromFile(path); + if (buffer != null) break; + } + + return buffer; + #end + } + + /** + Creates an `AudioBuffer` from a `VorbisFile`. + + @param vorbisFile The `VorbisFile` object containing the audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + #if lime_vorbis + + public static function fromVorbisFile(vorbisFile:VorbisFile):AudioBuffer + { + if (vorbisFile == null) return null; + + var info = vorbisFile.info(); + + var audioBuffer = new AudioBuffer(); + audioBuffer.channels = info.channels; + audioBuffer.sampleRate = info.rate; + audioBuffer.bitsPerSample = 16; + audioBuffer.__srcVorbisFile = vorbisFile; + + return audioBuffer; + } + #else + public static function fromVorbisFile(vorbisFile:Dynamic):AudioBuffer + { + return null; + } + #end + + /** + Asynchronously loads an `AudioBuffer` from a file. + + @param path The file path to the audio data. + @return A `Future` that resolves to the loaded `AudioBuffer`. + **/ + public static function loadFromFile(path:String):Future + { + #if (flash || (js && html5)) + var promise = new Promise(); + + var audioBuffer = AudioBuffer.fromFile(path); + + if (audioBuffer != null) + { + #if flash + audioBuffer.__srcSound.addEventListener(flash.events.Event.COMPLETE, function(event) + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcSound.addEventListener(flash.events.ProgressEvent.PROGRESS, function(event) + { + promise.progress(Std.int(event.bytesLoaded), Std.int(event.bytesTotal)); + }); + + audioBuffer.__srcSound.addEventListener(flash.events.IOErrorEvent.IO_ERROR, promise.error); + #elseif (js && html5 && lime_howlerjs) + if (audioBuffer != null) + { + audioBuffer.__srcHowl.on("load", function() + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcHowl.on("loaderror", function(id, msg) + { + promise.error(msg); + }); + + audioBuffer.__srcHowl.load(); + } + #else + promise.complete(audioBuffer); + #end + } + else + { + promise.error(null); + } + + return promise.future; + #else + // TODO: Streaming + + var request = new HTTPRequest(); + return request.load(path).then(function(buffer) + { + if (buffer != null) + { + return Future.withValue(buffer); + } + else + { + return cast Future.withError(""); + } + }); + #end + } + + /** + Asynchronously loads an `AudioBuffer` from multiple files. + + @param paths An array of file paths to search for audio data. + @return A `Future` that resolves to the loaded `AudioBuffer`. + **/ + public static function loadFromFiles(paths:Array):Future + { + #if (js && html5 && lime_howlerjs) + var promise = new Promise(); + + var audioBuffer = AudioBuffer.fromFiles(paths); + + if (audioBuffer != null) + { + audioBuffer.__srcHowl.on("load", function() + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcHowl.on("loaderror", function() + { + promise.error(null); + }); + + audioBuffer.__srcHowl.load(); + } + else + { + promise.error(null); + } + + return promise.future; + #else + return new Future(fromFiles.bind(paths), true); + #end + } + + private static function __getCodec(bytes:Bytes):String + { + var signature = bytes.getString(0, 4); + + switch (signature) + { + case "OggS": + return "audio/ogg"; + case "fLaC": + return "audio/flac"; + case "RIFF" if (bytes.getString(8, 4) == "WAVE"): + return "audio/wav"; + default: + switch ([bytes.get(0), bytes.get(1), bytes.get(2)]) + { + case [73, 68, 51] | [255, 251, _] | [255, 250, _] | [255, 243, _]: return "audio/mp3"; + default: + } + } + + Log.error("Unsupported sound format"); + return null; + } + + // Get & Set Methods + @:noCompletion private function get_src():Dynamic + { + #if (js && html5) + #if lime_howlerjs + return __srcHowl; + #else + return __srcAudio; + #end + #elseif flash + return __srcSound; + #elseif lime_vorbis + return __srcVorbisFile; + #else + return __srcCustom; + #end + } + + @:noCompletion private function set_src(value:Dynamic):Dynamic + { + #if (js && html5) + #if lime_howlerjs + return __srcHowl = value; + #else + return __srcAudio = value; + #end + #elseif flash + return __srcSound = value; + #elseif lime_vorbis + return __srcVorbisFile = value; + #else + return __srcCustom = value; + #end + } +} diff --git a/source/lime/media/AudioSource.hx b/source/lime/media/AudioSource.hx index bc5948df..5ee8251a 100644 --- a/source/lime/media/AudioSource.hx +++ b/source/lime/media/AudioSource.hx @@ -1,29 +1,98 @@ package lime.media; import lime.app.Event; -import lime.media.openal.AL; -import lime.media.openal.ALSource; import lime.math.Vector4; #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end +/** + The `AudioSource` class provides a way to control audio playback in a Lime application. + It allows for playing, pausing, and stopping audio, as well as controlling various + audio properties such as gain, pitch, and looping. + + Depending on the platform, the audio backend may vary, but the API remains consistent. + + @see lime.media.AudioBuffer +**/ class AudioSource { + /** + An event that is dispatched when the audio playback is complete. + **/ public var onComplete = new EventVoid>(); + + /** + An event that is dispatched when the audio playback looped. + **/ + public var onLoop = new EventVoid>(); + + /** + The `AudioBuffer` associated with this `AudioSource`. + **/ public var buffer:AudioBuffer; + + /** + An property if this 'AudioSource' is playing. + **/ + public var playing(get, null):Bool; + + /** + The current playback position of the audio, in milliseconds. + **/ public var currentTime(get, set):Float; + + /** + The gain (volume) of the audio. A value of `1.0` represents the default volume. + **/ public var gain(get, set):Float; - public var length(get, set):Float; + + /** + The length of the audio, in milliseconds. + **/ + public var length(get, set):Null; + + /** + The number of times the audio will loop. A value of `0` means the audio will not loop. + **/ public var loops(get, set):Int; + + /** + In which audio playback time the audio will loop. + **/ + public var loopTime(get, set):Float; + + /** + The pitch of the audio. A value of `1.0` represents the default pitch. + **/ public var pitch(get, set):Float; - public var offset:Int; + + /** + The offset within the audio buffer to start playback, in samples. + **/ + public var offset:Float; + + /** + The 3D position of the audio source, represented as a `Vector4`. + **/ public var position(get, set):Vector4; + /** + The latency of the audio source. + **/ + public var latency(get, never):Float; + @:noCompletion private var __backend:AudioSourceBackend; - public function new(buffer:AudioBuffer = null, offset:Int = 0, length:Null = null, loops:Int = 0) + /** + Creates a new `AudioSource` instance. + @param buffer The `AudioBuffer` to associate with this `AudioSource`. + @param offset The starting offset within the audio buffer, in samples. + @param length The length of the audio to play, in milliseconds. If `null`, the full buffer is used. + @param loops The number of times to loop the audio. `0` means no looping. + **/ + public function new(buffer:AudioBuffer = null, offset:Float = 0, length:Null = null, loops:Int = 0) { this.buffer = buffer; this.offset = offset; @@ -35,104 +104,134 @@ class AudioSource this.length = length; } - this.loops = loops; - if (buffer != null) { init(); } + + this.loops = loops; } - public function dispose():Void + /** + Releases any resources used by this `AudioSource`. + **/ + inline public function dispose():Void { __backend.dispose(); } - @:noCompletion private function init():Void + @:noCompletion inline private function init():Void { __backend.init(); } - public function play():Void + /** + Starts or resumes audio playback. + **/ + inline public function play():Void { __backend.play(); } - public function pause():Void + /** + Pauses audio playback. + **/ + inline public function pause():Void { __backend.pause(); } - public function stop():Void + /** + Stops audio playback and resets the playback position to the beginning. + **/ + inline public function stop():Void { __backend.stop(); } // Get & Set Methods - @:noCompletion private function get_currentTime():Float + @:noCompletion inline private function get_playing():Bool + { + @:privateAccess return __backend.playing; + } + + @:noCompletion inline private function get_currentTime():Float { return __backend.getCurrentTime(); } - @:noCompletion private function set_currentTime(value:Float):Float + @:noCompletion inline private function set_currentTime(value:Float):Float { return __backend.setCurrentTime(value); } - @:noCompletion private function get_gain():Float + @:noCompletion inline private function get_gain():Float { return __backend.getGain(); } - @:noCompletion private function set_gain(value:Float):Float + @:noCompletion inline private function set_gain(value:Float):Float { return __backend.setGain(value); } - @:noCompletion private function get_length():Float + @:noCompletion inline private function get_length():Null { return __backend.getLength(); } - @:noCompletion private function set_length(value:Float):Float + @:noCompletion inline private function set_length(value:Null):Null { return __backend.setLength(value); } - @:noCompletion private function get_loops():Int + @:noCompletion inline private function get_loops():Int { return __backend.getLoops(); } - @:noCompletion private function set_loops(value:Int):Int + @:noCompletion inline private function set_loops(value:Int):Int { return __backend.setLoops(value); } - @:noCompletion private function get_pitch():Float + @:noCompletion inline private function get_loopTime():Float + { + return __backend.getLoopTime(); + } + + @:noCompletion inline private function set_loopTime(value:Float):Float + { + return __backend.setLoopTime(value); + } + + @:noCompletion inline private function get_pitch():Float { return __backend.getPitch(); } - @:noCompletion private function set_pitch(value:Float):Float + @:noCompletion inline private function set_pitch(value:Float):Float { return __backend.setPitch(value); } - @:noCompletion private function get_position():Vector4 + @:noCompletion inline private function get_position():Vector4 { return __backend.getPosition(); } - @:noCompletion private function set_position(value:Vector4):Vector4 + @:noCompletion inline private function set_position(value:Vector4):Vector4 { return __backend.setPosition(value); } + + @:noCompletion inline private function get_latency():Float + { + return __backend.getLatency(); + } } -#if flash -@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.flash.FlashAudioSource; -#elseif (js && html5) +#if (js && html5) @:noCompletion private typedef AudioSourceBackend = lime._internal.backend.html5.HTML5AudioSource; #else @:noCompletion private typedef AudioSourceBackend = lime._internal.backend.native.NativeAudioSource; diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 0838f391..469759e9 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -1,10 +1,15 @@ package openfl.media; #if !flash +import haxe.Int64; + import openfl.events.Event; import openfl.events.EventDispatcher; #if lime import lime.media.AudioSource; + +#if lime_cffi import lime._internal.backend.native.NativeAudioSource; #end +#if lime_vorbis import lime.media.openal.AL; #end #end /** @@ -16,11 +21,19 @@ import lime.media.AudioSource; object to the channel. @event soundComplete Dispatched when a sound has finished playing. + + @see [Playing sounds](https://books.openfl.org/openfl-developers-guide/working-with-sound/playing-sounds.html) + @see `openfl.media.Sound` **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug #end +#if (lime && lime_cffi) +@:access(lime._internal.backend.native.NativeAudioSource) +@:access(lime.media.AudioSource) +#end +@:access(openfl.media.Sound) @:access(openfl.media.SoundMixer) @:final @:keep class SoundChannel extends EventDispatcher { @@ -28,7 +41,13 @@ import lime.media.AudioSource; The current amplitude(volume) of the left channel, from 0(silent) to 1 (full amplitude). **/ - public var leftPeak(default, null):Float; + public var leftPeak(get, null):Float; + + /** + The current amplitude(volume) of the right channel, from 0(silent) to 1 + (full amplitude). + **/ + public var rightPeak(get, null):Float; /** When the sound is playing, the `position` property indicates in @@ -45,12 +64,6 @@ import lime.media.AudioSource; **/ public var position(get, set):Float; - /** - The current amplitude(volume) of the right channel, from 0(silent) to 1 - (full amplitude). - **/ - public var rightPeak(default, null):Float; - /** The SoundTransform object assigned to the sound channel. A SoundTransform object includes properties for setting volume, panning, left speaker @@ -58,13 +71,23 @@ import lime.media.AudioSource; **/ public var soundTransform(get, set):SoundTransform; + /** + self explanatory + */ + public var loopTime(get, set):Float; + public var endTime(get, set):Null; public var pitch(get, set):Float; + public var loops(get, set):Int; + @:noCompletion private var __sound:Sound; @:noCompletion private var __isValid:Bool; @:noCompletion private var __soundTransform:SoundTransform; + @:noCompletion private var __lastPeakTime:Float; + @:noCompletion private var __leftPeak:Float; + @:noCompletion private var __rightPeak:Float; #if lime @:noCompletion private var __source:AudioSource; - @:noCompletion private var __audioSource(get, set):AudioSource; + @:noCompletion private var __audioSource(get, set):AudioSource; // forward??? compatibility?? #end #if openfljs @@ -87,28 +110,10 @@ import lime.media.AudioSource; { super(this); - leftPeak = 1; - rightPeak = 1; + if (soundTransform != null) __soundTransform = soundTransform; + else __soundTransform = new SoundTransform(); - if (soundTransform != null) - { - __soundTransform = soundTransform; - } - else - { - __soundTransform = new SoundTransform(); - } - - #if lime - if (source != null) - { - __source = source; - __source.onComplete.add(source_onComplete); - __isValid = true; - - __source.play(); - } - #end + __initAudioSource(source); SoundMixer.__registerSoundChannel(this); } @@ -134,6 +139,7 @@ import lime.media.AudioSource; #if lime __source.onComplete.remove(source_onComplete); + __source.onLoop.remove(source_onLoop); __source.dispose(); __source = null; #end @@ -145,6 +151,105 @@ import lime.media.AudioSource; this.soundTransform = soundTransform; } + // hi i made these - raltyro + // took me 8 months to figure this out + @:noCompletion inline private static var scanSamples = 480; + + #if (lime_cffi && !macro) + @:noCompletion private function __checkUpdatePeaks(time:Float):Bool + { + if (Math.abs(__lastPeakTime - time) < Math.max(1, pitch * 8)) return false; + __lastPeakTime = time; + return true; + } + + #if lime_vorbis + @:noCompletion private function __updateVorbisPeaks():Void + { + if (__source.buffer == null || !__checkUpdatePeaks(position)) return; + + var index = AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET), bufferDatas = __source.__backend.bufferDatas; + var bufferi = NativeAudioSource.STREAM_NUM_BUFFERS - __source.__backend.queuedBuffers, bytes = bufferDatas[bufferi].buffer; + + var channels = __source.buffer.channels, todo = scanSamples, sample; + var lfilled = false, rfilled = channels < 2; + + while(todo > 0 && (!lfilled || !rfilled)) { + if (index >= NativeAudioSource.STREAM_BUFFER_SIZE) { + if ((bytes = bufferDatas[++bufferi].buffer) == null) break; + index = 0; + } + + if (!lfilled) { + if ((sample = bytes.getUInt16(index * channels * 2) / 65535) > .5) sample = -(sample - 1); + if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; + } + + if (!rfilled) { + if ((sample = bytes.getUInt16(index * channels * 2 + 2) / 65535) > .5) sample = -(sample - 1); + if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; + } + + index++; + todo--; + } + } + #end + #end + + @:noCompletion private function __updatePeaks():Void + { + __leftPeak = __rightPeak = 0; + + #if (lime_cffi && !macro) + if (!__isValid) return; + + #if lime_vorbis if (__source.__backend.stream) return __updateVorbisPeaks(); #end + + var buffer = __source.buffer; + if (buffer == null || buffer.data == null || !__checkUpdatePeaks(position)) return; + + var index = Int64.make(0, AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET)); + var bytes = buffer.data.buffer, length = __source.__backend.samples; + if (index >= length) return; + + var temp = index + scanSamples; + if (temp < length) length = temp; + + var channels = buffer.channels, sample; + var lfilled = false, rfilled = channels < 2; + + while(index < length && (!lfilled || !rfilled)) { + if (!lfilled) { + if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2))) / 65535) > .5) sample = -(sample - 1); + if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; + } + if (!rfilled) { + if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2) + 2)) / 65535) > .5) sample = -(sample - 1); + if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; + } + index++; + } + #end + } + + @:noCompletion private function __initAudioSource(source:#if lime AudioSource #else Dynamic #end):Void + { + #if lime + __source = source; + if (__source == null) + { + return; + } + + __source.onComplete.add(source_onComplete); + __source.onLoop.add(source_onLoop); + __isValid = true; + + __source.play(); + #end + } + // Get & Set Methods @:noCompletion private function get_position():Float { @@ -167,27 +272,6 @@ import lime.media.AudioSource; return value; } - @:noCompletion private function get_pitch():Float - { - if (!__isValid) return 1; - - #if lime - return __source.pitch; - #else - return 1; - #end - } - - @:noCompletion private function set_pitch(value:Float):Float - { - if (!__isValid) return 1; - - #if lime - __source.pitch = value; - #end - return value; - } - @:noCompletion private function get_soundTransform():SoundTransform { return __soundTransform.clone(); @@ -203,7 +287,7 @@ import lime.media.AudioSource; var pan = SoundMixer.__soundTransform.pan + __soundTransform.pan; if (pan < -1) pan = -1; - else if (pan > 1) pan = 1; + if (pan > 1) pan = 1; var volume = SoundMixer.__soundTransform.volume * __soundTransform.volume; @@ -225,6 +309,107 @@ import lime.media.AudioSource; return value; } + @:noCompletion private function get_pitch():Float + { + if (!__isValid) return 1; + + #if lime + return __source.pitch; + #else + return 0; + #end + } + + @:noCompletion private function set_pitch(value:Float):Float + { + if (!__isValid) return 1; + + #if lime + return __source.pitch = value; + #else + return 0; + #end + } + + @:noCompletion private function get_loopTime():Float + { + if (!__isValid) return -1; + + #if lime + return __source.loopTime; + #else + return -1; + #end + } + + @:noCompletion private function set_loopTime(value:Float):Float + { + if (!__isValid) return -1; + + #if lime + return __source.loopTime = value; + #else + return -1; + #end + } + + @:noCompletion private function get_endTime():Null + { + if (!__isValid) return null; + + #if lime + return __source.length; + #else + return null; + #end + } + + @:noCompletion private function set_endTime(value:Null):Null + { + if (!__isValid) return null; + + #if lime + return __source.length = value; + #else + return null; + #end + } + + @:noCompletion private function get_loops():Int + { + if (!__isValid) return 0; + + #if lime + return __source.loops; + #else + return 0; + #end + } + + @:noCompletion private function set_loops(value:Int):Int + { + if (!__isValid) return 0; + + #if lime + return __source.loops = value; + #else + return 0; + #end + } + + @:noCompletion private function get_leftPeak():Float + { + __updatePeaks(); + return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + @:noCompletion private function get_rightPeak():Float + { + __updatePeaks(); + return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + // Event Handlers @:noCompletion private function source_onComplete():Void { @@ -234,14 +419,14 @@ import lime.media.AudioSource; dispatchEvent(new Event(Event.SOUND_COMPLETE)); } - function set___audioSource(value:AudioSource):AudioSource { - return __source = value; + @:noCompletion private function source_onLoop():Void + { + //dispatchEvent(new Event(Event.SOUND_LOOP)); } - function get___audioSource():AudioSource { - return __source; - } + @:noCompletion private function get___audioSource():AudioSource return __source; + @:noCompletion private function set___audioSource(source:AudioSource):AudioSource return __source = source; } #else typedef SoundChannel = flash.media.SoundChannel; -#end \ No newline at end of file +#end