diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx new file mode 100644 index 00000000..0716dc06 --- /dev/null +++ b/source/flixel/sound/FlxSound.hx @@ -0,0 +1,917 @@ +package flixel.sound; + +import lime.media.AudioBuffer; +import lime.media.AudioSource; +import lime.media.AudioManager; + +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. + * Seperates the looping from onComplete. + */ + 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; + + /** + * 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 _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; + _offset = _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 (destroySound) { + onFinish.removeAll(); + + if (group != null) group.remove(this); + + if (_channel != null) { + _channel.stop(); + _channel = null; + } + if (_source != null) { + _source.onComplete.remove(stopped); + _source.onLoop.remove(source_looped); + _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; + + 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 (_sound.__buffer == null) return; + 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(stopped); + _source.onLoop.add(source_looped); + _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. + */ + 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 #if lime_cffi || _source.__backend.disposed || _source.__backend.handle == null #end) + makeChannel(); + + if (_channel != null) { + #if FLX_PITCH + _timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0; + pitch = _pitch; + #end + + _channel.soundTransform = _transform; + _channel.__lastPeakTime = -10; + _channel.__leftPeak = 0; + _channel.__rightPeak = 0; + + #if lime_cffi _source.__backend.playing = true; #end + _source.offset = 0; + _source.currentTime = startTime + _offset; + #if !lime_cffi _source.play(); #end + + looped = looped; + loopTime = loopTime; + endTime = endTime; + + active = true; + } + else { + exists = false; + active = false; + } + } + + function stopped() { + onFinish.dispatch(); + + if (onComplete != null) onComplete(); + + if (looped) { + cleanup(false); + play(false, loopTime, endTime); + } + else cleanup(autoDestroy); + } + + function source_looped() { + if (onComplete != null) onComplete(); + + if (!looped) { + cleanup(autoDestroy); + _lastTime = FlxG.game.getTicks(); + _time = loopTime; + } + else _channel.loops = 999; + } + + /** + * 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(v:Float):Float { + _volume = FlxMath.bound(v, 0, 10); + updateTransform(); + return _volume; + } + + inline function set_muted(v:Bool):Bool { + muted = v; + updateTransform(); + return muted; + } + + inline function get_loaded():Bool + return buffer != null; + + inline function get_streamed():Bool + @:privateAccess return #if lime_vorbis _sound != null && _sound.__buffer.__srcVorbisFile != null #else false #end; + + inline function get_buffer():AudioBuffer + @:privateAccess return _sound != null ? _sound.__buffer : null; + + function update_amplitude():Void @:privateAccess { + if (_channel == null || !_channel.__updatePeaks(get_time()) || !_amplitudeUpdate) return; + + _amplitudeUpdate = false; + _amplitudeLeft = _channel.__leftPeak; + _amplitudeRight = _channel.__rightPeak; + } + + 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 Math.max(_amplitudeLeft, _amplitudeRight);//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 { + _realPitch = (_pitch = v) * _timeScaleAdjust; + if (_channel != null) _channel.pitch = _realPitch; + return _pitch; + } + #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 || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected /*CNE IMPLEMENTATION*/) 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, _offset, 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; + } + + inline function get_offset():Float return _offset; + inline function set_offset(offset:Float):Float { + if (_offset == (_offset = offset)) return offset; + //time = time + _offset; + 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..2f0621c8 100644 --- a/source/funkin/backend/system/Conductor.hx +++ b/source/funkin/backend/system/Conductor.hx @@ -159,16 +159,9 @@ 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; - return; - } - - if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) { - // update conductor - songPosition = lastSongPos; - } else { - songPosition += songOffset + elapsed * 1000; + if (FlxG.sound.music != null) { // CNE FlxSound is Interpolated. + lastSongPos = FlxG.sound.music.time - songOffset; + if (FlxG.sound.music.playing) songPosition = FlxG.sound.music.time; } } diff --git a/source/funkin/backend/system/modules/AudioSwitchFix.hx b/source/funkin/backend/system/modules/AudioSwitchFix.hx index 941846e0..25d904db 100644 --- a/source/funkin/backend/system/modules/AudioSwitchFix.hx +++ b/source/funkin/backend/system/modules/AudioSwitchFix.hx @@ -4,6 +4,8 @@ import flixel.FlxState; import flixel.sound.FlxSound; import funkin.backend.utils.NativeAPI; import lime.media.AudioManager; +import lime.media.AudioSource; +import haxe.Timer; /** * if you are stealing this keep this comment at least please lol @@ -12,58 +14,45 @@ import lime.media.AudioManager; */ @:dox(hide) class AudioSwitchFix { - @:noCompletion - private static function onStateSwitch(state:FlxState):Void { - #if windows - if (Main.audioDisconnected) { + public static function onAudioDisconnected() @:privateAccess { + var soundList:Array = [FlxG.sound.music]; + for (sound in FlxG.sound.list) if (sound.playing) { + sound.pause(); + soundList.push(sound); + } - var playingList:Array = []; - for(e in FlxG.sound.list) { - if (e.playing) { - playingList.push({ - sound: e, - time: e.time - }); - e.stop(); - } - } - if (FlxG.sound.music != null) - FlxG.sound.music.stop(); + var i = AudioSource.activeSources.length; + while (i-- > 0) AudioSource.activeSources[i].dispose(); - AudioManager.shutdown(); - AudioManager.init(); - // #if !lime_doc_gen - // if (AudioManager.context.type == OPENAL) - // { - // var alc = AudioManager.context.openal; + AudioManager.shutdown(); - // var device = alc.openDevice(); - // var ctx = alc.createContext(device); - // alc.makeContextCurrent(ctx); - // alc.processContext(ctx); - // } - // #end - Main.changeID++; + AudioManager.init(); + Main.changeID++; + // #if !lime_doc_gen + // if (AudioManager.context.type == OPENAL) + // { + // var alc = AudioManager.context.openal; - for(e in playingList) { - e.sound.play(e.time); - } + // var device = alc.openDevice(); + // var ctx = alc.createContext(device); + // alc.makeContextCurrent(ctx); + // alc.processContext(ctx); + // } + // #end - Main.audioDisconnected = false; - } - #end + for (sound in soundList) { + sound.makeChannel(); + sound.resume(); + } + + Main.audioDisconnected = false; } + private static var timer:Timer; + + private static function onRun() if (Main.audioDisconnected) onAudioDisconnected(); public static function init() { - #if windows NativeAPI.registerAudio(); - FlxG.signals.preStateCreate.add(onStateSwitch); - #end + if (timer == null) (timer = new Timer(1000)).run = onRun; } -} - -@:dox(hide) -typedef PlayingSound = { - var sound:FlxSound; - var time:Float; } \ No newline at end of file diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 49c5a991..0ad9af3d 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -3,43 +3,184 @@ package funkin.backend.utils; import flixel.sound.FlxSound; import lime.media.AudioBuffer; -// Thank you yosh :DDD -lunar -// (https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx) +#if lime_vorbis +import lime.media.vorbis.Vorbis; +import lime.media.vorbis.VorbisFile; +import lime.utils.ArrayBuffer; +#end + +// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx +// REWRITTEN BY RALTYRO class AudioAnalyzer { + public var sound:FlxSound; public var buffer:AudioBuffer; - var __peakByte:Float = 0; - var __timeMulti:Float = 0; + var __toBits:Float; + var __wordSize:Int; + var __bitUnsignedSize:Int; + var __bitSize:Int; + + #if lime_vorbis + var __vorbis:VorbisFile; + #end public function new(sound:FlxSound) { - @:privateAccess buffer = sound._sound.__buffer; - - __peakByte = Math.pow(2, buffer.bitsPerSample-1)-1; - - __timeMulti = 1 / (1 / buffer.sampleRate); - __timeMulti *= buffer.bitsPerSample; - __timeMulti -= __timeMulti % buffer.bitsPerSample; + this.sound = sound; + __check(); } - public function analyze(startPos:Float, endPos:Float):Float { - var bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample; - var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample; + inline function __check() if (sound.buffer != buffer) { + __bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1); + __bitUnsignedSize = 1 << buffer.bitsPerSample; + __toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3); + __vorbis = null; + } - bytesStartPos -= bytesStartPos % buffer.bitsPerSample; - bytesEndPos -= bytesEndPos % buffer.bitsPerSample; + public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float { + __check(); + if (buffer.data != null) return __analyze(startPos, endPos, outSeperate); + #if lime_vorbis + else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate); + #end + return 0; + } - var maxByte:Int = 0; - for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) { - #if js - var byte:Int = buffer.data[bytesStartPos + (i * buffer.bitsPerSample)] - | (buffer.data[bytesStartPos + (i * buffer.bitsPerSample) + 1] << 8); - #else - var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample)) - | (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 1) << 8); - #end - if (byte > 256 * 128) byte -= 256 * 256; - if (maxByte < byte) maxByte = byte; + inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float { + var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end; + var max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null; + if (useSeperate) { + while (c < outSeperate.length) outSeperate[c++] *= __bitSize; + while (c++ < buffer.channels) outSeperate.push(0); + c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize); } - return maxByte/__peakByte; + pos -= pos % __wordSize; + end -= end % __wordSize; + + while (pos < end) { + // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0) + if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize; + else { + while (w < buffer.bitsPerSample) { + b |= #if js buf[pos] #else buf.get(pos) #end << w; + w += 8; + pos++; + } + if (b > __bitSize) b -= __bitUnsignedSize; + } + if (max < b) max = b; + if (useSeperate) { + if (outSeperate[c] < b) outSeperate[c] = b; + if (++c > buffer.channels) c = 0; + } + w = b = 0; + } + + return max / __bitSize; } + + #if lime_vorbis // As far i know, only native supports vorbis + var __buffer:ArrayBuffer; + var __bufferSize:Int; + function __prepareVorbis():Bool @:privateAccess { + if (buffer.__srcVorbisFile == null) return __vorbis != null; + if (__vorbis != null) return true; + if ((__vorbis = buffer.__srcVorbisFile.clone()) != null) { // IM HOPING IT HAVE A GC CLOSURE. + __buffer = new ArrayBuffer(__bufferSize = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3)); + return true; + } + return false; + } + + inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array):Float @:privateAccess { + var n = Math.floor((endPos - startPos) * __toBits); + if ((n -= n % __wordSize) < __wordSize) return 0; + + var pos = 0, max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null; + if (useSeperate) { + while (c < outSeperate.length) outSeperate[c++] *= __bitSize; + while (c++ < buffer.channels) outSeperate.push(0); + } + + // IT SHOULD BE ALWAYS BACKEND.STREAMED IF THIS GETS CALLED. + var backend = sound._source != null ? sound._source.__backend : null; + if (backend != null && backend.streamTimer != null) { + var i = backend.bufferSizes.length - backend.queuedBuffers; + var time = backend.bufferTimes[i] * 1000; + + if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) { + var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i]; + pos = Math.floor((startPos - time) * __toBits); + c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize); + pos -= pos % __wordSize; + + while (n > 0) { + if (__wordSize == 1) { + b = buf.get(pos++) - __bitSize; + n--; + } + else { + while (w < buffer.bitsPerSample) { + b |= buf.get(pos) << w; + w += 8; + pos++; + n--; + } + if (b > __bitSize) b -= __bitUnsignedSize; + } + if (max < b) max = b; + if (useSeperate) { + if (outSeperate[c] < b) outSeperate[c] = b; + if (++c > buffer.channels) c = 0; + } + if (pos >= size) { + if (++i >= backend.bufferDatas.length) break; + size = backend.bufferSizes[i]; + buf = backend.bufferDatas[i].buffer; + pos = 0; + } + w = b = 0; + } + + startPos = pos / __toBits + time; + // Cannot inline a not final return + } + } + if (n == 0) return max / __bitSize; + + if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) { + if (startPos < 1) __vorbis.rawSeek(0); + else __vorbis.timeSeek(startPos / 1000); + } + + pos = c = 0; + var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, result; + while (n > 0) { + n -= (result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true)); + + if (result == Vorbis.HOLE) continue; + else if (result <= 0) break; + + while (pos < result) { + if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize; + else { + while (w < buffer.bitsPerSample) { + b |= __buffer.get(pos) << w; + w += 8; + pos++; + } + if (b > __bitSize) b -= __bitUnsignedSize; + } + if (max < b) max = b; + if (useSeperate) { + if (outSeperate[c] < b) outSeperate[c] = b; + if (++c > buffer.channels) c = 0; + } + w = b = 0; + } + pos = 0; + } + + return max / __bitSize; + } + #end } \ No newline at end of file diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index b72003c0..2c420393 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -386,7 +386,6 @@ class CoolUtil if (FlxG.sound.music == null || !FlxG.sound.music.playing) { playMusic(Paths.music('freakyMenu'), true, fadeIn ? 0 : 1, true, 102); - FlxG.sound.music.persist = true; if (fadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7); } @@ -416,21 +415,26 @@ class CoolUtil */ @:noUsing public static function playMusic(path:String, Persist:Bool = false, Volume:Float = 1, Looped:Bool = true, DefaultBPM:Float = 102, ?Group:FlxSoundGroup) { Conductor.reset(); - FlxG.sound.playMusic(path, Volume, Looped, Group); - if (FlxG.sound.music != null) { - FlxG.sound.music.persist = Persist; - } + if (FlxG.sound.music == null) FlxG.sound.music = new FlxSound(); + else if (FlxG.sound.music.active) FlxG.sound.music.stop(); + FlxG.sound.music.loadEmbedded(path, Looped); + FlxG.sound.music.volume = Volume; + FlxG.sound.music.persist = Persist; + FlxG.sound.defaultMusicGroup.add(FlxG.sound.music); var infoPath = '${Path.withoutExtension(path)}.ini'; if (Assets.exists(infoPath)) { var musicInfo = IniUtil.parseAsset(infoPath, [ "BPM" => null, "TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT, - "LoopTime" => '${Flags.DEFAULT_LOOP_TIME}' + "LoopTime" => '${Flags.DEFAULT_LOOP_TIME}', + "EndTime" => null, + "Offset" => '0' ]); - if (FlxG.sound.music != null) - FlxG.sound.music.loopTime = Std.parseFloat(musicInfo["LoopTime"]) * 1000; + FlxG.sound.music.loopTime = Std.parseFloat(musicInfo["LoopTime"]) * 1000; + if (musicInfo["EndTime"] != null) FlxG.sound.music.endTime = Std.parseFloat(musicInfo["EndTime"]) * 1000; + FlxG.sound.music.offset = Std.parseFloat(musicInfo["Offset"]) * 1000; var timeSignParsed:Array> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)]; var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE; @@ -446,6 +450,8 @@ class CoolUtil Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat); } else Conductor.changeBPM(DefaultBPM); + + FlxG.sound.music.play(); } /** @@ -812,10 +818,11 @@ 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); } /** diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 36cc38f1..fea17595 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -616,22 +616,17 @@ class Charter extends UIState { updateWaveforms(); } - inline function isSoundLoaded(sound:FlxSound) { - @:privateAccess - return sound != null && sound._sound != null && sound._sound.length > 0; - } - public function updateWaveforms() { var wavesToGenerate:Array<{name:String, sound:FlxSound}> = []; - if(isSoundLoaded(FlxG.sound.music)) + if (FlxG.sound.music.loaded) wavesToGenerate.push({name: "Inst.ogg", sound: FlxG.sound.music}); - if (PlayState.SONG.meta.needsVoices != false && isSoundLoaded(vocals)) + if (PlayState.SONG.meta.needsVoices != false && vocals.loaded) wavesToGenerate.push({name: "Voices.ogg", sound: vocals}); for (strumLine in strumLines) - if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && isSoundLoaded(strumLine.vocals)) + if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && strumLine.vocals.loaded) wavesToGenerate.push({ name: 'Voices${strumLine.strumLine.vocalsSuffix}.ogg', sound: strumLine.vocals @@ -1575,12 +1570,10 @@ class Charter extends UIState { vocals.pause(); for (strumLine in strumLines.members) strumLine.vocals.pause(); } else { - FlxG.sound.music.play(); - vocals.play(); - vocals.time = FlxG.sound.music.time = Conductor.songPosition + Conductor.songOffset * 2; + FlxG.sound.music.play(true, Conductor.songPosition + Conductor.songOffset); + vocals.play(true, FlxG.sound.music.getActualTime()); for (strumLine in strumLines.members) { - strumLine.vocals.play(); - strumLine.vocals.time = vocals.time; + strumLine.vocals.play(true, FlxG.sound.music.getActualTime()); } } } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index f5f6bc5b..cb8103d2 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,25 +999,10 @@ class PlayState extends MusicBeatState inst.onComplete = endSong; - if (!paused) { - FlxG.sound.setMusic(inst); - FlxG.sound.music.play(); - } - vocals.play(); - - 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.play(); - } - vocals.play(); - inst.play(); + var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; + for (strumLine in strumLines.members) strumLine.vocals.play(true, time); + vocals.play(true, time); + inst.play(true, time); updateDiscordPresence(); @@ -1028,15 +1013,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 +1063,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)); + vocals = FlxG.sound.load(Options.streamedVocals ? Assets.getMusic(Paths.voices(SONG.meta.name, difficulty)) : 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; } @@ -1188,17 +1171,11 @@ class PlayState extends MusicBeatState @:dox(hide) function resyncVocals():Void { - vocals.pause(); - for (strumLine in strumLines.members) strumLine.vocals.pause(); + var time = Conductor.songPosition + Conductor.songOffset; + for (strumLine in strumLines.members) strumLine.vocals.play(true, time); + vocals.play(true, time); + inst.play(true, time); - FlxG.sound.music.play(); - Conductor.songPosition = FlxG.sound.music.time; - vocals.time = Conductor.songPosition + Conductor.songOffset; - for (strumLine in strumLines.members) { - strumLine.vocals.time = vocals.time; - strumLine.vocals.play(); - } - vocals.play(); gameAndCharsCall("onVocalsResync"); } @@ -1333,22 +1310,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/game/StrumLine.hx b/source/funkin/game/StrumLine.hx index d126b371..16defb56 100644 --- a/source/funkin/game/StrumLine.hx +++ b/source/funkin/game/StrumLine.hx @@ -114,7 +114,9 @@ class StrumLine extends FlxTypedGroup { this.opponentSide = opponentSide; this.controls = controls; this.notes = new NoteGroup(); - vocals = vocalPrefix != "" ? FlxG.sound.load(Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, vocalPrefix)) : new FlxSound(); + + var v = Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, vocalPrefix); + vocals = vocalPrefix != "" ? FlxG.sound.load(Options.streamedVocals ? Assets.getMusic(v) : v) : new FlxSound(); vocals.persist = false; } 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/funkin/options/Options.hx b/source/funkin/options/Options.hx index 71423236..63e791f5 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -40,6 +40,8 @@ class Options public static var songOffset:Float = 0; public static var framerate:Int = 120; public static var gpuOnlyBitmaps:Bool = #if (mac || web) false #else true #end; // causes issues on mac and web + public static var streamedMusic:Bool = true; + public static var streamedVocals:Bool = true; public static var lastLoadedMod:String = null; diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 9b97e299..1eb41258 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -43,6 +43,14 @@ class AppearanceOptions extends OptionsScreen { "gpuOnlyBitmaps")); } #end + add(new Checkbox( + "Streamed Music", + "If checked, only musics will have streamed audio, ALSO freeing a LOT of memory with the downside of higher cpu usage if more audio are being streamed at once (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on longer songs. If you aren't sure, leave this on.", + "streamedMusic")); + add(new Checkbox( + "Streamed Vocals", + "If checked, vocals will also be streamed if Streamed Music is checked, will impact the performance if the song uses alot of vocals (EXPERIMENTAL). If you aren't sure, leave this on.", + "streamedVocals")); add(new Checkbox( "Auto Pause", "If checked, switching windows will pause the game.", 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..75832ee4 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,584 +1,492 @@ +// ALL REWRITTEN FROM SCRATCH!!!! -raltyro 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; + +#if lime_vorbis +import lime.media.vorbis.Vorbis; import lime.media.vorbis.VorbisFile; -import lime.media.AudioManager; +#end + +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 -{ - public static var initBuffers:Array = []; +@:access(lime.utils.ArrayBufferView) +class NativeAudioSource { + private static var STREAM_BUFFER_SAMPLES:Int = 0x1000; + private static var STREAM_MAX_BUFFERS:Int = 32; + private static var STREAM_TIMER_FREQUENCY:Int = 100; + private static var STREAM_BUFFER_FREQUENCY:Int = 6; - 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 var buffers:Array; - private var bufferTimeBlocks:Array; - private var completed:Bool; - private var dataLength:Int; - private var format:Int; - private var handle:ALSource; - private var length:Null; - private var loops:Int; private var parent:AudioSource; + private var disposed:Bool; + private var streamed:Bool; private var playing:Bool; - private var position:Vector4; - private var samples:Float; - private var stream:Bool; - private var streamTimer:Timer; + private var completed:Bool; + + private var handle:ALSource; + private var buffers:Array; + private var unusedBuffers:Array; private var timer:Timer; + private var format:Int; + private var samples:Float; - private var pcmTotal:Int64; - private var sampleRate:Int; + private var streamTimer:Timer; + private var bufferDatas:Array; + private var bufferTimes:Array; + private var bufferSizes:Array; + private var bufferSize:Int; + private var requestBuffers:Int; + private var queuedBuffers:Int; + private var toLoop:Int; + private var streamEnded:Bool; + private var dataLength:Float; - var disposed:Bool = false; + private var position:Vector4 = new Vector4(); + private var length:Null; + private var loopTime:Null; + private var loops:Int; - public function new(parent:AudioSource) - { - this.parent = parent; + public function new(parent:AudioSource) this.parent = parent; - position = new Vector4(); - } - - public function dispose():Void - { - if (handle != null) - { - stop(); - AL.sourcei(handle, AL.BUFFER, null); - AL.deleteSource(handle); - if (buffers != null) - { - for (buffer in buffers) - { - AL.deleteBuffer(buffer); - } - buffers = null; - } - handle = null; - } + public function dispose() { disposed = true; - initBuffers.remove(this); + stop(); + + if (handle != null) { + AL.sourcei(handle, AL.BUFFER, AL.NONE); + AL.deleteSource(handle); + } + handle = null; + + if (buffers != null) AL.deleteBuffers(buffers); + buffers = null; + bufferDatas = null; + bufferTimes = null; + bufferSizes = null; + unusedBuffers = null; } - public function init():Void - { - dataLength = 0; - format = 0; + public function init() { + if (handle != null) return; - if (parent.buffer.channels == 1) - { - if (parent.buffer.bitsPerSample == 8) - { - format = AL.FORMAT_MONO8; - } - else if (parent.buffer.bitsPerSample == 16) - { - format = AL.FORMAT_MONO16; - } + if (disposed = (handle = AL.createSource()) == null) return; + AL.sourcef(handle, AL.MAX_GAIN, 10); + + var buffer = parent.buffer; + var channels = buffer.channels, bitsPerSample = buffer.bitsPerSample; + + // Default is just AL.FORMAT_MONO8 if it doesn't match any to avoid yo ears getting blasted + var isCreativeXFi = AL.getString(AL.RENDERER) == "X-Fi"; + if (channels > 2 && (isCreativeXFi || AL.isExtensionPresent("AL_EXT_MCFORMATS"))) { // https://github.com/openalext/openalext/blob/master/AL_EXT_MCFORMATS.txt + if (channels == 3) format = bitsPerSample == 32 ? 0x1209 : (bitsPerSample == 16 ? 0x1208 : 0x1207); + else if (channels == 4) format = bitsPerSample == 32 ? 0x1206 : (bitsPerSample == 16 ? 0x1205 : 0x1204); + else if (channels == 6) format = bitsPerSample == 32 ? 0x120C : (bitsPerSample == 16 ? 0x120B : 0x120A); + else if (channels == 7) format = bitsPerSample == 32 ? 0x120F : (bitsPerSample == 16 ? 0x120E : 0x120D); + else if (channels == 8) format = bitsPerSample == 32 ? 0x1212 : (bitsPerSample == 16 ? 0x1211 : 0x1210); } - 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; - } + // The 32 bit for STEREO and MONO are undocumented? https://github.com/kcat/openal-soft/issues/934, though it only works in X-Fi OpenAL?? + else if (bitsPerSample == 32 && isCreativeXFi) format = channels == 2 ? 0x1203 : 0x1202; + else if (channels == 2) format = bitsPerSample == 16 ? AL.FORMAT_STEREO16 : AL.FORMAT_STEREO8; + else format = bitsPerSample == 16 ? AL.FORMAT_MONO16 : AL.FORMAT_MONO8; + + if (buffer.data != null) { + streamed = false; + samples = (dataLength = (buffer.data.length >> 0)) / buffer.channels / (buffer.bitsPerSample >> 3); + + if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null) + AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate); + + AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer); } + #if lime_vorbis + else if (buffer.__srcVorbisFile != null) { + streamed = true; + dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3); - if (parent.buffer.__srcVorbisFile != null) - { - stream = true; - - 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); - - buffers = new Array(); - bufferTimeBlocks = new Array(); - - for (i in 0...STREAM_NUM_BUFFERS) - { - buffers.push(AL.createBuffer()); - bufferTimeBlocks.push(0); - } - - handle = AL.createSource(); - - samples = Int64.toInt(pcmTotal); + var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8; + bufferSize = STREAM_BUFFER_SAMPLES * buffer.channels * (constructor == Int8 ? buffer.bitsPerSample >> 3 : 1); + buffers = AL.genBuffers(STREAM_MAX_BUFFERS); + bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(bufferSize, constructor)]; + bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; + bufferSizes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; + unusedBuffers = []; + bufferSize *= constructor == Int8 ? 1 : buffer.bitsPerSample >> 3; } - else - { - if (parent.buffer.__srcBuffer == null) - { - parent.buffer.__srcBuffer = AL.createBuffer(); + #end - if (parent.buffer.__srcBuffer != null) - { - AL.bufferData(parent.buffer.__srcBuffer, format, parent.buffer.data, parent.buffer.data.length, parent.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)); + if (dataLength == 0) { + trace('NativeAudioSource Bug! dataLength is 0'); + dispose(); } - - initBuffers.push(this); } - 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() { + 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() { + if (!disposed) 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]; - } - bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell(); - - while (total < length) - { - readMax = 4096; - - if (readMax > length - total) - { - readMax = length - total; - } - - read = vorbisFile.read(buffer.buffer, total, readMax); - - if (read > 0) - { - total += read; - } - else - { - break; - } - } - - return buffer; - #else - return null; - #end - } - - private function refillBuffers(buffers:Array = null):Void - { - #if lime_vorbis - var vorbisFile = null; - var position = 0; - - if (buffers == null) - { - var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); - - if (buffersProcessed > 0) - { - vorbisFile = parent.buffer.__srcVorbisFile; - position = Int64.toInt(vorbisFile.pcmTell()); - - if (position < dataLength) - { - buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed); - } - } - } - - 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); - } - } - #end - } - - public function stop():Void - { - if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) - { + public function stop() { + if (!disposed) { AL.sourceStop(handle); + toLoop = 0; } playing = false; - - if (streamTimer != null) - { - streamTimer.stop(); - } - - if (timer != null) - { - timer.stop(); - } - - setCurrentTime(0); + stopStreamTimer(); + stopTimer(); } - // Event Handlers - private function streamTimer_onRun():Void - { - refillBuffers(); - } - - private function timer_onRun():Void - { - if (loops > 0) - { - playing = false; - loops--; - setCurrentTime(0); - play(); - return; - } - else - { - stop(); - } - + private function complete() { completed = true; + stop(); parent.onComplete.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; - } - } - + // Streaming, atleast for vorbis for now. + // just incase if we support more than 1 in the future?? which i doubt + private function streamTell():Float { + #if lime_vorbis + return parent.buffer.__srcVorbisFile.timeTell(); + #else return 0; + #end } - public function setCurrentTime(value:Float):Float - { - // `setCurrentTime()` has side effects and is never safe to skip. - /* if (value == getCurrentTime()) - { - return value; - } */ + private function streamSeek(samples:Int64) { + #if lime_vorbis + // There's apparently a bug in libvorbis <= 1.3.4, hhttps://github.com/xiph/vorbis/blob/master/CHANGES#L39 + // But it seems to also happen in this lime libvorbis version which is 1.3.7...? -ralty + if (samples < 4) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples); + #end + } - if (handle != null) - { - if (stream) - { - AL.sourceStop(handle); + private function readToBufferData(data:ArrayBufferView):Int { + #if lime_vorbis + var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize = parent.buffer.bitsPerSample >> 3; + var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize); + var n:Int = size < bufferSize ? Math.floor(size) : bufferSize; + n -= n % (parent.buffer.channels * wordSize); - parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000); - AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS); - refillBuffers(buffers); + var total = 0, result = 0, wasEOF = false; + while (total < bufferSize) { + result = n > 0 ? parent.buffer.__srcVorbisFile.read(data.buffer, total, n, isBigEndian, wordSize, true) : 0; - if (playing) AL.sourcePlay(handle); + if (result == Vorbis.HOLE) continue; + else if (result <= Vorbis.EREAD) break; + else if (result == 0) { + if (wasEOF || (streamEnded = loops <= toLoop)) break; + else { + wasEOF = true; + toLoop++; + + var samples = getSamples(loopTime != null ? loopTime + parent.offset : parent.offset); + streamSeek(samples); + if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = bufferSize - total)) n = Math.floor(size); + n -= n % (parent.buffer.channels * wordSize); + } } - 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); + else { + total += result; + n -= result; + wasEOF = false; } } - if (playing) - { - if (timer != null) - { - timer.stop(); + if (total < bufferSize) data.buffer.fill(total, n, 0); + if (result < 0) { + trace('NativeAudioSource Streaming Bug! reading result is $result, streamEnded: $streamEnded, total: $total, n: $n'); + return result; + } + return total; + #else + return 0; + #end + } + + private function fillBuffer(buffer:ALBuffer):Int { + var i = STREAM_MAX_BUFFERS - requestBuffers; + var data = bufferDatas[i], time = streamTell(); + var decoded = readToBufferData(data); + + if (decoded > 0) { + AL.bufferData(buffer, format, data, decoded, parent.buffer.sampleRate); + + var n = STREAM_MAX_BUFFERS - 1; + while (i < n) { + bufferDatas[i] = bufferDatas[i + 1]; + bufferTimes[i] = bufferTimes[i + 1]; + bufferSizes[i] = bufferSizes[++i]; + } + queuedBuffers = requestBuffers; + bufferDatas[n] = data; + bufferTimes[n] = time; + bufferSizes[n] = decoded; + } + + return decoded; + } + + private function streamRun() { + if (parent == null || parent.buffer == null || handle == null) return dispose(); + if (!playing || disposed) return stopStreamTimer(); + + try { + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer; + while (processed-- > 0) { + buffer = AL.sourceUnqueueBuffer(handle); + if (!streamEnded && --n > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); + else { + queuedBuffers = --requestBuffers; + unusedBuffers.push(buffer); + } } - var timeRemaining = (getLength() - value) / getPitch(); + if (!streamEnded) { + if (unusedBuffers.length != 0) { + requestBuffers++; + if (fillBuffer(buffer = unusedBuffers.pop()) > 0) { + queuedBuffers = requestBuffers; + AL.sourceQueueBuffer(handle, buffer); + } + else { + requestBuffers--; + unusedBuffers.push(buffer); + } + } + else if (queuedBuffers < STREAM_MAX_BUFFERS) { + if (fillBuffer(buffer = buffers[requestBuffers++]) > 0) AL.sourceQueueBuffer(handle, buffer); + else requestBuffers--; + } + } + } + catch(e) trace('NativeAudioSource Bug! in streamRun $e'); - if (timeRemaining > 0) - { - completed = false; - timer = new Timer(timeRemaining); - timer.run = timer_onRun; + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + AL.sourcePlay(handle); + resetTimer((getLength() - getCurrentTime()) / getPitch()); + } + } + + // Timers + inline function stopStreamTimer() { + if (streamTimer != null) streamTimer.stop(); + streamTimer = null; + } + + private function resetStreamTimer() { + stopStreamTimer(); + if (streamed) (streamTimer = new Timer(STREAM_TIMER_FREQUENCY)).run = streamRun; + } + + inline function stopTimer() { + if (timer != null) timer.stop(); + timer = null; + } + + private function resetTimer(timeRemaining:Float) { + stopTimer(); + (timer = new Timer(timeRemaining)).run = timer_onRun; + } + + private function timer_onRun() { + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); + if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING && (!streamed || !streamEnded)) { + resetTimer(timeRemaining); + return; + } + + if (loops == 0) { + complete(); + return; + } + + var start = loopTime != null ? loopTime : 0; + if (toLoop <= 0) { + setLoops(--loops); + if (start > 0 || AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) setCurrentTime(start); + else resetTimer((getLength() - start) / getPitch()); + } + else { + if ((loops -= toLoop) < 0) loops = 0; + toLoop = 0; + resetTimer((getLength() - start) / getPitch()); + } + + parent.onLoop.dispatch(); + } + + // Get & Set Methods + public function getCurrentTime():Float { + if (disposed) return 0; + else if (completed) return getLength(); + + var time = AL.getSourcef(handle, AL.SAMPLE_OFFSET) / parent.buffer.sampleRate; + if (streamed) { + if (playing && streamEnded && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + complete(); + return getLength(); } else - { - playing = false; - completed = true; + time += bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers]; + } + time *= 1000; + + var length = if (length == null) getRealLength(); else length; + if (loops > 0 && time > length) { + var start = loopTime != null ? Math.max(0, loopTime) : 0; + return ((time - start) % (length - start)) + start - parent.offset; + } + + return time - parent.offset; + } + + public function setCurrentTime(value:Float):Float { + if (disposed) return value; + + if (streamed) AL.sourceStop(handle); + else AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset)); + + if (playing) { + var timeRemaining = (getLength() - value) / getPitch(); + if (timeRemaining < 8) complete(); + else { + completed = streamEnded = false; + if (streamed) { + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + + unusedBuffers.resize(0); + streamSeek(getSamples(value + parent.offset)); + + requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY; + for (i in 0...queuedBuffers) { + if (!streamEnded && fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); + else queuedBuffers = --requestBuffers; + } + + if (!streamEnded) resetStreamTimer(); + } + + resetTimer(timeRemaining); + AL.sourcePlay(handle); } } + if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000; + return value; } - public function getGain():Float - { - if (handle != null) - { - return AL.getSourcef(handle, AL.GAIN); - } - else - { - return 1; - } - } + inline private function getRealLength():Float return samples / parent.buffer.sampleRate * 1000; + public function getLength():Null + return if (disposed) 0; else 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 || disposed) return length = value; + var buffer = parent.buffer, wordSize = buffer.bitsPerSample >> 3; + if ((length = value) == null) dataLength = streamed ? samples * buffer.channels * wordSize : getFloat(Int64.make(0, buffer.data.length)); + else dataLength = Math.ffloor(Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate) * buffer.channels * wordSize; + + if (playing) { + if (streamed && (streamEnded || toLoop > 0)) setCurrentTime(getCurrentTime()); + else { + 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 (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE); + else if (loops == 0 && value > 0) setCurrentTime(getCurrentTime()); + if (value < 0) return loops = 0; 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 (!streamed && !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 (AL.isExtensionPresent("AL_SOFT_source_latency")) { + 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; } -} + + inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >> 0); + inline private function getSamples(ms:Float):Int64 return Int64.fromFloat(Math.max(0, Math.min(ms / 1000 * parent.buffer.sampleRate, samples))); +} \ 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..40ebfbec --- /dev/null +++ b/source/lime/media/AudioBuffer.hx @@ -0,0 +1,491 @@ +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(); + __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], 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)], 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..87f9e327 100644 --- a/source/lime/media/AudioSource.hx +++ b/source/lime/media/AudioSource.hx @@ -1,29 +1,100 @@ 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 { + private static var activeSources:Array = []; + + /** + 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 +106,136 @@ 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(); + activeSources.remove(this); } - @:noCompletion private function init():Void + @:noCompletion inline private function init():Void { __backend.init(); + activeSources.push(this); } - 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/lime/media/vorbis/VorbisFile.hx b/source/lime/media/vorbis/VorbisFile.hx new file mode 100644 index 00000000..bd29a7a7 --- /dev/null +++ b/source/lime/media/vorbis/VorbisFile.hx @@ -0,0 +1,356 @@ +package lime.media.vorbis; + +#if (!lime_doc_gen || lime_vorbis) +import haxe.Int64; +import haxe.io.Bytes; +import lime._internal.backend.native.NativeCFFI; + +#if hl +@:keep +#end +@:access(lime._internal.backend.native.NativeCFFI) +class VorbisFile +{ + public var bitstream(default, null):Int; + + @:noCompletion private var handle:Dynamic; + + @:noCompletion var _filePath:String; + @:noCompletion var _bytes:Bytes; + + @:noCompletion private function new(handle:Dynamic) + { + this.handle = handle; + } + + public function bitrate(bitstream:Int = -1):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_bitrate(handle, bitstream); + #else + return 0; + #end + } + + public function bitrateInstant():Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_bitrate_instant(handle); + #else + return 0; + #end + } + + public function clear():Void + { + #if (lime_cffi && lime_vorbis && !macro) + NativeCFFI.lime_vorbis_file_clear(handle); + #end + handle = null; + _filePath = null; + _bytes = null; + } + + public function clone():VorbisFile + { + if (_filePath != null) return fromFile(_filePath); + if (_bytes != null) return fromBytes(_bytes); + return null; + } + + public function comment(bitstream:Int = -1):VorbisComment + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_comment(handle, bitstream); + + if (data != null) + { + var comment = new VorbisComment(); + comment.userComments = data.userComments; + comment.vendor = data.vendor; + return comment; + } + #end + + return null; + } + + public function crosslap(other:VorbisFile):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_crosslap(handle, other.handle); + #else + return 0; + #end + } + + public static function fromBytes(bytes:Bytes):VorbisFile + { + #if (lime_cffi && lime_vorbis && !macro) + var handle = NativeCFFI.lime_vorbis_file_from_bytes(bytes); + + if (handle != null) + { + var vorbisFile = new VorbisFile(handle); + vorbisFile._bytes = bytes; + return vorbisFile; + } + #end + + return null; + } + + public static function fromFile(path:String):VorbisFile + { + #if (lime_cffi && lime_vorbis && !macro) + var handle = NativeCFFI.lime_vorbis_file_from_file(path); + + if (handle != null) + { + var vorbisFile = new VorbisFile(handle); + vorbisFile._filePath = path; + return vorbisFile; + } + #end + + return null; + } + + public function info(bitstream:Int = -1):VorbisInfo + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_info(handle, bitstream); + + if (data != null) + { + var info = new VorbisInfo(); + info.bitrateLower = data.bitrateLower; + info.bitrateNominal = data.bitrateNominal; + info.bitrateUpper = data.bitrateUpper; + info.channels = data.channels; + info.rate = data.rate; + info.version = data.version; + return info; + } + #end + + return null; + } + + public function pcmSeek(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_pcm_seek(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function pcmSeekLap(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_pcm_seek_lap(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function pcmSeekPage(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_pcm_seek_page(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function pcmSeekPageLap(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_pcm_seek_page_lap(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function pcmTell():Int64 + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_pcm_tell(handle); + + if (data != null) + { + return Int64.make(data.high, data.low); + } + #end + + return Int64.ofInt(0); + } + + public function pcmTotal(bitstream:Int = -1):Int64 + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_pcm_total(handle, bitstream); + + if (data != null) + { + return Int64.make(data.high, data.low); + } + #end + + return Int64.ofInt(0); + } + + public function rawSeek(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_raw_seek(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function rawSeekLap(pos:Int64):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_raw_seek_lap(handle, pos.low, pos.high); + #else + return 0; + #end + } + + public function rawTell():Int64 + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_raw_tell(handle); + + if (data != null) + { + return Int64.make(data.high, data.low); + } + #end + + return Int64.ofInt(0); + } + + public function rawTotal(bitstream:Int = -1):Int64 + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_raw_total(handle, bitstream); + + if (data != null) + { + return Int64.make(data.high, data.low); + } + #end + + return Int64.ofInt(0); + } + + public function read(buffer:Bytes, position:Int, length:Int = 4096, bigEndianPacking:Bool = false, wordSize:Int = 2, signed:Bool = true):Int + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_read(handle, buffer, position, length, bigEndianPacking, wordSize, signed); + if (data == null) return 0; + bitstream = data.bitstream; + return data.returnValue; + #else + return 0; + #end + } + + // public function readFilter (buffer:Bytes, length:Int = 4096, endianness:Endian = LITTLE_ENDIAN, wordSize:Int = 2, signed:Bool = true, bitstream:Int = 0, filter, filter_param + public function readFloat(pcmChannels:Bytes, samples:Int):Int + { + #if (lime_cffi && lime_vorbis && !macro) + var data = NativeCFFI.lime_vorbis_file_read_float(handle, pcmChannels, samples); + if (data == null) return 0; + bitstream = data.bitstream; + return data.returnValue; + #else + return 0; + #end + } + + public function seekable():Bool + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_seekable(handle); + #else + return false; + #end + } + + public function serialNumber(bitstream:Int = -1):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_serial_number(handle, bitstream); + #else + return 0; + #end + } + + public function streams():Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_streams(handle); + #else + return 0; + #end + } + + public function timeSeek(s:Float):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_seek(handle, s); + #else + return 0; + #end + } + + public function timeSeekLap(s:Float):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_seek_lap(handle, s); + #else + return 0; + #end + } + + public function timeSeekPage(s:Float):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_seek_page(handle, s); + #else + return 0; + #end + } + + public function timeSeekPageLap(s:Float):Int + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_seek_page_lap(handle, s); + #else + return 0; + #end + } + + public function timeTell():Float + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_tell(handle); + #else + return 0; + #end + } + + public function timeTotal(bitstream:Int = -1):Float + { + #if (lime_cffi && lime_vorbis && !macro) + return NativeCFFI.lime_vorbis_file_time_total(handle, bitstream); + #else + return 0; + #end + } +} +#end diff --git a/source/openfl/media/Sound.hx b/source/openfl/media/Sound.hx index cbccc35b..808399c3 100644 --- a/source/openfl/media/Sound.hx +++ b/source/openfl/media/Sound.hx @@ -599,17 +599,6 @@ class Sound extends EventDispatcher #end } - var changeID:Int = 0; - public function regen() { - var audioBuffer = new AudioBuffer(); - audioBuffer.bitsPerSample = __buffer.bitsPerSample; - audioBuffer.channels = __buffer.channels; - audioBuffer.data = __buffer.data; - audioBuffer.sampleRate = __buffer.sampleRate; - - __buffer = audioBuffer; - } - /** Generates a new SoundChannel object to play back the sound. This method returns a SoundChannel object, which you access to stop the sound and to @@ -636,13 +625,6 @@ class Sound extends EventDispatcher return null; } - #if !macro - if (changeID < funkin.backend.system.Main.changeID) { - changeID = funkin.backend.system.Main.changeID; - regen(); - } - #end - if (sndTransform == null) { sndTransform = new SoundTransform(); @@ -690,26 +672,10 @@ class Sound extends EventDispatcher #if (js && html5 && howlerjs) return __buffer.src.duration() * 1000; #else - if (__buffer.data != null) - { - // var samples = (__buffer.data.length * 8) / (__buffer.channels * __buffer.bitsPerSample); - // return Std.int(samples / __buffer.sampleRate * 1000); - var samples = (Int64.make(0, __buffer.data.length) * Int64.ofInt(8)) / Int64.ofInt(__buffer.channels * __buffer.bitsPerSample); - var div = Int64.divMod(samples * Int64.ofInt(1000), Int64.ofInt(__buffer.sampleRate)); - var value = (div.quotient + (div.modulus / __buffer.sampleRate)); - return Int64.toInt(value); - } - else if (__buffer.__srcVorbisFile != null) - { - //var samples = Int64.toInt(__buffer.__srcVorbisFile.pcmTotal()); - //return Std.int(samples / __buffer.sampleRate * 1000); - var samples = __buffer.__srcVorbisFile.pcmTotal(); - var value = Int64.fromFloat(__buffer.__srcVorbisFile.timeTotal()) * 1000; - return Int64.toInt(value); - } - else - { - return 0; + if (__buffer.data != null) return (__buffer.data.length >> 0) / __buffer.channels / (__buffer.bitsPerSample >> 3) / __buffer.sampleRate * 1000; + else if (__buffer.__srcVorbisFile != null) { + var x = __buffer.__srcVorbisFile.pcmTotal(); + return (x.high * 4294967296. + (x.low >> 0)) / __buffer.sampleRate * 1000; } #end } diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 0838f391..6ccbf8ac 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -1,10 +1,13 @@ package openfl.media; #if !flash +import haxe.Int64; + import openfl.events.Event; import openfl.events.EventDispatcher; #if lime import lime.media.AudioSource; +import lime.media.openal.AL; #end /** @@ -16,11 +19,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 +39,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 +62,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 +69,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 +108,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 +137,7 @@ import lime.media.AudioSource; #if lime __source.onComplete.remove(source_onComplete); + __source.onLoop.remove(source_onLoop); __source.dispose(); __source = null; #end @@ -145,6 +149,90 @@ import lime.media.AudioSource; this.soundTransform = soundTransform; } + @:noCompletion private function __updatePeaks(time:Float):Bool + { + if (Math.abs(time - __lastPeakTime) < 8) return false; + + __lastPeakTime = time; + __leftPeak = __rightPeak = 0; + + #if !macro + if (!__isValid) return false; + + var buffer = __source.buffer; + var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1); + var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize), size = 0, buf; + + #if lime_cffi + var backend = __source.__backend, i = 0; + if (backend.streamed) { + size = backend.bufferSizes[i = backend.bufferSizes.length - backend.queuedBuffers]; + buf = backend.bufferDatas[i].buffer; + pos -= Math.floor(backend.bufferTimes[i] * buffer.sampleRate * buffer.channels * wordSize); + } + else + #end { + buf = buffer.data #if !js .buffer #end; + size = #if js buf.byteLength #else buf.length #end; + } + + var s = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0; + pos -= pos % wordSize; + + while (s > 0) { + if (wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - bitSize; + else { + while (w < buffer.bitsPerSample) { + b |= #if js buf[pos] #else buf.get(pos) #end << w; + w += 8; + pos++; + } + if (b > bitSize) b -= bitUnsignedSize; + } + ((c % 2 == 0) ? (if (__leftPeak < b) __leftPeak = b) : (if (__rightPeak < b) __rightPeak = b)); + + if (pos >= size) #if lime_cffi { + if (!backend.streamed || ++i >= backend.bufferDatas.length) break; + size = backend.bufferSizes[i]; + buf = backend.bufferDatas[i].buffer; + pos = 0; + } + #else break; #end + + if (++c > buffer.channels) { + c = 0; + s--; + } + w = b = 0; + } + + if (buffer.channels == 1) __rightPeak = __leftPeak / bitSize; + else { + __leftPeak = __leftPeak / bitSize; + __rightPeak = __rightPeak / bitSize; + } + #end + + return true; + } + + @: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 +255,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 +270,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 +292,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(get_position()); + return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + @:noCompletion private function get_rightPeak():Float + { + __updatePeaks(get_position()); + return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + // Event Handlers @:noCompletion private function source_onComplete():Void { @@ -234,14 +402,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 diff --git a/source/openfl/utils/Assets.hx b/source/openfl/utils/Assets.hx index db92e530..eef85212 100644 --- a/source/openfl/utils/Assets.hx +++ b/source/openfl/utils/Assets.hx @@ -243,16 +243,15 @@ class Assets public static function getMusic(id:String, useCache:Bool = true):Sound { - #if (lime_vorbis && lime > "7.9.0") - var path = getPath(id); - // TODO: What if it is a WAV or non-Vorbis file? - var vorbisFile = VorbisFile.fromFile(path); - var buffer = AudioBuffer.fromVorbisFile(vorbisFile); - return Sound.fromAudioBuffer(buffer); - #else - // TODO: Streaming sound - return getSound(id, useCache); + #if (lime_vorbis && lime > "7.9.0" && !macro) + if (Options.streamedMusic) { + var path = getPath(id); + // TODO: What if it is a WAV or non-Vorbis file? + var vorbisFile = VorbisFile.fromFile(path); + if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile)); + } #end + return getSound(id, useCache); } /**