From 6cb90a66c944c136acdc05a9f2d6381e0442e248 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:37:35 +0700 Subject: [PATCH 01/17] FlxSound & internal rewrite & Streamed Music --- source/flixel/sound/FlxSound.hx | 955 ++++++++++++++++++ source/funkin/backend/system/Conductor.hx | 4 +- source/funkin/backend/utils/CoolUtil.hx | 8 +- source/funkin/game/PlayState.hx | 70 +- source/funkin/menus/PauseSubState.hx | 2 +- .../backend/html5/HTML5AudioSource.hx | 44 +- .../backend/native/NativeAudioSource.hx | 774 ++++++-------- source/lime/media/AudioBuffer.hx | 494 +++++++++ source/lime/media/AudioSource.hx | 153 ++- source/openfl/media/SoundChannel.hx | 299 ++++-- 10 files changed, 2223 insertions(+), 580 deletions(-) create mode 100644 source/flixel/sound/FlxSound.hx create mode 100644 source/lime/media/AudioBuffer.hx diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx new file mode 100644 index 00000000..4884a926 --- /dev/null +++ b/source/flixel/sound/FlxSound.hx @@ -0,0 +1,955 @@ +package flixel.sound; + +import lime.media.AudioBuffer; +import lime.media.AudioSource; +import lime.media.openal.AL; +#if lime_vorbis +import lime.media.vorbis.VorbisFile; +#end + +import openfl.Assets; +import openfl.events.IEventDispatcher; +import openfl.events.Event; +import openfl.media.Sound; +import openfl.media.SoundChannel; +import openfl.media.SoundTransform; +import openfl.media.SoundMixer; +import openfl.net.URLRequest; +import openfl.utils.AssetType; +#if flash11 +import openfl.utils.ByteArray; +#end + +import flixel.math.FlxMath; +import flixel.math.FlxPoint; +import flixel.system.FlxAssets.FlxSoundAsset; +import flixel.tweens.FlxTween; +import flixel.util.FlxSignal; +import flixel.util.FlxStringUtil; +import flixel.FlxBasic; +import flixel.FlxObject; +import flixel.FlxG; + +@:access(flixel.FlxGame) +class FlxSound extends FlxBasic { + /** + * The x position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. + */ + public var x:Float; + + /** + * The y position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. + */ + public var y:Float; + + /** + * Whether or not this sound should be automatically destroyed when you switch states. + */ + public var persist:Bool; + + /** + * Whether or not the sound is currently playing. + */ + public var playing(get, never):Bool; + + /** + * Set volume to a value between 0 and 1 to change how this sound is. + */ + public var volume(get, set):Float; + + /** + * Whether to make this sound muted or not. + */ + public var muted(default, set):Bool; + + #if FLX_PITCH + /** + * Set pitch, which also alters the playback speed. Default is 1. + * @since 5.0.0 + */ + public var pitch(get, set):Float; + + /** + * Alters the pitch of the sound depends on the current FlxG.timeScale. Default is true. + * @since raltyMod + */ + public var timeScaleBased:Bool; + #end + + /** + * Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this. + */ + public var pan(get, set):Float; + + /** + * The position in runtime of the music playback in milliseconds. + * If set while paused, changes only come into effect after a `resume()` call. + */ + public var time(get, set):Float; + + /** + * The offset for this FlxSound. + * Useful for just generally offsetting this sound without affecting time. + * @since raltyMod + */ + public var offset(get, set):Float; + + /** + * The length of the sound in milliseconds. + * @since 4.2.0 + */ + public var length(get, never):Float; + + /** + * The latency of the sound in milliseconds. + * @since raltyMod + */ + //public var latency(get, never):Float; + + /** + * Whether or not this sound should loop. + */ + public var looped(default, set):Bool; + + /** + * In case of looping, the point (in milliseconds) from where to restart the sound when it loops back + * @since 4.1.0 + */ + public var loopTime(default, set):Float; + + /** + * At which point to stop playing the sound, in milliseconds. + * If not set / `null`, the sound completes normally. + * @since 4.2.0 + */ + public var endTime(default, set):Null; + + /** + * The sound's "target" (for proximity and panning). + * @since raltyMod + */ + public var target:Null; + + /** + * The maximum effective radius of this sound (for proximity and panning). + * @since raltyMod + */ + public var radius:Float; + + /** + * Whether the proximity alters the pan or not + * @since raltyMod + */ + public var proximityPan:Bool; + + /** + * Stores for how much channels are in the loaded sound. + * @since raltyMod + */ + public var channels(get, never):Int; + + /** + * Whether or not this sound is stereo instead of mono. + * @since raltyMod + */ + public var stereo(get, never):Bool; + + /** + * Whether or not this sound is loaded yet. + * @since raltyMod + */ + public var loaded(get, never):Bool; + + /** + * Whether or not this sound is streamed, only vorbis though. + * @since raltyMod + */ + public var streamed(get, never):Bool; + + /** + * Stores the average wave amplitude of both stereo channels. + * @since raltyMod + */ + public var amplitude(get, never):Float; + + /** + * Just the amplitude of the left stereo channel + * @since raltyMod + */ + public var amplitudeLeft(get, never):Float; + + /** + * Just the amplitude of the right stereo channel + * @since raltyMod + */ + public var amplitudeRight(get, never):Float; + + /** + * The ID3 song name. Defaults to null. Currently only works for MP3 streamed sounds. + */ + public var name(default, null):String; + + /** + * The ID3 artist name. Defaults to null. Currently only works for MP3 streamed sounds. + */ + public var artist(default, null):String; + + /** + * Whether to call `destroy()` when the sound has finished playing. + */ + public var autoDestroy:Bool; + + /** + * Signal that is dispatched on sound complete. + */ + public final onFinish:FlxSignal; + + /** + * Tracker for sound complete callback. If assigned, will be called + * each time when sound reaches its end. + */ + //@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.") + public var onComplete:Void->Void; + + /** + * The sound group this sound belongs to + */ + public var group(default, set):FlxSoundGroup; + + /** + * Stores the sound lime AudioBuffer if exists. + * @since raltyMod + */ + public var buffer(get, never):AudioBuffer; + + #if lime_vorbis + /** + * Stores the sound VorbisFile if exists. + * @since raltyMod + */ + public var vorbis(get, never):VorbisFile; + #end + + /** + * The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`) + * @since 4.1.0 + */ + public var fadeTween:FlxTween; + + @:allow(flixel.system.frontEnds.SoundFrontEnd.load) var _sound:Sound; + var _transform:SoundTransform; + var _channel:SoundChannel; + var _source:AudioSource; + var _paused:Bool; + var _volume:Float; + var _volumeAdjust:Float; + var _time:Float; + var _offset:Float; + var _timeInterpolation:Float; + var _lastTime:Null; // FlxG.game.getTicks(), in MS + var _length:Float; + #if FLX_PITCH + var _pitch:Float; + var _timeScaleAdjust:Float; + var _realPitch:Float; + #end + var _amplitudeLeft:Float; + var _amplitudeRight:Float; + var _amplitudeUpdate:Bool; + var _amplitudeTime:Float; + var _alreadyPaused:Null; + + public function new() { + super(); + onFinish = new FlxSignal(); + revive(); + } + + /** + * Resets this FlxSound properties. + * + * @param clean Whether if this FlxSound also needs to be cleaned up too. + */ + public function reset():Void { + if (_source != null) stop(); + x = y = 0; + + muted = false; + looped = false; + loopTime = 0; + endTime = null; + autoDestroy = false; + visible = false; + target = null; + radius = 0; + proximityPan = true; + onComplete = null; + timeScaleBased = true; + + _alreadyPaused = null; + _cameras = null; + _lastTime = null; + _paused = false; + _length = _time = 0; + _volume = _volumeAdjust = 1; + _amplitudeLeft = _amplitudeRight = 0; + _amplitudeUpdate = true; + #if FLX_PITCH _pitch = _realPitch = _timeScaleAdjust = 1; #end + } + + /** + * Internal cleanup function for cleaning up this FlxSound. + * @since raltyMod + */ + function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess { + active = false; + _lastTime = null; + + if (_channel != null) { + _channel.removeEventListener(Event.SOUND_COMPLETE, stopped); + } + + if (destroySound) { + onFinish.removeAll(); + + if (group != null) group.remove(this); + + if (_channel != null) { + _channel.stop(); + _channel = null; + } + if (_source != null) _source.dispose(); + _source = null; + _sound = null; + + reset(); + } + else if (_channel != null && _channel.__isValid) { + if (resetPosition) { + _source.stop(); + + _time = 0; + _paused = false; + } + else if (!_paused) { + get_time(); + _source.pause(); + } + } + } + + /** + * Handles fade out, fade in, panning, proximity, and amplitude operations each frame. + */ + override function update(elapsed:Float):Void { + if (!playing) return; + + var timeScaleTarget = timeScaleBased ? FlxG.timeScale : 1.0; + if (_timeScaleAdjust != timeScaleTarget) { + _timeScaleAdjust = timeScaleTarget; + pitch = _pitch; + if (_channel == null) return; + } + + _amplitudeUpdate = true; + + var radialMultiplier = 1.0; + + // Distance-based volume control (TODO for Ralty: REDO THIS) + if (target != null) { + var targetPosition = target.getPosition(); + radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / radius; + targetPosition.put(); + radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1); + + if (proximityPan && _transform != null) { + var d:Float = (x - target.x) / radius; + _transform.pan = FlxMath.bound(d, -1, 1); + } + } + + _volumeAdjust = radialMultiplier; + updateTransform(); + } + + override function revive() { + reset(); + super.revive(); + } + + override function kill() { + super.kill(); + reset(); + } + + override function destroy() { + super.destroy(); + cleanup(true); + } + + /** + * One of the main setup functions for sounds, this function loads a sound from an embedded MP3. + * + * @param embeddedSound An embedded Class object representing an MP3 file. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @param onComplete Called when the sound finished playing + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadEmbedded(embeddedSound:FlxSoundAsset, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound { + if (!exists || embeddedSound == null) return this; + cleanup(true); + + if ((embeddedSound is Sound)) _sound = embeddedSound; + else if ((embeddedSound is Class)) _sound = Type.createInstance(embeddedSound, []); + else if ((embeddedSound is String)) { + if (Assets.exists(embeddedSound, AssetType.MUSIC) && this == FlxG.sound.music) + _sound = Assets.getMusic(embeddedSound); + else if (Assets.exists(embeddedSound, AssetType.SOUND)) + _sound = Assets.getSound(embeddedSound); + else + FlxG.log.error('Could not find a Sound asset with an ID of \'$embeddedSound\'.'); + } + + return init(looped, autoDestroy, onComplete); + } + + /** + * One of the main setup functions for sounds, this function loads a sound from a URL. + * + * @param soundURL A string representing the URL of the MP3 file you want to play. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @param onComplete Called when the sound finished playing + * @param onLoad Called when the sound finished loading. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadStream(soundURL:String, looped = false, autoDestroy = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound { + if (!exists) return this; + cleanup(true); + + _sound = new Sound(); + _sound.addEventListener(Event.ID3, gotID3); + var loadCallback:Event->Void = null; + loadCallback = function(e:Event) + { + (e.target : IEventDispatcher).removeEventListener(e.type, loadCallback); + // Check if the sound was destroyed before calling. Weak ref doesn't guarantee GC. + if (_sound == e.target) + { + _length = _sound.length; + if (onLoad != null) + onLoad(); + } + } + // Use a weak reference so this can be garbage collected if destroyed before loading. + _sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true); + _sound.load(new URLRequest(soundURL)); + + return init(looped, autoDestroy, onComplete); + } + + #if flash11 + /** + * One of the main setup functions for sounds, this function loads a sound from a ByteArray. + * + * @param bytes A ByteArray object. + * @param looped Whether or not this sound should loop endlessly. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function loadByteArray(bytes:ByteArray, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound { + if (!exists) return this; + cleanup(true); + + _sound = new Sound(); + _sound.addEventListener(Event.ID3, gotID3); + _sound.loadCompressedDataFromByteArray(bytes, bytes.length); + + return init(looped, autoDestroy, onComplete); + } + #end + + function init(?looped:Null, ?autoDestroy:Null, ?onComplete:NullVoid>):FlxSound { + if (looped != null) this.looped = looped; + if (autoDestroy != null) this.autoDestroy = autoDestroy; + if (onComplete != null) this.onComplete = onComplete; // DEPRECATED + + if (_sound != null) makeChannel(); + else _length = 0; + + endTime = null; + + return this; + } + + /** + * Call inbetween init function if a sound asset does exists. + * @since raltyMod + */ + function makeChannel() @:privateAccess { + if (_source != null) _source.dispose(); + + if (_channel == null) (_source = (_channel = new SoundChannel(null)).__source = new AudioSource(_sound.__buffer)).gain = 0; + else { + (_source = new AudioSource(_sound.__buffer)).gain = 0; + _channel.__dispose(); + _channel.__source = _source; + SoundMixer.__registerSoundChannel(_channel); + } + if (_transform == null) _transform = new SoundTransform(); + _transform.pan = 0; + _length = _source.length; + + _source.onComplete.add(_channel.source_onComplete); + _source.onLoop.add(_channel.source_onLoop); + _channel.__soundTransform = _transform; + _channel.__isValid = true; + } + + /** + * Call after adjusting the volume to update the sound channel's settings. + */ + @:allow(flixel.sound.FlxSoundGroup) + function updateTransform() { + if (_transform == null) { + _transform = new SoundTransform(); + pan = pan; + } + _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || muted) ? 0 : 1) * FlxG.sound.volume * #end + (group != null ? group.volume : 1) * _volume * _volumeAdjust; + + if (_channel != null) _channel.soundTransform = _transform; + } + + /** + * Call this function if you want this sound's volume to change + * based on distance from a particular FlxObject. + * + * @param X The X position of the sound. + * @param Y The Y position of the sound. + * @param TargetObject The object you want to track. + * @param Radius The maximum distance this sound can travel. + * @param Pan Whether panning should be used in addition to the volume changes. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function proximity(x = 0.0, y = 0.0, ?targetObject:FlxObject, ?radius:Float, pan = true):FlxSound { + setPosition(x, y); + if (targetObject != null) this.target = targetObject; + if (radius != null) this.radius = radius; + proximityPan = pan; + + return this; + } + + /** + * Helper function to set the coordinates of this object. + * Sound positioning is used in conjunction with proximity/panning. + * + * @param x The new x position + * @param y The new y position + */ + public inline function setPosition(x = 0.0, y = 0.0):Void { + this.x = x; + this.y = y; + } + + /** + * Call this function to play the sound - also works on paused sounds. + * + * @param forceRestart Whether to start the sound over or not. + * Default value is false, meaning if the sound is already playing or was + * paused when you call play(), it will continue playing from its current + * position, NOT start again from the beginning. + * @param startTime At which point to start playing the sound, in milliseconds. + * @param endTime At which point to stop playing the sound, in milliseconds. + * If not set / `null`, the sound completes normally. + */ + public function play(forceRestart = false, startTime = 0.0, ?endTime:Null):FlxSound { + if (!exists) return this; + + if (forceRestart) cleanup(false, true); + else if (playing) return this; + + if (endTime != null) this.endTime = endTime; + if (_paused) resume(); + else startSound(startTime); + + return this; + } + + /** + * Unpause a sound. Only works on sounds that have been paused. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function resume():FlxSound { + if (_paused) startSound(_time); + return this; + } + + /** + * Call this function to pause this sound. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function pause():FlxSound { + if (!playing) return this; + + cleanup(false, false); + _paused = true; + return this; + } + + /** + * Call this function to stop this sound. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function stop():FlxSound { + cleanup(autoDestroy, true); + return this; + } + + /** + * Helper function that tweens this sound's volume. + * + * @param duration The amount of time the fade-out operation should take. + * @param to The volume to tween to, 0 by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void):FlxSound { + if (fadeTween != null) fadeTween.cancel(); + fadeTween = FlxTween.num(volume, to, duration, {onComplete: onComplete}, volumeTween); + + return this; + } + + /** + * Helper function that tweens this sound's volume. + * If the sound wasn't playing at all, it'll play before the tween starts. + * + * @param duration The amount of time the fade-in operation should take. + * @param from The volume to tween from, 0 by default. + * @param to The volume to tween to, 1 by default. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void):FlxSound { + if (!playing) play(); + if (fadeTween != null) fadeTween.cancel(); + fadeTween = FlxTween.num(from, to, duration, {onComplete: onComplete}, volumeTween); + + return this; + } + + function volumeTween(f:Float) volume = f; + + /** + * Returns the currently selected "real" volume of the sound (takes fades and proximity into account). + * + * @return The adjusted volume of the sound. + */ + public inline function getActualVolume():Float + return _volume * _volumeAdjust; + + #if FLX_PITCH + /** + * Returns the currently selected "real" pitch of the sound. + * + * @return The adjusted pitch of the sound. + */ + public inline function getActualPitch():Float + return _realPitch; + #end + + /** + * Returns the actual time coming from the internal, can be used for detecting sync error. + * + * @return The actual time of the sound. + */ + public inline function getActualTime():Float { + get_time(); + return _time; + } + + /** + * An internal helper function used to attempt to start playing + * the sound and populate the _channel variable. + */ + var _tries = 0; + function startSound(StartTime:Float) @:privateAccess { + if (_sound == null) return; + + _paused = false; + _time = StartTime; + _lastTime = FlxG.game.getTicks(); + if (_channel == null || !_channel.__isValid || _source.__backend == null || _source.__backend.disposed #if lime_openal || _source.__backend.handle == null #end) + makeChannel(); + + if (_channel != null) { + #if FLX_PITCH + _timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0; + _realPitch = -1.0; + pitch = _pitch; + #end + + // TODO: fix the buffer loud beep for streaming sounds... + #if lime_openal + _source.__backend.setGain(0); + #end + + _channel.soundTransform = _transform; + _channel.__lastPeakTime = 0; + _channel.__leftPeak = 0; + _channel.__rightPeak = 0; + _channel.addEventListener(Event.SOUND_COMPLETE, stopped); + _source.__backend.playing = true; + _source.offset = 0; + _source.currentTime = _time; + + #if lime_openal + if (_source.__backend.handle == null) return destroy(); + + var s = false; + try {s = AL.getSourcei(_source.__backend.handle, AL.SOURCE_STATE) == AL.PLAYING;} catch(e) {} + if (s) updateTransform(); + else { + if (++_tries > 10) return; + makeChannel(); + return startSound(StartTime); + } + + _tries = 0; + #end + + looped = looped; + loopTime = loopTime; + endTime = endTime; + _amplitudeTime = -1; + + active = true; + } + else { + exists = false; + active = false; + } + } + + function stopped(?_) { + onFinish.dispatch(); + + if (onComplete != null) onComplete(); + + if (looped) { + cleanup(false); + play(false, loopTime - _offset, endTime - _offset); + } + else cleanup(autoDestroy); + } + + /** + * Internal event handler for ID3 info (i.e. fetching the song name). + */ + function gotID3(_) { + name = _sound.id3.songName; + artist = _sound.id3.artist; + _sound.removeEventListener(Event.ID3, gotID3); + } + + #if FLX_SOUND_SYSTEM + @:allow(flixel.system.frontEnds.SoundFrontEnd) + function onFocus() if (!_alreadyPaused) { + resume(); + _alreadyPaused = null; + } + + @:allow(flixel.system.frontEnds.SoundFrontEnd) + function onFocusLost() if (_alreadyPaused == null && !(_alreadyPaused = _paused)) pause(); + #end + + function set_group(value:FlxSoundGroup):FlxSoundGroup { + if (value != null) value.add(this); + else group.remove(this); + return group; + } + + inline function get_playing():Bool @:privateAccess + return _channel != null && _channel.__isValid && _source.playing; + + inline function get_volume():Float + return _volume; + + inline function set_volume(Volume:Float):Float { + _volume = FlxMath.bound(Volume, 0, 4); + updateTransform(); + return _volume; + } + + inline function set_muted(Muted:Bool):Bool { + muted = Muted; + updateTransform(); + return muted; + } + + inline function get_loaded():Bool + return buffer != null; + + inline function get_streamed():Bool + return #if lime_vorbis vorbis != null #else false #end; + + inline function get_buffer():AudioBuffer + @:privateAccess return _sound != null ? _sound.__buffer : null; + + #if lime_vorbis + inline function get_vorbis():VorbisFile + @:privateAccess return _sound != null ? _sound.__buffer.__srcVorbisFile : null; + #end + + function update_amplitude():Void @:privateAccess { + if (_channel == null || _time == _amplitudeTime || !_amplitudeUpdate) return; + _channel.__updatePeaks(); + + _amplitudeUpdate = false; + _amplitudeLeft = _channel.__leftPeak; + _amplitudeRight = _channel.__rightPeak; + _amplitudeTime = _time; + } + + inline function get_amplitudeLeft():Float { + update_amplitude(); + return _amplitudeLeft; + } + + inline function get_amplitudeRight():Float { + update_amplitude(); + return _amplitudeRight; + } + + inline function get_amplitude():Float { + update_amplitude(); + return if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft; + } + + inline function get_channels():Int + @:privateAccess return (buffer != null) ? buffer.channels : 0; + + inline function get_stereo():Bool + return channels > 1; + + #if FLX_PITCH + inline function get_pitch():Float + return _pitch; + + function set_pitch(v:Float):Float { + var adjusted:Float = FlxMath.bound(v * _timeScaleAdjust, 0); + if (_channel != null && _realPitch != adjusted) { + if ((_channel.pitch = adjusted) > 0 && _realPitch <= 0) { + _realPitch = adjusted; + time = _time; + } + else { + get_time(); + _realPitch = adjusted; + } + } + return _pitch = FlxMath.bound(v, 0); + } + #end + + function set_looped(v:Bool):Bool { + if (playing) { + if (v) _channel.loops = 999; + else _channel.loops = 0; + } + return looped = v; + } + + function set_loopTime(v:Float):Float { + if (playing) _channel.loopTime = v; + return loopTime = v; + } + + function set_endTime(v:Null):Null { + if (playing) { + if (v != null && v > 0 && v < length) _channel.endTime = v; + else _channel.endTime = null; + } + return endTime = v; + } + + inline function get_pan():Float return _transform.pan; + inline function set_pan(pan:Float):Float return _transform.pan = pan; + + inline function getFakeTime():Float { + if (@:privateAccess _channel.__isValid && _source.playing && _realPitch > 0 && _lastTime != null) + return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation; + else + return _time; + } + function get_time():Float { + if (_channel == null) return _time; + + final pos = _channel.position - _offset; + if (!playing || _realPitch <= 0) { + _lastTime = null; + return _time = pos; + } + + final fakeTime = getFakeTime(); + if (pos != _time) { + _lastTime = FlxG.game.getTicks(); + if ((_timeInterpolation = 1 - Math.min(fakeTime - pos, 1000) * 0.001) < 1 && _timeInterpolation > .9) + return _time = fakeTime; + else { + _timeInterpolation = 1; + return _time = pos; + } + } + else + return fakeTime; + } + + function set_time(time:Float):Float @:privateAccess { + time = FlxMath.bound(time, 0, length - 1); + if (_channel != null && _realPitch > 0) { + if (!_channel.__isValid) { + cleanup(false, true); + startSound(time); + } + else if (playing) { + _source.offset = 0; + _source.currentTime = time + _offset; + } + } + + _lastTime = null; + return _time = time; + } + + function get_offset():Float return _offset; + function set_offset(offset:Float):Float { + if (offset == _offset) return offset; + _offset = offset; + time = time; + return offset; + } + + inline function get_length():Float return _length - _offset; + + /*function get_latency():Float { + if (_channel != null) return _source.latency; + return 0; + }*/ + + override function toString():String { + return FlxStringUtil.getDebugString([ + LabelValuePair.weak("playing", playing), + LabelValuePair.weak("time", _time), + LabelValuePair.weak("length", length), + LabelValuePair.weak("volume", volume), + LabelValuePair.weak("pitch", pitch) + ]); + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx index aaf47774..7ffcf4d8 100644 --- a/source/funkin/backend/system/Conductor.hx +++ b/source/funkin/backend/system/Conductor.hx @@ -160,11 +160,11 @@ class Conductor private static function __updateSongPos(elapsed:Float) { if (FlxG.sound.music == null || !FlxG.sound.music.playing) { - lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.time - songOffset : -songOffset; + lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.getActualTime() - songOffset : -songOffset; return; } - if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) { + if (lastSongPos != (lastSongPos = FlxG.sound.music.getActualTime() - songOffset)) { // update conductor songPosition = lastSongPos; } else { diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index b72003c0..f88ef235 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -812,10 +812,12 @@ class CoolUtil * @param music Music */ public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) { - if (frontEnd.music != null) - @:privateAccess frontEnd.destroySound(frontEnd.music); + if (frontEnd.music == music) return; + + if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music); frontEnd.list.remove(music); - frontEnd.music = music; + frontEnd.defaultMusicGroup.add(frontEnd.music = music); + music.persist = true; } /** diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index f5f6bc5b..d81c9444 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -509,7 +509,7 @@ class PlayState extends MusicBeatState @:noCompletion @:dox(hide) private var _endSongCalled:Bool = false; @:dox(hide) - var __vocalOffsetViolation:Float = 0; + var __vocalSyncTimer:Float = 0; private function get_accuracy():Float { if (accuracyPressedNotes <= 0) return -1; @@ -999,21 +999,11 @@ class PlayState extends MusicBeatState inst.onComplete = endSong; - if (!paused) { - FlxG.sound.setMusic(inst); - FlxG.sound.music.play(); - } - vocals.play(); + var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; + inst.time = vocals.time = time; - vocals.pause(); - inst.pause(); for (strumLine in strumLines.members) { - strumLine.vocals.play(); - strumLine.vocals.pause(); - } - inst.time = vocals.time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; - for (strumLine in strumLines.members) { - strumLine.vocals.time = vocals.time; + strumLine.vocals.time = time; strumLine.vocals.play(); } vocals.play(); @@ -1028,15 +1018,14 @@ class PlayState extends MusicBeatState scripts.call("destroy"); for(g in __cachedGraphics) g.useCount--; - @:privateAccess - for (strumLine in strumLines.members) - FlxG.sound.destroySound(strumLine.vocals); - super.destroy(); - scripts = FlxDestroyUtil.destroy(scripts); @:privateAccess { - FlxG.sound.destroySound(inst); + for (strumLine in strumLines.members) FlxG.sound.destroySound(strumLine.vocals); + if (FlxG.sound.music != inst) FlxG.sound.destroySound(inst); FlxG.sound.destroySound(vocals); } + scripts = FlxDestroyUtil.destroy(scripts); + + super.destroy(); WindowUtils.resetAffixes(); SaveWarning.reset(); @@ -1079,15 +1068,14 @@ class PlayState extends MusicBeatState curSong = songData.meta.name.toLowerCase(); curSongID = curSong.replace(" ", "-"); - inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty)); + FlxG.sound.setMusic(inst = FlxG.sound.load(Assets.getMusic(Paths.inst(SONG.meta.name, difficulty)))); if (SONG.meta.needsVoices != false && Assets.exists(Paths.voices(SONG.meta.name, difficulty))) // null or true vocals = FlxG.sound.load(Paths.voices(SONG.meta.name, difficulty)); else vocals = new FlxSound(); - inst.group = FlxG.sound.defaultMusicGroup; - vocals.group = FlxG.sound.defaultMusicGroup; - inst.persist = vocals.persist = false; + vocals.group = FlxG.sound.defaultMusicGroup; + vocals.persist = false; generatedMusic = true; } @@ -1191,14 +1179,15 @@ class PlayState extends MusicBeatState vocals.pause(); for (strumLine in strumLines.members) strumLine.vocals.pause(); - FlxG.sound.music.play(); - Conductor.songPosition = FlxG.sound.music.time; - vocals.time = Conductor.songPosition + Conductor.songOffset; + var time = Conductor.songPosition + Conductor.songOffset; + inst.time = vocals.time = time; for (strumLine in strumLines.members) { - strumLine.vocals.time = vocals.time; + strumLine.vocals.time = time; strumLine.vocals.play(); } vocals.play(); + inst.play(); + gameAndCharsCall("onVocalsResync"); } @@ -1333,22 +1322,19 @@ class PlayState extends MusicBeatState if (Conductor.songPosition >= 0) startSong(); } - } else if (FlxG.sound.music != null) { - var instTime = FlxG.sound.music.time; - var isOffsync:Bool = vocals.time != instTime; - if(!isOffsync) { - for(strumLine in strumLines.members) { - if(strumLine.vocals.time != instTime) { - isOffsync = true; - break; - } + } + else if (FlxG.sound.music != null && (__vocalSyncTimer -= elapsed) < 0) { + __vocalSyncTimer = 1; + + var instTime = FlxG.sound.music.getActualTime(); + var isOffsync:Bool = vocals.loaded && Math.abs(instTime - vocals.getActualTime()) > 30; + if (!isOffsync) { + for (strumLine in strumLines.members) { + if ((isOffsync = strumLine.vocals.loaded && Math.abs(instTime - strumLine.vocals.getActualTime()) > 30)) break; } } - __vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (isOffsync ? elapsed : -elapsed / 2)); - if (__vocalOffsetViolation > Flags.VOCAL_OFFSET_VIOLATION_THRESHOLD) { - resyncVocals(); - __vocalOffsetViolation = 0; - } + + if (isOffsync) resyncVocals(); } while(events.length > 0 && events.last().time <= Conductor.songPosition) diff --git a/source/funkin/menus/PauseSubState.hx b/source/funkin/menus/PauseSubState.hx index e8ad62af..47b1a9a6 100644 --- a/source/funkin/menus/PauseSubState.hx +++ b/source/funkin/menus/PauseSubState.hx @@ -65,7 +65,7 @@ class PauseSubState extends MusicBeatSubstate menuItems = event.options; - pauseMusic = FlxG.sound.load(Paths.music(event.music), 0, true); + pauseMusic = FlxG.sound.load(Assets.getMusic(Paths.music(event.music)), 0, true); pauseMusic.persist = false; pauseMusic.group = FlxG.sound.defaultMusicGroup; pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2))); diff --git a/source/lime/_internal/backend/html5/HTML5AudioSource.hx b/source/lime/_internal/backend/html5/HTML5AudioSource.hx index bfca4198..7fd31c4e 100644 --- a/source/lime/_internal/backend/html5/HTML5AudioSource.hx +++ b/source/lime/_internal/backend/html5/HTML5AudioSource.hx @@ -2,6 +2,7 @@ package lime._internal.backend.html5; import lime.math.Vector4; import lime.media.AudioSource; +import lime.media.AudioManager; @:access(lime.media.AudioBuffer) class HTML5AudioSource @@ -9,7 +10,7 @@ class HTML5AudioSource private var completed:Bool; private var gain:Float; private var id:Int; - private var length:Float; + private var length:Null; private var loops:Int; private var parent:AudioSource; private var playing:Bool; @@ -24,7 +25,9 @@ class HTML5AudioSource position = new Vector4(); } - public function dispose():Void {} + public function dispose():Void { + stop(); + } public function init():Void {} @@ -98,8 +101,9 @@ class HTML5AudioSource { loops--; stop(); - // currentTime = 0; + if (loopTime != null && loopTime > 0) setCurrentTime(loopTime); play(); + parent.onLoop.dispatch(); return; } else if (parent.buffer != null && parent.buffer.__srcHowl != null) @@ -128,7 +132,7 @@ class HTML5AudioSource } else if (parent.buffer != null && parent.buffer.__srcHowl != null) { - var time = (parent.buffer.__srcHowl.seek(id) * 1000) - parent.offset; + var time = parent.buffer.__srcHowl.seek(id) * 1000.0 - parent.offset; if (time < 0) return 0; return time; } @@ -137,6 +141,20 @@ class HTML5AudioSource return 0; } + public function getLatency():Float + { + var ctx = AudioManager.context.web; + if (ctx != null) + { + var baseLatency:Float = untyped ctx.baseLatency != null ? untyped ctx.baseLatency : 0; + var outputLatency:Float = untyped ctx.outputLatency != null ? untyped ctx.outputLatency : 0; + + return (baseLatency + outputLatency) * 1000; + } + + return 0; + } + public function setCurrentTime(value:Float):Float { #if lime_howlerjs @@ -172,7 +190,7 @@ class HTML5AudioSource return gain = value; } - public function getLength():Float + public function getLength():Null { if (length != 0) { @@ -182,14 +200,14 @@ class HTML5AudioSource #if lime_howlerjs if (parent.buffer != null && parent.buffer.__srcHowl != null) { - return parent.buffer.__srcHowl.duration() * 1000; + return parent.buffer.__srcHowl.duration() * 1000.0; } #end return 0; } - public function setLength(value:Float):Float + public function setLength(value:Null):Null { return length = value; } @@ -204,6 +222,14 @@ class HTML5AudioSource return loops = value; } + public function getLoopTime():Float { + return loopTime; + } + + public function setLoopTime(value:Float):Float { + return loopTime = value; + } + public function getPitch():Float { #if lime_howlerjs @@ -218,9 +244,11 @@ class HTML5AudioSource #if lime_howlerjs parent.buffer.__srcHowl.rate(value); #end + return getPitch(); } + public function getPosition():Vector4 { #if lime_howlerjs @@ -250,4 +278,4 @@ class HTML5AudioSource return position; } -} \ No newline at end of file +} diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index e7cbd821..dcd84632 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,236 +1,193 @@ package lime._internal.backend.native; -import haxe.Int64; import haxe.Timer; -import lime.math.Vector4; +import haxe.Int64; + import lime.media.openal.AL; import lime.media.openal.ALBuffer; import lime.media.openal.ALSource; import lime.media.vorbis.VorbisFile; -import lime.media.AudioManager; + +import lime.math.Vector4; +import lime.media.AudioBuffer; import lime.media.AudioSource; -import lime.utils.UInt8Array; +import lime.utils.ArrayBufferView; #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end @:access(lime.media.AudioBuffer) -class NativeAudioSource -{ +@:access(lime.utils.ArrayBufferView) +@:access(lime.media.vorbis.VorbisFile) +class NativeAudioSource { public static var initBuffers:Array = []; - private static var STREAM_BUFFER_SIZE = 48000; - #if (native_audio_buffers && !macro) - private static var STREAM_NUM_BUFFERS = Std.parseInt(haxe.macro.Compiler.getDefine("native_audio_buffers")); - #else - private static var STREAM_NUM_BUFFERS = 3; - #end - private static var STREAM_TIMER_FREQUENCY = 100; + private static final STREAM_BUFFER_SIZE:Int = 0x8000; + private static final STREAM_NUM_BUFFERS:Int = 5; + private static final STREAM_TIMER_FREQUENCY:Int = 100; + + //#if (lime >= "8.3.0") + //private static var hasALSoftLatencyExt:Null; + //#end private var buffers:Array; + private var bufferDatas:Array; private var bufferTimeBlocks:Array; - private var completed:Bool; - private var dataLength:Int; - private var format:Int; - private var handle:ALSource; + private var bufferLoops:Int; + private var queuedBuffers:Int; + private var requestBuffers:Int; + private var length:Null; - private var loops:Int; - private var parent:AudioSource; + private var loopTime:Null; private var playing:Bool; + private var loops:Int; private var position:Vector4; - private var samples:Float; + + private var bitsPerSample:Int; + private var format:Int; + private var dataLength:Int64; + private var samples:Int64; + private var completed:Bool; private var stream:Bool; - private var streamTimer:Timer; + + private var handle:ALSource; + private var parent:AudioSource; private var timer:Timer; + private var streamTimer:Timer; + private var disposed:Bool; + private var safeEnd:Bool; - private var pcmTotal:Int64; - private var sampleRate:Int; - - var disposed:Bool = false; - - public function new(parent:AudioSource) - { + public function new(parent:AudioSource) { this.parent = parent; - position = new Vector4(); } - public function dispose():Void - { - if (handle != null) - { - stop(); + public function dispose():Void { + disposed = true; + stop(); + + if (handle != null) { AL.sourcei(handle, AL.BUFFER, null); AL.deleteSource(handle); - if (buffers != null) - { - for (buffer in buffers) - { - AL.deleteBuffer(buffer); - } - buffers = null; - } - handle = null; } - disposed = true; + handle = null; + + if (buffers != null) AL.deleteBuffers(buffers); + buffers = null; + initBuffers.remove(this); } - public function init():Void - { - dataLength = 0; - format = 0; + public function init():Void { + //#if (lime >= "8.3.0") + //if (hasALSoftLatencyExt == null) hasALSoftLatencyExt = AL.isExtensionPresent("AL_SOFT_source_latency"); + //#end - if (parent.buffer.channels == 1) - { - if (parent.buffer.bitsPerSample == 8) - { - format = AL.FORMAT_MONO8; - } - else if (parent.buffer.bitsPerSample == 16) - { - format = AL.FORMAT_MONO16; - } - } - else if (parent.buffer.channels == 2) - { - if (parent.buffer.bitsPerSample == 8) - { - format = AL.FORMAT_STEREO8; - } - else if (parent.buffer.bitsPerSample == 16) - { - format = AL.FORMAT_STEREO16; - } - } + if (handle != null) return; - if (parent.buffer.__srcVorbisFile != null) - { - stream = true; + var buffer = parent.buffer; + if (disposed = (handle = AL.createSource()) == null) return; - var vorbisFile = parent.buffer.__srcVorbisFile; - pcmTotal = vorbisFile.pcmTotal(); - var info = vorbisFile.info(); - sampleRate = info.rate; - var _dataLength = pcmTotal * Int64.ofInt(parent.buffer.channels) * (Int64.ofInt(parent.buffer.bitsPerSample) / Int64.ofInt(8)); - dataLength = Int64.toInt(_dataLength); + AL.sourcef(handle, AL.MAX_GAIN, 10); + bufferLoops = 0; - buffers = new Array(); - bufferTimeBlocks = new Array(); + bitsPerSample = buffer.bitsPerSample; + if (buffer.channels > 1) format = bitsPerSample == 8 ? AL.FORMAT_STEREO8 : AL.FORMAT_STEREO16; + else format = bitsPerSample == 8 ? AL.FORMAT_MONO8 : AL.FORMAT_MONO16; - for (i in 0...STREAM_NUM_BUFFERS) - { + var vorbisFile = buffer.__srcVorbisFile; + if (stream = vorbisFile != null) { + dataLength = (samples = vorbisFile.pcmTotal()) * buffer.channels * (Int64.ofInt(bitsPerSample) / 8); + buffers = []; + bufferDatas = []; + bufferTimeBlocks = []; + + var constructor = bitsPerSample == 8 ? Int8 : Int16; + for (i in 0...STREAM_NUM_BUFFERS) { buffers.push(AL.createBuffer()); + bufferDatas.push(new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)); bufferTimeBlocks.push(0); } - - handle = AL.createSource(); - - samples = Int64.toInt(pcmTotal); } - else - { - if (parent.buffer.__srcBuffer == null) - { - parent.buffer.__srcBuffer = AL.createBuffer(); + else { + samples = ((dataLength = buffer.data.length) * 8) / (buffer.channels * bitsPerSample); - if (parent.buffer.__srcBuffer != null) - { - AL.bufferData(parent.buffer.__srcBuffer, format, parent.buffer.data, parent.buffer.data.length, parent.buffer.sampleRate); - } - } + if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null) + AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate); - dataLength = parent.buffer.data.length; - - handle = AL.createSource(); - - if (handle != null) - { - AL.sourcei(handle, AL.BUFFER, parent.buffer.__srcBuffer); - } - - samples = Int64.toInt((Int64.make(0, dataLength) * Int64.ofInt(8)) / Int64.ofInt(parent.buffer.channels * parent.buffer.bitsPerSample)); + AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer); } initBuffers.push(this); + + if (dataLength == 0) { + trace('NativeAudioSource Bug! dataLength is 0'); + dispose(); + } } - public function play():Void - { - if (disposed) { - trace("TRIED TO PLAY DISPOSED?? WTFFF"); - trace(haxe.CallStack.toString(haxe.CallStack.callStack())); - } - if (playing || handle == null) - { - return; - } + public function play():Void { + if (playing || disposed) return; playing = true; - - if (stream) - { - setCurrentTime(getCurrentTime()); - - streamTimer = new Timer(STREAM_TIMER_FREQUENCY); - streamTimer.run = streamTimer_onRun; - } - else - { - var time = completed ? 0 : getCurrentTime(); - - setCurrentTime(time); - } + if (completed) setCurrentTime(0); + else setCurrentTime(getCurrentTime()); } - public function pause():Void - { + public function pause():Void { + if (!(disposed = handle == null)) AL.sourcePause(handle); + playing = false; - - if (handle == null) return; - AL.sourcePause(handle); - - if (streamTimer != null) - { - streamTimer.stop(); - } - - if (timer != null) - { - timer.stop(); - } + stopStreamTimer(); + stopTimer(); } - private function readVorbisFileBuffer(vorbisFile:VorbisFile, length:Int):UInt8Array - { - #if lime_vorbis - var buffer = new UInt8Array(length); - var read = 0, total = 0, readMax; - - for (i in 0...STREAM_NUM_BUFFERS-1) - { - bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; + public function stop():Void { + if (!(disposed = handle == null)) { + if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.STOPPED) AL.sourceStop(handle); + if (stream) AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED) + AL.getSourcei(handle, AL.BUFFERS_PROCESSED)); } - bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell(); - while (total < length) - { - readMax = 4096; + requestBuffers = queuedBuffers = bufferLoops = 0; + playing = false; + stopStreamTimer(); + stopTimer(); + } - if (readMax > length - total) - { - readMax = length - total; + private function complete():Void { + stop(); + + completed = true; + parent.onComplete.dispatch(); + } + + private function readVorbisFileBuffer(vorbisFile:VorbisFile, max:Int):ArrayBufferView { + #if lime_vorbis + var id = STREAM_NUM_BUFFERS - requestBuffers, read = STREAM_NUM_BUFFERS - 1, total = 0, readMax; + var buffer = bufferDatas[id]; + + for (i in id...read) { + bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; + bufferDatas[i] = bufferDatas[i + 1]; + } + bufferTimeBlocks[read] = vorbisFile.timeTell(); + bufferDatas[read] = buffer; + queuedBuffers = requestBuffers; + + while (total < STREAM_BUFFER_SIZE) { + if ((readMax = 4096) > (read = max - total)) readMax = read; + if (vorbisFile.handle == null) break; + if (readMax > 0 && (read = vorbisFile.read(buffer.buffer, total, readMax)) > 0) total += read; + else if (safeEnd = (loops > bufferLoops)) { + if (readMax == 4096) continue; + bufferLoops++; vorbisFile.timeSeek((loopTime != null ? Math.max(0, loopTime / 1000) : 0) + parent.offset / 1000); + if ((max = (dataLength - (vorbisFile.pcmTell() * (Int64.ofInt(bitsPerSample) / 8) * parent.buffer.channels)).low) > STREAM_BUFFER_SIZE) + max = STREAM_BUFFER_SIZE; } - - read = vorbisFile.read(buffer.buffer, total, readMax); - - if (read > 0) - { - total += read; - } - else - { + else { + buffer.buffer.fill(total, STREAM_BUFFER_SIZE - total - 1, 0); + resetTimer((getLength() - getCurrentTime()) / getPitch()); break; } } @@ -241,344 +198,281 @@ class NativeAudioSource #end } - private function refillBuffers(buffers:Array = null):Void - { + private function fillBuffers(buffers:Array):Void { #if lime_vorbis - var vorbisFile = null; - var position = 0; + if (parent == null || parent.buffer == null) return dispose(); + if (handle == null || buffers.length < 1) return; - if (buffers == null) - { - var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + final buffer = parent.buffer, vorbisFile = buffer.__srcVorbisFile; + final actualDataRate = (Int64.ofInt(bitsPerSample) / 8) * buffer.channels; + var position = vorbisFile.pcmTell() * actualDataRate, length = getLengthSamples() * actualDataRate, trackLoops = bufferLoops; + if (position >= length && safeEnd) return; - if (buffersProcessed > 0) - { - vorbisFile = parent.buffer.__srcVorbisFile; - position = Int64.toInt(vorbisFile.pcmTell()); + var sampleRate = buffer.sampleRate, numBuffers = 0, data, size:Int64; + for (buffer in buffers) { + if ((size = length - position) > STREAM_BUFFER_SIZE) size = Int64.ofInt(STREAM_BUFFER_SIZE); + data = readVorbisFileBuffer(vorbisFile, Int64.toInt(size)); - if (position < dataLength) - { - buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed); - } + if (disposed) return; + AL.bufferData(buffer, format, data, STREAM_BUFFER_SIZE, sampleRate); + numBuffers++; + + if (safeEnd) break; + else if (bufferLoops != trackLoops) { + position = vorbisFile.pcmTell() * actualDataRate; + trackLoops = bufferLoops; } } + AL.sourceQueueBuffers(handle, numBuffers, buffers); - if (buffers != null) - { - if (vorbisFile == null) - { - vorbisFile = parent.buffer.__srcVorbisFile; - position = Int64.toInt(vorbisFile.pcmTell()); - } - - var numBuffers = 0; - var data; - - for (buffer in buffers) - { - if (dataLength - position >= STREAM_BUFFER_SIZE) - { - data = readVorbisFileBuffer(vorbisFile, STREAM_BUFFER_SIZE); - AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); - position += STREAM_BUFFER_SIZE; - numBuffers++; - } - else if (position < dataLength) - { - data = readVorbisFileBuffer(vorbisFile, dataLength - position); - AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); - numBuffers++; - break; - } - } - - AL.sourceQueueBuffers(handle, numBuffers, buffers); - - // OpenAL can unexpectedly stop playback if the buffers run out - // of data, which typically happens if an operation (such as - // resizing a window) freezes the main thread. - // If AL is supposed to be playing but isn't, restart it here. - if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) - { - AL.sourcePlay(handle); + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + if (bufferLoops > 0) { // TODO: remove this once buffer loop stutters fixed + setCurrentTime(getCurrentTime()); + return; } + AL.sourcePlay(handle); + resetTimer(Std.int((getLength() - getCurrentTime()) / getPitch())); } #end } - public function stop():Void - { - if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) - { - AL.sourceStop(handle); + private function streamRun():Void { + #if lime_vorbis + if (disposed = (handle == null || buffers == null)) return dispose(); + + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile == null) return dispose(); + + try { + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + if (processed > 0 && playing) { + fillBuffers(AL.sourceUnqueueBuffers(handle, processed)); + if ((!safeEnd || loops > 0) && queuedBuffers < STREAM_NUM_BUFFERS && playing) { + /*if (requestBuffers >= STREAM_NUM_BUFFERS) { + AL.sourceUnqueueBuffers(handle, queuedBuffers); + setCurrentTime((bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers] + + (STREAM_BUFFER_SIZE / ((bitsPerSample / 8) * parent.buffer.channels) / parent.buffer.sampleRate) + ) * 1000 - parent.offset); + } + else*/fillBuffers([buffers[++requestBuffers - 1]]); + } + } } - - playing = false; - - if (streamTimer != null) - { - streamTimer.stop(); - } - - if (timer != null) - { - timer.stop(); - } - - setCurrentTime(0); + catch(e) trace(e); + #end } - // Event Handlers - private function streamTimer_onRun():Void - { - refillBuffers(); + // Timers + inline function stopStreamTimer():Void if (streamTimer != null) streamTimer.stop(); + + private function resetStreamTimer():Void { + stopStreamTimer(); + + #if lime_vorbis + streamTimer = new Timer(STREAM_TIMER_FREQUENCY); + streamTimer.run = streamRun; + #end } - private function timer_onRun():Void - { - if (loops > 0) - { - playing = false; - loops--; - setCurrentTime(0); - play(); + inline function stopTimer():Void if (timer != null) timer.stop(); + + private function resetTimer(timeRemaining:Float):Void { + stopTimer(); + + /*if (timeRemaining <= 26) { + timer_onRun(); + return; + }*/ + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + + private function timer_onRun():Void { + if (!safeEnd && bufferLoops <= 0) { + #if lime_vorbis + var ranOut = false; + if (stream) { + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile == null) return dispose(); + ranOut = vorbisFile.pcmTell() >= getLengthSamples() || queuedBuffers < 3; + } + + if (!ranOut) + #end + { + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); + if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { + resetTimer(timeRemaining); + return; + } + } + } + safeEnd = false; + + if (loops <= 0) { + complete(); return; } - else - { - stop(); - } - completed = true; - parent.onComplete.dispatch(); + if (bufferLoops <= 0) { + loops--; + var loopTime = loopTime ?? 0; + if (stream || loopTime > 0) setCurrentTime(loopTime); + else if (!stream) { + setLoops(loops); + if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) + setCurrentTime(0); + } + } + else { + loops -= bufferLoops; + bufferLoops = 0; + } + parent.onLoop.dispatch(); } // Get & Set Methods - public function getCurrentTime():Float - { - if (completed) - { - return getLength(); - } - else if (handle != null) - { - if (stream) - { - var time = (Std.int(bufferTimeBlocks[0] * 1000) + Std.int(AL.getSourcef(handle, AL.SEC_OFFSET) * 1000)) - parent.offset; - if (time < 0) return 0; - return time; - } - else - { - var offset:Int = AL.getSourcei(handle, AL.BYTE_OFFSET); - var ratio = (offset / dataLength); - var totalSeconds = samples / parent.buffer.sampleRate; - - var time = totalSeconds * ratio * 1000 - parent.offset; - - // var time = Std.int (AL.getSourcef (handle, AL.SEC_OFFSET) * 1000) - parent.offset; - if (time < 0) return 0; - return time; + public function getCurrentTime():Float { + if (completed) return getLength(); + else if (!disposed) { + var time = AL.getSourcef(handle, AL.SEC_OFFSET); + if (stream) time += bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers]; + time = time * 1000 - parent.offset; + if (loops > 0 && time > getLength()) { + var start = loopTime != null ? Math.max(0, loopTime + parent.offset) : parent.offset; + return ((time - start) % (getLength() - start)) + start; } + if (time > 0) return time; } - return 0; } - public function setCurrentTime(value:Float):Float - { - // `setCurrentTime()` has side effects and is never safe to skip. - /* if (value == getCurrentTime()) - { - return value; - } */ + public function setCurrentTime(value:Float):Float { + if (disposed = (handle == null)) return value; - if (handle != null) - { - if (stream) - { - AL.sourceStop(handle); + var total = getRealLength(); + var time = Math.max(0, Math.min(total, value + parent.offset)), ratio = time / total; - parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000); - AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS); - refillBuffers(buffers); + if (stream) { + // TODO: smooth setCurrentTime for stream (dont refill buffers again) - if (playing) AL.sourcePlay(handle); - } - else if (parent.buffer != null) - { - AL.sourceRewind(handle); - - // AL.sourcef (handle, AL.SEC_OFFSET, (value + parent.offset) / 1000); - - var secondOffset = (value + parent.offset) / 1000; - var totalSeconds = samples / parent.buffer.sampleRate; - - if (secondOffset < 0) secondOffset = 0; - else if (secondOffset > totalSeconds) secondOffset = totalSeconds; - - var ratio = (secondOffset / totalSeconds); - var totalOffset = Std.int(dataLength * ratio); - - AL.sourcei(handle, AL.BYTE_OFFSET, totalOffset); - if (playing) AL.sourcePlay(handle); + AL.sourceStop(handle); + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + + #if lime_vorbis + var vorbisFile = parent.buffer.__srcVorbisFile; + if (vorbisFile != null) { + // var chunk = Std.int(Math.floor(getFloat(samples) * ratio / STREAM_BUFFER_SIZE) * STREAM_BUFFER_SIZE); + vorbisFile.pcmSeek(Int64.fromFloat(getFloat(samples) * ratio)); + fillBuffers(buffers.slice(0, requestBuffers = queuedBuffers = 3)); + // AL.sourcei(handle, AL.SAMPLE_OFFSET, Std.int((samples * ratio) - chunk)); + if (playing) resetStreamTimer(); } + #end + } + else { + AL.sourceRewind(handle); + AL.sourcei(handle, AL.BYTE_OFFSET, Int64.fromFloat(getFloat(dataLength) * ratio)); } - if (playing) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (getLength() - value) / getPitch(); - - if (timeRemaining > 0) - { - completed = false; - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } - else - { - playing = false; - completed = true; + if (playing) { + var timeRemaining = (getLength() - time) / getPitch(); + if (completed = timeRemaining <= 0) complete(); + else { + AL.sourcePlay(handle); + resetTimer(timeRemaining); } } return value; } - public function getGain():Float - { - if (handle != null) - { - return AL.getSourcef(handle, AL.GAIN); - } - else - { - return 1; - } + inline private function getLengthSamples():Int64 { + return if (length == null) samples; + else Int64.fromFloat((length + parent.offset) / 1000 * parent.buffer.sampleRate); + } + + inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >>> 0); + + inline private function getRealLength():Float return getFloat(samples) / parent.buffer.sampleRate * 1000; + + public function getLength():Null { + return if (length == null) getRealLength() - parent.offset; + else length - parent.offset; } - public function setGain(value:Float):Float - { - if (handle != null) - { - AL.sourcef(handle, AL.GAIN, value); - } + public function setLength(value:Null):Null { + if (value == length) return value; + length = value; + if (playing) { + var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); + if (timeRemaining > 0) resetTimer(timeRemaining); + } return value; } - public function getLength():Float - { - if (length != null) - { - return length; + public function getPitch():Float { + return if (disposed) 1; + else AL.getSourcef(handle, AL.PITCH); + } + + public function setPitch(value:Float):Float { + if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value; + if (playing) { + var timeRemaining = (getLength() - getCurrentTime()) / value; + if (timeRemaining > 0) resetTimer(timeRemaining); } - - return (samples / parent.buffer.sampleRate * 1000) - parent.offset; + AL.sourcef(handle, AL.PITCH, value); + return value; } - public function setLength(value:Float):Float - { - if (playing && length != value) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (value - getCurrentTime()) / getPitch(); - - if (timeRemaining > 0) - { - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } - } - - return length = value; + public function getGain():Float { + if (disposed) return 1; + return AL.getSourcef(handle, AL.GAIN); } - public function getLoops():Int - { - return loops; + public function setGain(value:Float):Float { + if (!disposed) AL.sourcef(handle, AL.GAIN, value); + return value; } - public function setLoops(value:Int):Int - { + inline public function getLoops():Int return loops; + + inline public function setLoops(value:Int):Int { + if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE); return loops = value; } - public function getPitch():Float - { - if (handle != null) - { - return AL.getSourcef(handle, AL.PITCH); - } - else - { - return 1; - } + inline public function getLoopTime():Float return loopTime; + + inline public function setLoopTime(value:Float):Float { + if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE); + return loopTime = value; } - public function setPitch(value:Float):Float - { - if (playing && value != getPitch()) - { - if (timer != null) - { - timer.stop(); - } - - var timeRemaining = (getLength() - getCurrentTime()) / value; - - if (timeRemaining > 0) - { - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } + public function getLatency():Float { + /*#if (lime >= "8.3.0") + if (hasALSoftLatencyExt) { + final offsets = AL.getSourcedvSOFT(handle, AL.SEC_OFFSET_LATENCY_SOFT, 2); + if (offsets != null) return offsets[1] * 1000; } - - if (handle != null) - { - AL.sourcef(handle, AL.PITCH, value); - } - - return value; + #end*/ + return 0; } - public function getPosition():Vector4 - { - if (handle != null) - { - #if !emscripten - var value = AL.getSource3f(handle, AL.POSITION); - position.x = value[0]; - position.y = value[1]; - position.z = value[2]; - #end - } + public function getPosition():Vector4 return position; - return position; - } - - public function setPosition(value:Vector4):Vector4 - { + public function setPosition(value:Vector4):Vector4 { position.x = value.x; position.y = value.y; position.z = value.z; position.w = value.w; - if (handle != null) - { + if (!disposed) { AL.distanceModel(AL.NONE); AL.source3f(handle, AL.POSITION, position.x, position.y, position.z); } - return position; } -} +} \ No newline at end of file diff --git a/source/lime/media/AudioBuffer.hx b/source/lime/media/AudioBuffer.hx new file mode 100644 index 00000000..87215835 --- /dev/null +++ b/source/lime/media/AudioBuffer.hx @@ -0,0 +1,494 @@ +package lime.media; + +import haxe.io.Bytes; +import haxe.io.Path; +import lime._internal.backend.native.NativeCFFI; +import lime._internal.format.Base64; +import lime.app.Future; +import lime.app.Promise; +import lime.media.openal.AL; +import lime.media.openal.ALBuffer; +import lime.media.vorbis.VorbisFile; +import lime.net.HTTPRequest; +import lime.utils.Log; +import lime.utils.UInt8Array; +#if lime_howlerjs +import lime.media.howlerjs.Howl; +#end +#if (js && html5) +import js.html.Audio; +#elseif flash +import flash.media.Sound; +import flash.net.URLRequest; +#end + +@:access(lime._internal.backend.native.NativeCFFI) +@:access(lime.utils.Assets) +#if hl +@:keep +#end +#if !lime_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end + +/** + The `AudioBuffer` class represents a buffer of audio data that can be played back using an `AudioSource`. + It supports a variety of audio formats and platforms, providing a consistent API for loading and managing audio data. + + Depending on the platform, the audio backend may differ, but the class provides a unified interface for accessing + audio data, whether it's stored in memory, loaded from a file, or streamed. + + @see lime.media.AudioSource +**/ +class AudioBuffer +{ + /** + The number of bits per sample in the audio data. + **/ + public var bitsPerSample:Int; + + /** + The number of audio channels (e.g., 1 for mono, 2 for stereo). + **/ + public var channels:Int; + + /** + The raw audio data stored as a `UInt8Array`. + **/ + public var data:UInt8Array; + + /** + The sample rate of the audio data, in Hz. + **/ + public var sampleRate:Int; + + /** + The source of the audio data. This can be an `Audio`, `Sound`, `Howl`, or other platform-specific object. + **/ + public var src(get, set):Dynamic; + + @:noCompletion private var __srcAudio:#if (js && html5) Audio #else Dynamic #end; + @:noCompletion private var __srcBuffer:#if lime_cffi ALBuffer #else Dynamic #end; + @:noCompletion private var __srcCustom:Dynamic; + @:noCompletion private var __srcHowl:#if lime_howlerjs Howl #else Dynamic #end; + @:noCompletion private var __srcSound:#if flash Sound #else Dynamic #end; + @:noCompletion private var __srcVorbisFile:#if lime_vorbis VorbisFile #else Dynamic #end; + + #if commonjs + private static function __init__() + { + var p = untyped AudioBuffer.prototype; + untyped Object.defineProperties(p, + { + "src": {get: p.get_src, set: p.set_src} + }); + } + #end + + /** + Creates a new, empty `AudioBuffer` instance. + **/ + public function new() {} + + /** + Disposes of the resources used by this `AudioBuffer`, such as unloading any associated audio data. + **/ + public function dispose():Void + { + #if (js && html5 && lime_howlerjs) + if (__srcHowl != null) __srcHowl.unload(); + __srcHowl = null; + #end + #if lime_cffi + if (__srcBuffer != null) AL.deleteBuffer(__srcBuffer); + __srcBuffer = null; + #end + #if lime_vorbis + if (__srcVorbisFile != null) { + __srcVorbisFile.clear(); + @:privateAccess __srcVorbisFile.handle = null; + } + __srcVorbisFile = null; + #end + } + + /** + Creates an `AudioBuffer` from a Base64-encoded string. + + @param base64String The Base64-encoded audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + public static function fromBase64(base64String:String):AudioBuffer + { + if (base64String == null) return null; + + #if (js && html5 && lime_howlerjs) + // if base64String doesn't contain codec data, add it. + if (base64String.indexOf(",") == -1) + { + base64String = "data:" + __getCodec(Base64.decode(base64String)) + ";base64," + base64String; + } + + var audioBuffer = new AudioBuffer(); + audioBuffer.src = new Howl({src: [base64String], html5: true, preload: false}); + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + // if base64String contains codec data, strip it then decode it. + var base64StringSplit = base64String.split(","); + var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1]; + var bytes:Bytes = Base64.decode(base64StringNoEncoding); + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer); + #else + // if base64String contains codec data, strip it then decode it. + var base64StringSplit = base64String.split(","); + var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1]; + var bytes:Bytes = Base64.decode(base64StringNoEncoding); + var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + return audioBuffer; + } + #end + #end + + return null; + } + + /** + Creates an `AudioBuffer` from a `Bytes` object. + + @param bytes The `Bytes` object containing the audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + public static function fromBytes(bytes:Bytes):AudioBuffer + { + if (bytes == null) return null; + + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], html5: true, preload: false}); + + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer); + #else + var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + return audioBuffer; + } + #end + #end + + return null; + } + + /** + Creates an `AudioBuffer` from a file. + + @param path The file path to the audio data. + @return An `AudioBuffer` instance with the audio data loaded from the file. + **/ + public static function fromFile(path:String #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer + { + if (path == null) return null; + + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + + #if force_html5_audio + audioBuffer.__srcHowl = new Howl({src: [path], html5: true, preload: false}); + #else + audioBuffer.__srcHowl = new Howl({src: [path], html5: howlHtml5, preload: false}); + #end + + return audioBuffer; + #elseif flash + switch (Path.extension(path)) + { + case "ogg", "wav": + return null; + default: + } + + var audioBuffer = new AudioBuffer(); + audioBuffer.__srcSound = new Sound(new URLRequest(path)); + return audioBuffer; + #elseif (lime_cffi && !macro) + #if !cs + var audioBuffer = new AudioBuffer(); + audioBuffer.data = new UInt8Array(Bytes.alloc(0)); + + //audioBuffer = NativeCFFI.lime_audio_load_file(path, audioBuffer); + //if (audioBuffer != null) audioBuffer.initBuffer(); + //return audioBuffer; + return NativeCFFI.lime_audio_load_file(path, audioBuffer); + #else + var data:Dynamic = NativeCFFI.lime_audio_load_file(path, null); + + if (data != null) + { + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = data.bitsPerSample; + audioBuffer.channels = data.channels; + audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b)); + audioBuffer.sampleRate = data.sampleRate; + //audioBuffer.initBuffer(); + return audioBuffer; + } + + return null; + #end + #else + return null; + #end + } + + /** + Creates an `AudioBuffer` from an array of file paths. + + @param paths An array of file paths to search for audio data. + @return An `AudioBuffer` instance with the audio data loaded from the first valid file found. + **/ + public static function fromFiles(paths:Array #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer + { + #if (js && html5 && lime_howlerjs) + var audioBuffer = new AudioBuffer(); + + #if force_html5_audio + audioBuffer.__srcHowl = new Howl({src: paths, html5: true, preload: false}); + #else + audioBuffer.__srcHowl = new Howl({src: paths, html5: howlHtml5, preload: false}); + #end + + return audioBuffer; + #else + var buffer = null; + + for (path in paths) + { + buffer = AudioBuffer.fromFile(path); + if (buffer != null) break; + } + + return buffer; + #end + } + + /** + Creates an `AudioBuffer` from a `VorbisFile`. + + @param vorbisFile The `VorbisFile` object containing the audio data. + @return An `AudioBuffer` instance with the decoded audio data. + **/ + #if lime_vorbis + + public static function fromVorbisFile(vorbisFile:VorbisFile):AudioBuffer + { + if (vorbisFile == null) return null; + + var info = vorbisFile.info(); + + var audioBuffer = new AudioBuffer(); + audioBuffer.channels = info.channels; + audioBuffer.sampleRate = info.rate; + audioBuffer.bitsPerSample = 16; + audioBuffer.__srcVorbisFile = vorbisFile; + + return audioBuffer; + } + #else + public static function fromVorbisFile(vorbisFile:Dynamic):AudioBuffer + { + return null; + } + #end + + /** + Asynchronously loads an `AudioBuffer` from a file. + + @param path The file path to the audio data. + @return A `Future` that resolves to the loaded `AudioBuffer`. + **/ + public static function loadFromFile(path:String):Future + { + #if (flash || (js && html5)) + var promise = new Promise(); + + var audioBuffer = AudioBuffer.fromFile(path); + + if (audioBuffer != null) + { + #if flash + audioBuffer.__srcSound.addEventListener(flash.events.Event.COMPLETE, function(event) + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcSound.addEventListener(flash.events.ProgressEvent.PROGRESS, function(event) + { + promise.progress(Std.int(event.bytesLoaded), Std.int(event.bytesTotal)); + }); + + audioBuffer.__srcSound.addEventListener(flash.events.IOErrorEvent.IO_ERROR, promise.error); + #elseif (js && html5 && lime_howlerjs) + if (audioBuffer != null) + { + audioBuffer.__srcHowl.on("load", function() + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcHowl.on("loaderror", function(id, msg) + { + promise.error(msg); + }); + + audioBuffer.__srcHowl.load(); + } + #else + promise.complete(audioBuffer); + #end + } + else + { + promise.error(null); + } + + return promise.future; + #else + // TODO: Streaming + + var request = new HTTPRequest(); + return request.load(path).then(function(buffer) + { + if (buffer != null) + { + return Future.withValue(buffer); + } + else + { + return cast Future.withError(""); + } + }); + #end + } + + /** + Asynchronously loads an `AudioBuffer` from multiple files. + + @param paths An array of file paths to search for audio data. + @return A `Future` that resolves to the loaded `AudioBuffer`. + **/ + public static function loadFromFiles(paths:Array):Future + { + #if (js && html5 && lime_howlerjs) + var promise = new Promise(); + + var audioBuffer = AudioBuffer.fromFiles(paths); + + if (audioBuffer != null) + { + audioBuffer.__srcHowl.on("load", function() + { + promise.complete(audioBuffer); + }); + + audioBuffer.__srcHowl.on("loaderror", function() + { + promise.error(null); + }); + + audioBuffer.__srcHowl.load(); + } + else + { + promise.error(null); + } + + return promise.future; + #else + return new Future(fromFiles.bind(paths), true); + #end + } + + private static function __getCodec(bytes:Bytes):String + { + var signature = bytes.getString(0, 4); + + switch (signature) + { + case "OggS": + return "audio/ogg"; + case "fLaC": + return "audio/flac"; + case "RIFF" if (bytes.getString(8, 4) == "WAVE"): + return "audio/wav"; + default: + switch ([bytes.get(0), bytes.get(1), bytes.get(2)]) + { + case [73, 68, 51] | [255, 251, _] | [255, 250, _] | [255, 243, _]: return "audio/mp3"; + default: + } + } + + Log.error("Unsupported sound format"); + return null; + } + + // Get & Set Methods + @:noCompletion private function get_src():Dynamic + { + #if (js && html5) + #if lime_howlerjs + return __srcHowl; + #else + return __srcAudio; + #end + #elseif flash + return __srcSound; + #elseif lime_vorbis + return __srcVorbisFile; + #else + return __srcCustom; + #end + } + + @:noCompletion private function set_src(value:Dynamic):Dynamic + { + #if (js && html5) + #if lime_howlerjs + return __srcHowl = value; + #else + return __srcAudio = value; + #end + #elseif flash + return __srcSound = value; + #elseif lime_vorbis + return __srcVorbisFile = value; + #else + return __srcCustom = value; + #end + } +} diff --git a/source/lime/media/AudioSource.hx b/source/lime/media/AudioSource.hx index bc5948df..5ee8251a 100644 --- a/source/lime/media/AudioSource.hx +++ b/source/lime/media/AudioSource.hx @@ -1,29 +1,98 @@ package lime.media; import lime.app.Event; -import lime.media.openal.AL; -import lime.media.openal.ALSource; import lime.math.Vector4; #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end +/** + The `AudioSource` class provides a way to control audio playback in a Lime application. + It allows for playing, pausing, and stopping audio, as well as controlling various + audio properties such as gain, pitch, and looping. + + Depending on the platform, the audio backend may vary, but the API remains consistent. + + @see lime.media.AudioBuffer +**/ class AudioSource { + /** + An event that is dispatched when the audio playback is complete. + **/ public var onComplete = new EventVoid>(); + + /** + An event that is dispatched when the audio playback looped. + **/ + public var onLoop = new EventVoid>(); + + /** + The `AudioBuffer` associated with this `AudioSource`. + **/ public var buffer:AudioBuffer; + + /** + An property if this 'AudioSource' is playing. + **/ + public var playing(get, null):Bool; + + /** + The current playback position of the audio, in milliseconds. + **/ public var currentTime(get, set):Float; + + /** + The gain (volume) of the audio. A value of `1.0` represents the default volume. + **/ public var gain(get, set):Float; - public var length(get, set):Float; + + /** + The length of the audio, in milliseconds. + **/ + public var length(get, set):Null; + + /** + The number of times the audio will loop. A value of `0` means the audio will not loop. + **/ public var loops(get, set):Int; + + /** + In which audio playback time the audio will loop. + **/ + public var loopTime(get, set):Float; + + /** + The pitch of the audio. A value of `1.0` represents the default pitch. + **/ public var pitch(get, set):Float; - public var offset:Int; + + /** + The offset within the audio buffer to start playback, in samples. + **/ + public var offset:Float; + + /** + The 3D position of the audio source, represented as a `Vector4`. + **/ public var position(get, set):Vector4; + /** + The latency of the audio source. + **/ + public var latency(get, never):Float; + @:noCompletion private var __backend:AudioSourceBackend; - public function new(buffer:AudioBuffer = null, offset:Int = 0, length:Null = null, loops:Int = 0) + /** + Creates a new `AudioSource` instance. + @param buffer The `AudioBuffer` to associate with this `AudioSource`. + @param offset The starting offset within the audio buffer, in samples. + @param length The length of the audio to play, in milliseconds. If `null`, the full buffer is used. + @param loops The number of times to loop the audio. `0` means no looping. + **/ + public function new(buffer:AudioBuffer = null, offset:Float = 0, length:Null = null, loops:Int = 0) { this.buffer = buffer; this.offset = offset; @@ -35,104 +104,134 @@ class AudioSource this.length = length; } - this.loops = loops; - if (buffer != null) { init(); } + + this.loops = loops; } - public function dispose():Void + /** + Releases any resources used by this `AudioSource`. + **/ + inline public function dispose():Void { __backend.dispose(); } - @:noCompletion private function init():Void + @:noCompletion inline private function init():Void { __backend.init(); } - public function play():Void + /** + Starts or resumes audio playback. + **/ + inline public function play():Void { __backend.play(); } - public function pause():Void + /** + Pauses audio playback. + **/ + inline public function pause():Void { __backend.pause(); } - public function stop():Void + /** + Stops audio playback and resets the playback position to the beginning. + **/ + inline public function stop():Void { __backend.stop(); } // Get & Set Methods - @:noCompletion private function get_currentTime():Float + @:noCompletion inline private function get_playing():Bool + { + @:privateAccess return __backend.playing; + } + + @:noCompletion inline private function get_currentTime():Float { return __backend.getCurrentTime(); } - @:noCompletion private function set_currentTime(value:Float):Float + @:noCompletion inline private function set_currentTime(value:Float):Float { return __backend.setCurrentTime(value); } - @:noCompletion private function get_gain():Float + @:noCompletion inline private function get_gain():Float { return __backend.getGain(); } - @:noCompletion private function set_gain(value:Float):Float + @:noCompletion inline private function set_gain(value:Float):Float { return __backend.setGain(value); } - @:noCompletion private function get_length():Float + @:noCompletion inline private function get_length():Null { return __backend.getLength(); } - @:noCompletion private function set_length(value:Float):Float + @:noCompletion inline private function set_length(value:Null):Null { return __backend.setLength(value); } - @:noCompletion private function get_loops():Int + @:noCompletion inline private function get_loops():Int { return __backend.getLoops(); } - @:noCompletion private function set_loops(value:Int):Int + @:noCompletion inline private function set_loops(value:Int):Int { return __backend.setLoops(value); } - @:noCompletion private function get_pitch():Float + @:noCompletion inline private function get_loopTime():Float + { + return __backend.getLoopTime(); + } + + @:noCompletion inline private function set_loopTime(value:Float):Float + { + return __backend.setLoopTime(value); + } + + @:noCompletion inline private function get_pitch():Float { return __backend.getPitch(); } - @:noCompletion private function set_pitch(value:Float):Float + @:noCompletion inline private function set_pitch(value:Float):Float { return __backend.setPitch(value); } - @:noCompletion private function get_position():Vector4 + @:noCompletion inline private function get_position():Vector4 { return __backend.getPosition(); } - @:noCompletion private function set_position(value:Vector4):Vector4 + @:noCompletion inline private function set_position(value:Vector4):Vector4 { return __backend.setPosition(value); } + + @:noCompletion inline private function get_latency():Float + { + return __backend.getLatency(); + } } -#if flash -@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.flash.FlashAudioSource; -#elseif (js && html5) +#if (js && html5) @:noCompletion private typedef AudioSourceBackend = lime._internal.backend.html5.HTML5AudioSource; #else @:noCompletion private typedef AudioSourceBackend = lime._internal.backend.native.NativeAudioSource; diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 0838f391..469759e9 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -1,10 +1,15 @@ package openfl.media; #if !flash +import haxe.Int64; + import openfl.events.Event; import openfl.events.EventDispatcher; #if lime import lime.media.AudioSource; + +#if lime_cffi import lime._internal.backend.native.NativeAudioSource; #end +#if lime_vorbis import lime.media.openal.AL; #end #end /** @@ -16,11 +21,19 @@ import lime.media.AudioSource; object to the channel. @event soundComplete Dispatched when a sound has finished playing. + + @see [Playing sounds](https://books.openfl.org/openfl-developers-guide/working-with-sound/playing-sounds.html) + @see `openfl.media.Sound` **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug #end +#if (lime && lime_cffi) +@:access(lime._internal.backend.native.NativeAudioSource) +@:access(lime.media.AudioSource) +#end +@:access(openfl.media.Sound) @:access(openfl.media.SoundMixer) @:final @:keep class SoundChannel extends EventDispatcher { @@ -28,7 +41,13 @@ import lime.media.AudioSource; The current amplitude(volume) of the left channel, from 0(silent) to 1 (full amplitude). **/ - public var leftPeak(default, null):Float; + public var leftPeak(get, null):Float; + + /** + The current amplitude(volume) of the right channel, from 0(silent) to 1 + (full amplitude). + **/ + public var rightPeak(get, null):Float; /** When the sound is playing, the `position` property indicates in @@ -45,12 +64,6 @@ import lime.media.AudioSource; **/ public var position(get, set):Float; - /** - The current amplitude(volume) of the right channel, from 0(silent) to 1 - (full amplitude). - **/ - public var rightPeak(default, null):Float; - /** The SoundTransform object assigned to the sound channel. A SoundTransform object includes properties for setting volume, panning, left speaker @@ -58,13 +71,23 @@ import lime.media.AudioSource; **/ public var soundTransform(get, set):SoundTransform; + /** + self explanatory + */ + public var loopTime(get, set):Float; + public var endTime(get, set):Null; public var pitch(get, set):Float; + public var loops(get, set):Int; + @:noCompletion private var __sound:Sound; @:noCompletion private var __isValid:Bool; @:noCompletion private var __soundTransform:SoundTransform; + @:noCompletion private var __lastPeakTime:Float; + @:noCompletion private var __leftPeak:Float; + @:noCompletion private var __rightPeak:Float; #if lime @:noCompletion private var __source:AudioSource; - @:noCompletion private var __audioSource(get, set):AudioSource; + @:noCompletion private var __audioSource(get, set):AudioSource; // forward??? compatibility?? #end #if openfljs @@ -87,28 +110,10 @@ import lime.media.AudioSource; { super(this); - leftPeak = 1; - rightPeak = 1; + if (soundTransform != null) __soundTransform = soundTransform; + else __soundTransform = new SoundTransform(); - if (soundTransform != null) - { - __soundTransform = soundTransform; - } - else - { - __soundTransform = new SoundTransform(); - } - - #if lime - if (source != null) - { - __source = source; - __source.onComplete.add(source_onComplete); - __isValid = true; - - __source.play(); - } - #end + __initAudioSource(source); SoundMixer.__registerSoundChannel(this); } @@ -134,6 +139,7 @@ import lime.media.AudioSource; #if lime __source.onComplete.remove(source_onComplete); + __source.onLoop.remove(source_onLoop); __source.dispose(); __source = null; #end @@ -145,6 +151,105 @@ import lime.media.AudioSource; this.soundTransform = soundTransform; } + // hi i made these - raltyro + // took me 8 months to figure this out + @:noCompletion inline private static var scanSamples = 480; + + #if (lime_cffi && !macro) + @:noCompletion private function __checkUpdatePeaks(time:Float):Bool + { + if (Math.abs(__lastPeakTime - time) < Math.max(1, pitch * 8)) return false; + __lastPeakTime = time; + return true; + } + + #if lime_vorbis + @:noCompletion private function __updateVorbisPeaks():Void + { + if (__source.buffer == null || !__checkUpdatePeaks(position)) return; + + var index = AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET), bufferDatas = __source.__backend.bufferDatas; + var bufferi = NativeAudioSource.STREAM_NUM_BUFFERS - __source.__backend.queuedBuffers, bytes = bufferDatas[bufferi].buffer; + + var channels = __source.buffer.channels, todo = scanSamples, sample; + var lfilled = false, rfilled = channels < 2; + + while(todo > 0 && (!lfilled || !rfilled)) { + if (index >= NativeAudioSource.STREAM_BUFFER_SIZE) { + if ((bytes = bufferDatas[++bufferi].buffer) == null) break; + index = 0; + } + + if (!lfilled) { + if ((sample = bytes.getUInt16(index * channels * 2) / 65535) > .5) sample = -(sample - 1); + if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; + } + + if (!rfilled) { + if ((sample = bytes.getUInt16(index * channels * 2 + 2) / 65535) > .5) sample = -(sample - 1); + if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; + } + + index++; + todo--; + } + } + #end + #end + + @:noCompletion private function __updatePeaks():Void + { + __leftPeak = __rightPeak = 0; + + #if (lime_cffi && !macro) + if (!__isValid) return; + + #if lime_vorbis if (__source.__backend.stream) return __updateVorbisPeaks(); #end + + var buffer = __source.buffer; + if (buffer == null || buffer.data == null || !__checkUpdatePeaks(position)) return; + + var index = Int64.make(0, AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET)); + var bytes = buffer.data.buffer, length = __source.__backend.samples; + if (index >= length) return; + + var temp = index + scanSamples; + if (temp < length) length = temp; + + var channels = buffer.channels, sample; + var lfilled = false, rfilled = channels < 2; + + while(index < length && (!lfilled || !rfilled)) { + if (!lfilled) { + if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2))) / 65535) > .5) sample = -(sample - 1); + if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; + } + if (!rfilled) { + if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2) + 2)) / 65535) > .5) sample = -(sample - 1); + if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; + } + index++; + } + #end + } + + @:noCompletion private function __initAudioSource(source:#if lime AudioSource #else Dynamic #end):Void + { + #if lime + __source = source; + if (__source == null) + { + return; + } + + __source.onComplete.add(source_onComplete); + __source.onLoop.add(source_onLoop); + __isValid = true; + + __source.play(); + #end + } + // Get & Set Methods @:noCompletion private function get_position():Float { @@ -167,27 +272,6 @@ import lime.media.AudioSource; return value; } - @:noCompletion private function get_pitch():Float - { - if (!__isValid) return 1; - - #if lime - return __source.pitch; - #else - return 1; - #end - } - - @:noCompletion private function set_pitch(value:Float):Float - { - if (!__isValid) return 1; - - #if lime - __source.pitch = value; - #end - return value; - } - @:noCompletion private function get_soundTransform():SoundTransform { return __soundTransform.clone(); @@ -203,7 +287,7 @@ import lime.media.AudioSource; var pan = SoundMixer.__soundTransform.pan + __soundTransform.pan; if (pan < -1) pan = -1; - else if (pan > 1) pan = 1; + if (pan > 1) pan = 1; var volume = SoundMixer.__soundTransform.volume * __soundTransform.volume; @@ -225,6 +309,107 @@ import lime.media.AudioSource; return value; } + @:noCompletion private function get_pitch():Float + { + if (!__isValid) return 1; + + #if lime + return __source.pitch; + #else + return 0; + #end + } + + @:noCompletion private function set_pitch(value:Float):Float + { + if (!__isValid) return 1; + + #if lime + return __source.pitch = value; + #else + return 0; + #end + } + + @:noCompletion private function get_loopTime():Float + { + if (!__isValid) return -1; + + #if lime + return __source.loopTime; + #else + return -1; + #end + } + + @:noCompletion private function set_loopTime(value:Float):Float + { + if (!__isValid) return -1; + + #if lime + return __source.loopTime = value; + #else + return -1; + #end + } + + @:noCompletion private function get_endTime():Null + { + if (!__isValid) return null; + + #if lime + return __source.length; + #else + return null; + #end + } + + @:noCompletion private function set_endTime(value:Null):Null + { + if (!__isValid) return null; + + #if lime + return __source.length = value; + #else + return null; + #end + } + + @:noCompletion private function get_loops():Int + { + if (!__isValid) return 0; + + #if lime + return __source.loops; + #else + return 0; + #end + } + + @:noCompletion private function set_loops(value:Int):Int + { + if (!__isValid) return 0; + + #if lime + return __source.loops = value; + #else + return 0; + #end + } + + @:noCompletion private function get_leftPeak():Float + { + __updatePeaks(); + return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + @:noCompletion private function get_rightPeak():Float + { + __updatePeaks(); + return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume); + } + + // Event Handlers @:noCompletion private function source_onComplete():Void { @@ -234,14 +419,14 @@ import lime.media.AudioSource; dispatchEvent(new Event(Event.SOUND_COMPLETE)); } - function set___audioSource(value:AudioSource):AudioSource { - return __source = value; + @:noCompletion private function source_onLoop():Void + { + //dispatchEvent(new Event(Event.SOUND_LOOP)); } - function get___audioSource():AudioSource { - return __source; - } + @:noCompletion private function get___audioSource():AudioSource return __source; + @:noCompletion private function set___audioSource(source:AudioSource):AudioSource return __source = source; } #else typedef SoundChannel = flash.media.SoundChannel; -#end \ No newline at end of file +#end From aa5cf639e8a646fad3f300acb17e289ce8473ed3 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:16:55 +0700 Subject: [PATCH 02/17] Fix Resyncing --- source/funkin/game/PlayState.hx | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index d81c9444..346e4626 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -1000,14 +1000,9 @@ class PlayState extends MusicBeatState inst.onComplete = endSong; var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0; - inst.time = vocals.time = time; - - for (strumLine in strumLines.members) { - strumLine.vocals.time = time; - strumLine.vocals.play(); - } - vocals.play(); - inst.play(); + for (strumLine in strumLines.members) strumLine.vocals.play(true, time); + vocals.play(true, time); + inst.play(true, time); updateDiscordPresence(); @@ -1176,17 +1171,10 @@ 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; - inst.time = vocals.time = time; - for (strumLine in strumLines.members) { - strumLine.vocals.time = time; - strumLine.vocals.play(); - } - vocals.play(); - inst.play(); + for (strumLine in strumLines.members) strumLine.vocals.play(true, time); + vocals.play(true, time); + inst.play(true, time); gameAndCharsCall("onVocalsResync"); } From 80bc6506b91b8be9e3dd393594d1765e2a795a78 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:59:26 +0700 Subject: [PATCH 03/17] Rewrite Internal Audio Source & Fix AudioSwitchFix --- source/flixel/sound/FlxSound.hx | 10 +- .../backend/system/modules/AudioSwitchFix.hx | 72 +-- .../backend/native/NativeAudioSource.hx | 538 ++++++++---------- source/lime/media/AudioSource.hx | 4 + source/openfl/media/Sound.hx | 18 - source/openfl/media/SoundChannel.hx | 4 +- 6 files changed, 277 insertions(+), 369 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 4884a926..5109ecfa 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -2,6 +2,7 @@ package flixel.sound; import lime.media.AudioBuffer; import lime.media.AudioSource; +import lime.media.AudioManager; import lime.media.openal.AL; #if lime_vorbis import lime.media.vorbis.VorbisFile; @@ -695,11 +696,6 @@ class FlxSound extends FlxBasic { pitch = _pitch; #end - // TODO: fix the buffer loud beep for streaming sounds... - #if lime_openal - _source.__backend.setGain(0); - #end - _channel.soundTransform = _transform; _channel.__lastPeakTime = 0; _channel.__leftPeak = 0; @@ -716,7 +712,7 @@ class FlxSound extends FlxBasic { try {s = AL.getSourcei(_source.__backend.handle, AL.SOURCE_STATE) == AL.PLAYING;} catch(e) {} if (s) updateTransform(); else { - if (++_tries > 10) return; + if (++_tries > 2) return; makeChannel(); return startSound(StartTime); } @@ -889,7 +885,7 @@ class FlxSound extends FlxBasic { return _time; } function get_time():Float { - if (_channel == null) return _time; + if (_channel == null || AudioManager.context == null) return _time; final pos = _channel.position - _offset; if (!playing || _realPitch <= 0) { diff --git a/source/funkin/backend/system/modules/AudioSwitchFix.hx b/source/funkin/backend/system/modules/AudioSwitchFix.hx index 941846e0..382fcc0c 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,40 @@ 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) soundList.push(sound); + for (source in AudioSource.activeSources) source.dispose(); - 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(); + AudioManager.shutdown(); - AudioManager.shutdown(); - AudioManager.init(); - // #if !lime_doc_gen - // if (AudioManager.context.type == OPENAL) - // { - // var alc = AudioManager.context.openal; + AudioManager.init(); + Main.changeID++; + // #if !lime_doc_gen + // if (AudioManager.context.type == OPENAL) + // { + // var alc = AudioManager.context.openal; - // var device = alc.openDevice(); - // var ctx = alc.createContext(device); - // alc.makeContextCurrent(ctx); - // alc.processContext(ctx); - // } - // #end - Main.changeID++; + // var device = alc.openDevice(); + // var ctx = alc.createContext(device); + // alc.makeContextCurrent(ctx); + // alc.processContext(ctx); + // } + // #end - for(e in playingList) { - e.sound.play(e.time); - } + for (sound in soundList) { + sound._paused = true; + sound.resume(); + } - Main.audioDisconnected = false; - } - #end + 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/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index dcd84632..00a96cee 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,3 +1,7 @@ +// ALL REWRITTEN FROM SCRATCH!!!! -raltyro +// YES ALL OF IT!!! +// FUCK + package lime._internal.backend.native; import haxe.Timer; @@ -6,7 +10,11 @@ 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; +#end import lime.math.Vector4; import lime.media.AudioBuffer; @@ -19,99 +27,74 @@ import lime.utils.ArrayBufferView; #end @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) -@:access(lime.media.vorbis.VorbisFile) class NativeAudioSource { - public static var initBuffers:Array = []; - - private static final STREAM_BUFFER_SIZE:Int = 0x8000; - private static final STREAM_NUM_BUFFERS:Int = 5; + private static final STREAM_BUFFER_SIZE:Int = 0x4000; + private static final STREAM_MAX_BUFFERS:Int = 16; private static final STREAM_TIMER_FREQUENCY:Int = 100; - //#if (lime >= "8.3.0") - //private static var hasALSoftLatencyExt:Null; - //#end - - private var buffers:Array; - private var bufferDatas:Array; - private var bufferTimeBlocks:Array; - private var bufferLoops:Int; - private var queuedBuffers:Int; - private var requestBuffers:Int; - - private var length:Null; - private var loopTime:Null; + private var parent:AudioSource; + private var disposed:Bool; + private var streamed:Bool; private var playing:Bool; - private var loops:Int; - private var position:Vector4; + private var completed:Bool; - private var bitsPerSample:Int; + private var handle:ALSource; + private var buffers:Array; + private var timer:Timer; private var format:Int; private var dataLength:Int64; private var samples:Int64; - private var completed:Bool; - private var stream:Bool; - private var handle:ALSource; - private var parent:AudioSource; - private var timer:Timer; private var streamTimer:Timer; - private var disposed:Bool; - private var safeEnd:Bool; + private var bufferDatas:Array; + private var bufferTimes:Array; + private var requestBuffers:Int; + private var queuedBuffers:Int; + private var toLoop:Int; + private var streamEnded:Bool; - public function new(parent:AudioSource) { - this.parent = parent; - position = new Vector4(); - } + private var position:Vector4 = new Vector4(); + private var length:Null; + private var loopTime:Null; + private var loops:Int; - public function dispose():Void { + public function new(parent:AudioSource) this.parent = parent; + + public function dispose() { disposed = true; stop(); if (handle != null) { - AL.sourcei(handle, AL.BUFFER, null); + AL.sourcei(handle, AL.BUFFER, AL.NONE); AL.deleteSource(handle); } handle = null; if (buffers != null) AL.deleteBuffers(buffers); buffers = null; - - initBuffers.remove(this); } - public function init():Void { - //#if (lime >= "8.3.0") - //if (hasALSoftLatencyExt == null) hasALSoftLatencyExt = AL.isExtensionPresent("AL_SOFT_source_latency"); - //#end - + public function init() { if (handle != null) return; var buffer = parent.buffer; + if (buffer.channels == 1) format = buffer.bitsPerSample == 8 ? AL.FORMAT_MONO8 : AL.FORMAT_MONO16; + else format = buffer.bitsPerSample == 8 ? AL.FORMAT_STEREO8 : AL.FORMAT_STEREO16; + if (disposed = (handle = AL.createSource()) == null) return; - AL.sourcef(handle, AL.MAX_GAIN, 10); - bufferLoops = 0; - - bitsPerSample = buffer.bitsPerSample; - if (buffer.channels > 1) format = bitsPerSample == 8 ? AL.FORMAT_STEREO8 : AL.FORMAT_STEREO16; - else format = bitsPerSample == 8 ? AL.FORMAT_MONO8 : AL.FORMAT_MONO16; var vorbisFile = buffer.__srcVorbisFile; - if (stream = vorbisFile != null) { - dataLength = (samples = vorbisFile.pcmTotal()) * buffer.channels * (Int64.ofInt(bitsPerSample) / 8); - buffers = []; - bufferDatas = []; - bufferTimeBlocks = []; + if (streamed = vorbisFile != null) { + dataLength = (samples = vorbisFile.pcmTotal()) * buffer.channels * (Int64.ofInt(buffer.bitsPerSample) / 8); - var constructor = bitsPerSample == 8 ? Int8 : Int16; - for (i in 0...STREAM_NUM_BUFFERS) { - buffers.push(AL.createBuffer()); - bufferDatas.push(new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)); - bufferTimeBlocks.push(0); - } + var constructor = buffer.bitsPerSample == 8 ? Int8 : Int16; + buffers = AL.genBuffers(STREAM_MAX_BUFFERS); + bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)]; + bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; } else { - samples = ((dataLength = buffer.data.length) * 8) / (buffer.channels * bitsPerSample); + samples = ((dataLength = buffer.data.length) * 8) / (buffer.channels * buffer.bitsPerSample); if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null) AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate); @@ -119,15 +102,13 @@ class NativeAudioSource { AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer); } - initBuffers.push(this); - if (dataLength == 0) { trace('NativeAudioSource Bug! dataLength is 0'); dispose(); } } - public function play():Void { + public function play() { if (playing || disposed) return; playing = true; @@ -135,256 +116,35 @@ class NativeAudioSource { else setCurrentTime(getCurrentTime()); } - public function pause():Void { - if (!(disposed = handle == null)) AL.sourcePause(handle); + public function pause() { + if (!disposed) AL.sourcePause(handle); playing = false; stopStreamTimer(); stopTimer(); } - public function stop():Void { - if (!(disposed = handle == null)) { + public function stop() { + if (!disposed) { if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.STOPPED) AL.sourceStop(handle); - if (stream) AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED) + AL.getSourcei(handle, AL.BUFFERS_PROCESSED)); + if (streamed) AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + requestBuffers = queuedBuffers = toLoop = 0; } - requestBuffers = queuedBuffers = bufferLoops = 0; playing = false; stopStreamTimer(); stopTimer(); } - private function complete():Void { - stop(); - + private function complete() { completed = true; + stop(); parent.onComplete.dispatch(); } - private function readVorbisFileBuffer(vorbisFile:VorbisFile, max:Int):ArrayBufferView { - #if lime_vorbis - var id = STREAM_NUM_BUFFERS - requestBuffers, read = STREAM_NUM_BUFFERS - 1, total = 0, readMax; - var buffer = bufferDatas[id]; - - for (i in id...read) { - bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; - bufferDatas[i] = bufferDatas[i + 1]; - } - bufferTimeBlocks[read] = vorbisFile.timeTell(); - bufferDatas[read] = buffer; - queuedBuffers = requestBuffers; - - while (total < STREAM_BUFFER_SIZE) { - if ((readMax = 4096) > (read = max - total)) readMax = read; - if (vorbisFile.handle == null) break; - if (readMax > 0 && (read = vorbisFile.read(buffer.buffer, total, readMax)) > 0) total += read; - else if (safeEnd = (loops > bufferLoops)) { - if (readMax == 4096) continue; - bufferLoops++; vorbisFile.timeSeek((loopTime != null ? Math.max(0, loopTime / 1000) : 0) + parent.offset / 1000); - if ((max = (dataLength - (vorbisFile.pcmTell() * (Int64.ofInt(bitsPerSample) / 8) * parent.buffer.channels)).low) > STREAM_BUFFER_SIZE) - max = STREAM_BUFFER_SIZE; - } - else { - buffer.buffer.fill(total, STREAM_BUFFER_SIZE - total - 1, 0); - resetTimer((getLength() - getCurrentTime()) / getPitch()); - break; - } - } - - return buffer; - #else - return null; - #end - } - - private function fillBuffers(buffers:Array):Void { - #if lime_vorbis - if (parent == null || parent.buffer == null) return dispose(); - if (handle == null || buffers.length < 1) return; - - final buffer = parent.buffer, vorbisFile = buffer.__srcVorbisFile; - final actualDataRate = (Int64.ofInt(bitsPerSample) / 8) * buffer.channels; - var position = vorbisFile.pcmTell() * actualDataRate, length = getLengthSamples() * actualDataRate, trackLoops = bufferLoops; - if (position >= length && safeEnd) return; - - var sampleRate = buffer.sampleRate, numBuffers = 0, data, size:Int64; - for (buffer in buffers) { - if ((size = length - position) > STREAM_BUFFER_SIZE) size = Int64.ofInt(STREAM_BUFFER_SIZE); - data = readVorbisFileBuffer(vorbisFile, Int64.toInt(size)); - - if (disposed) return; - AL.bufferData(buffer, format, data, STREAM_BUFFER_SIZE, sampleRate); - numBuffers++; - - if (safeEnd) break; - else if (bufferLoops != trackLoops) { - position = vorbisFile.pcmTell() * actualDataRate; - trackLoops = bufferLoops; - } - } - AL.sourceQueueBuffers(handle, numBuffers, buffers); - - if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { - if (bufferLoops > 0) { // TODO: remove this once buffer loop stutters fixed - setCurrentTime(getCurrentTime()); - return; - } - AL.sourcePlay(handle); - resetTimer(Std.int((getLength() - getCurrentTime()) / getPitch())); - } - #end - } - - private function streamRun():Void { - #if lime_vorbis - if (disposed = (handle == null || buffers == null)) return dispose(); - - var vorbisFile = parent.buffer.__srcVorbisFile; - if (vorbisFile == null) return dispose(); - - try { - var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); - if (processed > 0 && playing) { - fillBuffers(AL.sourceUnqueueBuffers(handle, processed)); - if ((!safeEnd || loops > 0) && queuedBuffers < STREAM_NUM_BUFFERS && playing) { - /*if (requestBuffers >= STREAM_NUM_BUFFERS) { - AL.sourceUnqueueBuffers(handle, queuedBuffers); - setCurrentTime((bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers] + - (STREAM_BUFFER_SIZE / ((bitsPerSample / 8) * parent.buffer.channels) / parent.buffer.sampleRate) - ) * 1000 - parent.offset); - } - else*/fillBuffers([buffers[++requestBuffers - 1]]); - } - } - } - catch(e) trace(e); - #end - } - - // Timers - inline function stopStreamTimer():Void if (streamTimer != null) streamTimer.stop(); - - private function resetStreamTimer():Void { - stopStreamTimer(); - - #if lime_vorbis - streamTimer = new Timer(STREAM_TIMER_FREQUENCY); - streamTimer.run = streamRun; - #end - } - - inline function stopTimer():Void if (timer != null) timer.stop(); - - private function resetTimer(timeRemaining:Float):Void { - stopTimer(); - - /*if (timeRemaining <= 26) { - timer_onRun(); - return; - }*/ - timer = new Timer(timeRemaining); - timer.run = timer_onRun; - } - - private function timer_onRun():Void { - if (!safeEnd && bufferLoops <= 0) { - #if lime_vorbis - var ranOut = false; - if (stream) { - var vorbisFile = parent.buffer.__srcVorbisFile; - if (vorbisFile == null) return dispose(); - ranOut = vorbisFile.pcmTell() >= getLengthSamples() || queuedBuffers < 3; - } - - if (!ranOut) - #end - { - var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); - if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { - resetTimer(timeRemaining); - return; - } - } - } - safeEnd = false; - - if (loops <= 0) { - complete(); - return; - } - - if (bufferLoops <= 0) { - loops--; - var loopTime = loopTime ?? 0; - if (stream || loopTime > 0) setCurrentTime(loopTime); - else if (!stream) { - setLoops(loops); - if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) - setCurrentTime(0); - } - } - else { - loops -= bufferLoops; - bufferLoops = 0; - } - parent.onLoop.dispatch(); - } - - // Get & Set Methods - public function getCurrentTime():Float { - if (completed) return getLength(); - else if (!disposed) { - var time = AL.getSourcef(handle, AL.SEC_OFFSET); - if (stream) time += bufferTimeBlocks[STREAM_NUM_BUFFERS - queuedBuffers]; - time = time * 1000 - parent.offset; - if (loops > 0 && time > getLength()) { - var start = loopTime != null ? Math.max(0, loopTime + parent.offset) : parent.offset; - return ((time - start) % (getLength() - start)) + start; - } - if (time > 0) return time; - } - return 0; - } - - public function setCurrentTime(value:Float):Float { - if (disposed = (handle == null)) return value; - - var total = getRealLength(); - var time = Math.max(0, Math.min(total, value + parent.offset)), ratio = time / total; - - if (stream) { - // TODO: smooth setCurrentTime for stream (dont refill buffers again) - - AL.sourceStop(handle); - AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); - - #if lime_vorbis - var vorbisFile = parent.buffer.__srcVorbisFile; - if (vorbisFile != null) { - // var chunk = Std.int(Math.floor(getFloat(samples) * ratio / STREAM_BUFFER_SIZE) * STREAM_BUFFER_SIZE); - vorbisFile.pcmSeek(Int64.fromFloat(getFloat(samples) * ratio)); - fillBuffers(buffers.slice(0, requestBuffers = queuedBuffers = 3)); - // AL.sourcei(handle, AL.SAMPLE_OFFSET, Std.int((samples * ratio) - chunk)); - if (playing) resetStreamTimer(); - } - #end - } - else { - AL.sourceRewind(handle); - AL.sourcei(handle, AL.BYTE_OFFSET, Int64.fromFloat(getFloat(dataLength) * ratio)); - } - - if (playing) { - var timeRemaining = (getLength() - time) / getPitch(); - if (completed = timeRemaining <= 0) complete(); - else { - AL.sourcePlay(handle); - resetTimer(timeRemaining); - } - } - - return value; + inline private function getSamples(ms:Float):Int64 { + var v = Int64.fromFloat(ms / 1000 * parent.buffer.sampleRate); + return v > samples ? samples : (v < 0 ? 0 : v); } inline private function getLengthSamples():Int64 { @@ -396,6 +156,186 @@ class NativeAudioSource { inline private function getRealLength():Float return getFloat(samples) / parent.buffer.sampleRate * 1000; + // just incase if we support more than 1 in the future?? which i doubt + /* + private function streamTell():Float { + #if lime_vorbis + if (parent.buffer.__srcVorbisFile != null) return parent.buffer.__srcVorbisFile.timeTell(); + #end + return 0; + } + */ + private function streamTell():Float return #if lime_vorbis parent.buffer.__srcVorbisFile.timeTell() #else 0 #end; + + private function streamSeek(samples:Int64) { + #if lime_vorbis + //if (parent.buffer.__srcVorbisFile != null) { + // There's apparently a bug in libvorbis <= 1.3.4, hhttps://github.com/xiph/vorbis/blob/master/CHANGES#L39 + if (samples <= 2) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples); + // break; + //} + #end + } + + // Streaming, atleast for vorbis for now. + private function readToBufferData(data:ArrayBufferView):Int { + var total = 0, result = 0, wordSize:Int = parent.buffer.bitsPerSample == 8 ? 1 : 2; + + var vorbisFile = parent.buffer.__srcVorbisFile; + while (total < STREAM_BUFFER_SIZE) { + result = vorbisFile.read(data.buffer, total, STREAM_BUFFER_SIZE - total, false, wordSize, true); + + if (result == Vorbis.HOLE) continue; + else if (result == Vorbis.EREAD) break; + else if (result == 0) { + if (!(streamEnded = (loops <= toLoop))) { + toLoop++; + streamSeek(getSamples(loopTime != null ? loopTime : 0)); + } + break; + } + else total += result; + } + if (total < STREAM_BUFFER_SIZE) data.buffer.fill(total, STREAM_BUFFER_SIZE - total - 1, 0); + + if (result < 0) return result; + return total; + } + + private function fillBuffer(buffer:ALBuffer):Int { + var i = STREAM_MAX_BUFFERS - requestBuffers; + var bufferData = bufferDatas[i], bufferTime = streamTell(); + var decoded = readToBufferData(bufferData); + + if (decoded <= 0) return 0; + AL.bufferData(buffer, format, bufferData, decoded, parent.buffer.sampleRate); + + var n = STREAM_MAX_BUFFERS - 1; + for (x in i...n) { + bufferDatas[x] = bufferDatas[x + 1]; + bufferTimes[x] = bufferTimes[x + 1]; + } + queuedBuffers = requestBuffers; + bufferDatas[n] = bufferData; + bufferTimes[n] = bufferTime; + + return decoded; + } + + private function fillBuffers(buffers:Array) { + for (buffer in buffers) { + if (fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); + if (streamEnded) break; + } + + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + AL.sourcePlay(handle); + resetTimer((getLength() - getCurrentTime()) / getPitch()); + } + } + + private function streamRun() { + #if lime_vorbis + if (parent == null || parent.buffer == null || handle == null || buffers == null || parent.buffer.__srcVorbisFile == null) return dispose(); + if (!playing || streamEnded) stopStreamTimer(); + + try { + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + if (processed > 0) { + fillBuffers(AL.sourceUnqueueBuffers(handle, processed)); + if (queuedBuffers < STREAM_MAX_BUFFERS) fillBuffers([buffers[++requestBuffers - 1]]); + } + } + catch(e) trace(e); + #end + } + + // Timers + inline function stopStreamTimer() if (streamTimer != null) streamTimer.stop(); + + private function resetStreamTimer() { + stopStreamTimer(); + + if (streamed) { + streamTimer = new Timer(STREAM_TIMER_FREQUENCY); + streamTimer.run = streamRun; + } + } + + inline function stopTimer() if (timer != null) timer.stop(); + + private function resetTimer(timeRemaining:Float) { + stopTimer(); + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + + private function timer_onRun() { + if (!streamed || !streamEnded) { + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); + if (timeRemaining > 30 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { + resetTimer(timeRemaining); + return; + } + } + + if (loops == 0) { + complete(); + return; + } + + if (toLoop == 0) { + var start = loopTime != null ? loopTime : 0; + setLoops(--loops); + if (start > 0 || AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) setCurrentTime(start); + } + else if ((loops -= toLoop) < 0) loops = 0; + toLoop = 0; + + parent.onLoop.dispatch(); + } + + // Get & Set Methods + public function getCurrentTime():Float { + if (completed) return getLength(); + else if (!disposed) { + var time = ((streamed ? bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers] : 0) + AL.getSourcef(handle, AL.SEC_OFFSET)) * 1000 - parent.offset; + if (loops > 0 && time > getLength()) { + var start = (loopTime != null ? Math.max(0, loopTime) : 0) + parent.offset; + return ((time - start) % (getLength() - start)) + start; + } + else if (time > 0) return time; + } + return 0; + } + + public function setCurrentTime(value:Float):Float { + if (disposed) return value; + + if (streamed) { + AL.sourceStop(handle); + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + + streamSeek(getSamples(value)); + fillBuffers(buffers.slice(0, requestBuffers = queuedBuffers = 3)); + } + else + AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value)); + + if (playing) { + var timeRemaining = (getLength() - value) / AL.getSourcef(handle, AL.PITCH); + if (timeRemaining > 0) { + if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) AL.sourcePlay(handle); + if (streamed) resetStreamTimer(); + resetTimer(timeRemaining); + } + else + complete(); + } + + return value; + } + public function getLength():Null { return if (length == null) getRealLength() - parent.offset; else length - parent.offset; @@ -440,20 +380,22 @@ class NativeAudioSource { inline public function getLoops():Int return loops; inline public function setLoops(value:Int):Int { - if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE); + 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; } inline public function getLoopTime():Float return loopTime; inline public function setLoopTime(value:Float):Float { - if (!stream && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE); + if (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE); return loopTime = value; } public function getLatency():Float { /*#if (lime >= "8.3.0") - if (hasALSoftLatencyExt) { + 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; } @@ -475,4 +417,4 @@ class NativeAudioSource { } return position; } -} \ No newline at end of file +} diff --git a/source/lime/media/AudioSource.hx b/source/lime/media/AudioSource.hx index 5ee8251a..87f9e327 100644 --- a/source/lime/media/AudioSource.hx +++ b/source/lime/media/AudioSource.hx @@ -18,6 +18,8 @@ import lime.math.Vector4; **/ class AudioSource { + private static var activeSources:Array = []; + /** An event that is dispatched when the audio playback is complete. **/ @@ -118,11 +120,13 @@ class AudioSource inline public function dispose():Void { __backend.dispose(); + activeSources.remove(this); } @:noCompletion inline private function init():Void { __backend.init(); + activeSources.push(this); } /** diff --git a/source/openfl/media/Sound.hx b/source/openfl/media/Sound.hx index cbccc35b..1b4a8d5e 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(); diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 469759e9..7ba37349 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -169,7 +169,7 @@ import lime.media.AudioSource; if (__source.buffer == null || !__checkUpdatePeaks(position)) return; var index = AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET), bufferDatas = __source.__backend.bufferDatas; - var bufferi = NativeAudioSource.STREAM_NUM_BUFFERS - __source.__backend.queuedBuffers, bytes = bufferDatas[bufferi].buffer; + var bufferi = NativeAudioSource.STREAM_MAX_BUFFERS - __source.__backend.queuedBuffers, bytes = bufferDatas[bufferi].buffer; var channels = __source.buffer.channels, todo = scanSamples, sample; var lfilled = false, rfilled = channels < 2; @@ -204,7 +204,7 @@ import lime.media.AudioSource; #if (lime_cffi && !macro) if (!__isValid) return; - #if lime_vorbis if (__source.__backend.stream) return __updateVorbisPeaks(); #end + #if lime_vorbis if (__source.__backend.streamed) return __updateVorbisPeaks(); #end var buffer = __source.buffer; if (buffer == null || buffer.data == null || !__checkUpdatePeaks(position)) return; From 6834216878c0cfadd1432fe976f9df6d117afc77 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:24:25 +0700 Subject: [PATCH 04/17] Disable __updatePeaks & More work in audiosource --- source/flixel/sound/FlxSound.hx | 35 ++--- source/funkin/backend/system/Conductor.hx | 13 +- .../backend/native/NativeAudioSource.hx | 144 ++++++++++-------- source/openfl/media/SoundChannel.hx | 16 +- 4 files changed, 99 insertions(+), 109 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 5109ecfa..3edc4e88 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -4,9 +4,6 @@ import lime.media.AudioBuffer; import lime.media.AudioSource; import lime.media.AudioManager; import lime.media.openal.AL; -#if lime_vorbis -import lime.media.vorbis.VorbisFile; -#end import openfl.Assets; import openfl.events.IEventDispatcher; @@ -56,7 +53,7 @@ class FlxSound extends FlxBasic { public var playing(get, never):Bool; /** - * Set volume to a value between 0 and 1 to change how this sound is. + * Set volume to a value between 0 and 1* to change how this sound is. */ public var volume(get, set):Float; @@ -204,6 +201,7 @@ class FlxSound extends FlxBasic { /** * Signal that is dispatched on sound complete. + * Seperates the looping from onComplete. */ public final onFinish:FlxSignal; @@ -225,14 +223,6 @@ class FlxSound extends FlxBasic { */ public var buffer(get, never):AudioBuffer; - #if lime_vorbis - /** - * Stores the sound VorbisFile if exists. - * @since raltyMod - */ - public var vorbis(get, never):VorbisFile; - #end - /** * The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`) * @since 4.1.0 @@ -680,11 +670,11 @@ class FlxSound extends FlxBasic { * the sound and populate the _channel variable. */ var _tries = 0; - function startSound(StartTime:Float) @:privateAccess { + function startSound(startTime:Float) @:privateAccess { if (_sound == null) return; _paused = false; - _time = StartTime; + _time = startTime; _lastTime = FlxG.game.getTicks(); if (_channel == null || !_channel.__isValid || _source.__backend == null || _source.__backend.disposed #if lime_openal || _source.__backend.handle == null #end) makeChannel(); @@ -714,7 +704,7 @@ class FlxSound extends FlxBasic { else { if (++_tries > 2) return; makeChannel(); - return startSound(StartTime); + return startSound(startTime); } _tries = 0; @@ -777,14 +767,14 @@ class FlxSound extends FlxBasic { inline function get_volume():Float return _volume; - inline function set_volume(Volume:Float):Float { - _volume = FlxMath.bound(Volume, 0, 4); + inline function set_volume(v:Float):Float { + _volume = FlxMath.bound(v, 0, 10); updateTransform(); return _volume; } - inline function set_muted(Muted:Bool):Bool { - muted = Muted; + inline function set_muted(v:Bool):Bool { + muted = v; updateTransform(); return muted; } @@ -793,16 +783,11 @@ class FlxSound extends FlxBasic { return buffer != null; inline function get_streamed():Bool - return #if lime_vorbis vorbis != null #else false #end; + @: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; - #if lime_vorbis - inline function get_vorbis():VorbisFile - @:privateAccess return _sound != null ? _sound.__buffer.__srcVorbisFile : null; - #end - function update_amplitude():Void @:privateAccess { if (_channel == null || _time == _amplitudeTime || !_amplitudeUpdate) return; _channel.__updatePeaks(); diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx index 7ffcf4d8..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.getActualTime() - songOffset : -songOffset; - return; - } - - if (lastSongPos != (lastSongPos = FlxG.sound.music.getActualTime() - 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/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 00a96cee..42bfc72f 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,6 +1,7 @@ // ALL REWRITTEN FROM SCRATCH!!!! -raltyro // YES ALL OF IT!!! // FUCK +// TODO: add a frequency for how much buffers should be regenerated package lime._internal.backend.native; @@ -28,9 +29,10 @@ import lime.utils.ArrayBufferView; @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) class NativeAudioSource { - private static final STREAM_BUFFER_SIZE:Int = 0x4000; - private static final STREAM_MAX_BUFFERS:Int = 16; - private static final STREAM_TIMER_FREQUENCY:Int = 100; + private static var STREAM_BUFFER_SIZE:Int = 0x4000; + private static var STREAM_MAX_BUFFERS:Int = 32; + private static var STREAM_TIMER_FREQUENCY:Int = 100; + private static var STREAM_BUFFER_FREQUENCY:Int = 3; private var parent:AudioSource; private var disposed:Bool; @@ -40,10 +42,11 @@ class NativeAudioSource { private var handle:ALSource; private var buffers:Array; + private var unusedBuffers:Array; private var timer:Timer; private var format:Int; - private var dataLength:Int64; - private var samples:Int64; + private var dataLength:Float; + private var samples:Float; private var streamTimer:Timer; private var bufferDatas:Array; @@ -72,6 +75,9 @@ class NativeAudioSource { if (buffers != null) AL.deleteBuffers(buffers); buffers = null; + bufferDatas = null; + bufferTimes = null; + unusedBuffers = null; } public function init() { @@ -86,15 +92,16 @@ class NativeAudioSource { var vorbisFile = buffer.__srcVorbisFile; if (streamed = vorbisFile != null) { - dataLength = (samples = vorbisFile.pcmTotal()) * buffer.channels * (Int64.ofInt(buffer.bitsPerSample) / 8); + dataLength = (samples = getFloat(vorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); var constructor = buffer.bitsPerSample == 8 ? Int8 : Int16; buffers = AL.genBuffers(STREAM_MAX_BUFFERS); bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)]; bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; + unusedBuffers = []; } else { - samples = ((dataLength = buffer.data.length) * 8) / (buffer.channels * buffer.bitsPerSample); + samples = (dataLength = getFloat(Int64.make(0, buffer.data.length))) / buffer.channels / (buffer.bitsPerSample == 8 ? 1 : 2); if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null) AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate); @@ -142,62 +149,58 @@ class NativeAudioSource { parent.onComplete.dispatch(); } - inline private function getSamples(ms:Float):Int64 { - var v = Int64.fromFloat(ms / 1000 * parent.buffer.sampleRate); - return v > samples ? samples : (v < 0 ? 0 : v); - } - - inline private function getLengthSamples():Int64 { - return if (length == null) samples; - else Int64.fromFloat((length + parent.offset) / 1000 * parent.buffer.sampleRate); - } - - inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >>> 0); - - inline private function getRealLength():Float return getFloat(samples) / parent.buffer.sampleRate * 1000; - + // 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 - if (parent.buffer.__srcVorbisFile != null) return parent.buffer.__srcVorbisFile.timeTell(); + /*if (parent.buffer.__srcVorbisFile != null) */return parent.buffer.__srcVorbisFile.timeTell(); #end return 0; } - */ - private function streamTell():Float return #if lime_vorbis parent.buffer.__srcVorbisFile.timeTell() #else 0 #end; private function streamSeek(samples:Int64) { #if lime_vorbis //if (parent.buffer.__srcVorbisFile != null) { // There's apparently a bug in libvorbis <= 1.3.4, hhttps://github.com/xiph/vorbis/blob/master/CHANGES#L39 - if (samples <= 2) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples); + // 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); // break; //} #end } - // Streaming, atleast for vorbis for now. private function readToBufferData(data:ArrayBufferView):Int { var total = 0, result = 0, wordSize:Int = parent.buffer.bitsPerSample == 8 ? 1 : 2; + var pos = streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize; + var n = dataLength - pos < STREAM_BUFFER_SIZE ? Math.floor(dataLength - pos) : STREAM_BUFFER_SIZE; var vorbisFile = parent.buffer.__srcVorbisFile; while (total < STREAM_BUFFER_SIZE) { - result = vorbisFile.read(data.buffer, total, STREAM_BUFFER_SIZE - total, false, wordSize, true); + result = n > 0 ? vorbisFile.read(data.buffer, total, n, false, wordSize, true) : 0; if (result == Vorbis.HOLE) continue; else if (result == Vorbis.EREAD) break; else if (result == 0) { - if (!(streamEnded = (loops <= toLoop))) { + if (!(streamEnded = loops <= toLoop)) { + if (pos < dataLength) continue; toLoop++; - streamSeek(getSamples(loopTime != null ? loopTime : 0)); - } - break; - } - else total += result; - } - if (total < STREAM_BUFFER_SIZE) data.buffer.fill(total, STREAM_BUFFER_SIZE - total - 1, 0); + var samples = getSamples(loopTime != null ? loopTime : 0); + streamSeek(samples); + if (dataLength - (pos = getFloat(samples) * parent.buffer.channels * wordSize) < (n = STREAM_BUFFER_SIZE - total)) + n = Math.floor(dataLength - pos); + } + else + break; + } + else { + pos += result; + total += result; + n -= result; + } + } + + if (total < STREAM_BUFFER_SIZE) data.buffer.fill(total, n, 0); if (result < 0) return result; return total; } @@ -222,28 +225,30 @@ class NativeAudioSource { return decoded; } - private function fillBuffers(buffers:Array) { - for (buffer in buffers) { - if (fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); - if (streamEnded) break; - } - - if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { - AL.sourcePlay(handle); - resetTimer((getLength() - getCurrentTime()) / getPitch()); - } - } - private function streamRun() { #if lime_vorbis - if (parent == null || parent.buffer == null || handle == null || buffers == null || parent.buffer.__srcVorbisFile == null) return dispose(); - if (!playing || streamEnded) stopStreamTimer(); + if (parent == null || parent.buffer == null || handle == null) return dispose(); + if (!playing || streamEnded || disposed) stopStreamTimer(); try { - var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); - if (processed > 0) { - fillBuffers(AL.sourceUnqueueBuffers(handle, processed)); - if (queuedBuffers < STREAM_MAX_BUFFERS) fillBuffers([buffers[++requestBuffers - 1]]); + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer; + + while (processed-- > 0 && !streamEnded) { + buffer = AL.sourceUnqueueBuffer(handle); + if (n-- > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); + else unusedBuffers.push(buffer); + } + + if (!streamEnded) { + if (queuedBuffers < STREAM_MAX_BUFFERS) { + if (fillBuffer(buffer = buffers[requestBuffers++]) > 0) AL.sourceQueueBuffer(handle, buffer); + } + else if (unusedBuffers.length != 0 && fillBuffer(buffer = unusedBuffers.pop()) > 0) AL.sourceQueueBuffer(handle, buffer); + } + + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + AL.sourcePlay(handle); + resetTimer((getLength() - getCurrentTime()) / getPitch()); } } catch(e) trace(e); @@ -255,19 +260,14 @@ class NativeAudioSource { private function resetStreamTimer() { stopStreamTimer(); - - if (streamed) { - streamTimer = new Timer(STREAM_TIMER_FREQUENCY); - streamTimer.run = streamRun; - } + if (streamed) (streamTimer = new Timer(STREAM_TIMER_FREQUENCY)).run = streamRun; } inline function stopTimer() if (timer != null) timer.stop(); private function resetTimer(timeRemaining:Float) { stopTimer(); - timer = new Timer(timeRemaining); - timer.run = timer_onRun; + (timer = new Timer(timeRemaining)).run = timer_onRun; } private function timer_onRun() { @@ -316,8 +316,10 @@ class NativeAudioSource { AL.sourceStop(handle); AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + unusedBuffers.resize(0); streamSeek(getSamples(value)); - fillBuffers(buffers.slice(0, requestBuffers = queuedBuffers = 3)); + for (i in 0...(requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY)) + if (fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); } else AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value)); @@ -336,18 +338,25 @@ class NativeAudioSource { return value; } + inline private function getRealLength():Float return samples / parent.buffer.sampleRate * 1000; public function getLength():Null { return if (length == null) getRealLength() - parent.offset; else length - parent.offset; } public function setLength(value:Null):Null { - if (value == length) return value; - length = value; + if (value == length || disposed) return length = value; + + var buffer = parent.buffer; + if ((length = value) == null) dataLength = streamed ? samples * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2) : getFloat(Int64.make(0, buffer.data.length)); + else dataLength = Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); if (playing) { - var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); - if (timeRemaining > 0) resetTimer(timeRemaining); + if (streamed) setCurrentTime(getCurrentTime()); + else { + var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); + if (timeRemaining > 0) resetTimer(timeRemaining); + } } return value; } @@ -417,4 +426,7 @@ class NativeAudioSource { } 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))); } diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 7ba37349..bccb6138 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -7,9 +7,9 @@ import openfl.events.Event; import openfl.events.EventDispatcher; #if lime import lime.media.AudioSource; +import lime.media.openal.AL; #if lime_cffi import lime._internal.backend.native.NativeAudioSource; #end -#if lime_vorbis import lime.media.openal.AL; #end #end /** @@ -155,7 +155,6 @@ import lime.media.AudioSource; // took me 8 months to figure this out @:noCompletion inline private static var scanSamples = 480; - #if (lime_cffi && !macro) @:noCompletion private function __checkUpdatePeaks(time:Float):Bool { if (Math.abs(__lastPeakTime - time) < Math.max(1, pitch * 8)) return false; @@ -163,8 +162,7 @@ import lime.media.AudioSource; return true; } - #if lime_vorbis - @:noCompletion private function __updateVorbisPeaks():Void + /*@:noCompletion private function __updateVorbisPeaks():Void { if (__source.buffer == null || !__checkUpdatePeaks(position)) return; @@ -194,14 +192,11 @@ import lime.media.AudioSource; todo--; } } - #end - #end @:noCompletion private function __updatePeaks():Void { __leftPeak = __rightPeak = 0; - #if (lime_cffi && !macro) if (!__isValid) return; #if lime_vorbis if (__source.__backend.streamed) return __updateVorbisPeaks(); #end @@ -230,7 +225,12 @@ import lime.media.AudioSource; } index++; } - #end + }*/ + + @:noCompletion private function __updatePeaks():Void + { + __leftPeak = __rightPeak = 0; + if (!__isValid) return; } @:noCompletion private function __initAudioSource(source:#if lime AudioSource #else Dynamic #end):Void From 8b5314d66cab3df9d227bb25b22df24ce4a6aaf4 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:46:31 +0700 Subject: [PATCH 05/17] quick fix --- .../backend/native/NativeAudioSource.hx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 42bfc72f..25a34283 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -1,8 +1,4 @@ // ALL REWRITTEN FROM SCRATCH!!!! -raltyro -// YES ALL OF IT!!! -// FUCK -// TODO: add a frequency for how much buffers should be regenerated - package lime._internal.backend.native; import haxe.Timer; @@ -170,9 +166,9 @@ class NativeAudioSource { } private function readToBufferData(data:ArrayBufferView):Int { - var total = 0, result = 0, wordSize:Int = parent.buffer.bitsPerSample == 8 ? 1 : 2; - var pos = streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize; - var n = dataLength - pos < STREAM_BUFFER_SIZE ? Math.floor(dataLength - pos) : STREAM_BUFFER_SIZE; + var total = 0, result = 0, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2; + var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize); + var n = size < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; var vorbisFile = parent.buffer.__srcVorbisFile; while (total < STREAM_BUFFER_SIZE) { @@ -181,20 +177,15 @@ class NativeAudioSource { if (result == Vorbis.HOLE) continue; else if (result == Vorbis.EREAD) break; else if (result == 0) { - if (!(streamEnded = loops <= toLoop)) { - if (pos < dataLength) continue; - toLoop++; - + if (streamEnded = loops <= toLoop++) break; + else { var samples = getSamples(loopTime != null ? loopTime : 0); streamSeek(samples); - if (dataLength - (pos = getFloat(samples) * parent.buffer.channels * wordSize) < (n = STREAM_BUFFER_SIZE - total)) - n = Math.floor(dataLength - pos); + if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = STREAM_BUFFER_SIZE - total)) + n = Math.floor(size); } - else - break; } else { - pos += result; total += result; n -= result; } @@ -347,9 +338,9 @@ class NativeAudioSource { public function setLength(value:Null):Null { if (value == length || disposed) return length = value; - var buffer = parent.buffer; - if ((length = value) == null) dataLength = streamed ? samples * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2) : getFloat(Int64.make(0, buffer.data.length)); - else dataLength = Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); + var buffer = parent.buffer, wordSize = buffer.bitsPerSample == 8 ? 1 : 2; + 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) setCurrentTime(getCurrentTime()); From 5187f4a438feff2bb0ad5f1f5942726c30abf143 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:27:12 +0700 Subject: [PATCH 06/17] Register Endianness, stuff, add option to toggle --- source/funkin/editors/charter/Charter.hx | 19 +++------ source/funkin/options/Options.hx | 1 + .../options/categories/AppearanceOptions.hx | 4 ++ .../backend/native/NativeAudioSource.hx | 40 ++++++++++++------- source/openfl/utils/Assets.hx | 18 ++++----- 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 36cc38f1..3c6d7c68 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(Conductor.songPosition + Conductor.songOffset); + vocals.play(FlxG.sound.music.getActualTime()); for (strumLine in strumLines.members) { - strumLine.vocals.play(); - strumLine.vocals.time = vocals.time; + strumLine.vocals.play(FlxG.sound.music.getActualTime()); } } } diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index 71423236..b8483189 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -40,6 +40,7 @@ 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 lastLoadedMod:String = null; diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 9b97e299..272074ad 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -43,6 +43,10 @@ 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( "Auto Pause", "If checked, switching windows will pause the game.", diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 25a34283..ce2d6782 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -16,6 +16,7 @@ import lime.media.vorbis.VorbisFile; import lime.math.Vector4; import lime.media.AudioBuffer; import lime.media.AudioSource; +import lime.system.System; import lime.utils.ArrayBufferView; #if !lime_debug @@ -41,7 +42,6 @@ class NativeAudioSource { private var unusedBuffers:Array; private var timer:Timer; private var format:Int; - private var dataLength:Float; private var samples:Float; private var streamTimer:Timer; @@ -51,6 +51,7 @@ class NativeAudioSource { private var queuedBuffers:Int; private var toLoop:Int; private var streamEnded:Bool; + private var dataLength:Float; private var position:Vector4 = new Vector4(); private var length:Null; @@ -130,7 +131,10 @@ class NativeAudioSource { public function stop() { if (!disposed) { if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.STOPPED) AL.sourceStop(handle); - if (streamed) AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + if (streamed) { + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + unusedBuffers.resize(0); + } requestBuffers = queuedBuffers = toLoop = 0; } @@ -166,19 +170,21 @@ class NativeAudioSource { } private function readToBufferData(data:ArrayBufferView):Int { - var total = 0, result = 0, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2; + var total = 0, result = 0, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2, wasEOF = false; var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize); - var n = size < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; + var n:Int = size < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; var vorbisFile = parent.buffer.__srcVorbisFile; while (total < STREAM_BUFFER_SIZE) { - result = n > 0 ? vorbisFile.read(data.buffer, total, n, false, wordSize, true) : 0; + result = n > 0 ? vorbisFile.read(data.buffer, total, n, System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize, true) : 0; if (result == Vorbis.HOLE) continue; - else if (result == Vorbis.EREAD) break; + else if (result <= Vorbis.EREAD) break; else if (result == 0) { - if (streamEnded = loops <= toLoop++) break; + if (wasEOF || (streamEnded = loops <= toLoop++)) break; else { + wasEOF = true; + var samples = getSamples(loopTime != null ? loopTime : 0); streamSeek(samples); if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = STREAM_BUFFER_SIZE - total)) @@ -192,7 +198,10 @@ class NativeAudioSource { } if (total < STREAM_BUFFER_SIZE) data.buffer.fill(total, n, 0); - if (result < 0) return result; + if (result < 0) { + trace('NativeAudioSource Streaming Bug! reading result is $result'); + return result; + } return total; } @@ -222,19 +231,22 @@ class NativeAudioSource { if (!playing || streamEnded || disposed) stopStreamTimer(); try { - var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer; - - while (processed-- > 0 && !streamEnded) { - buffer = AL.sourceUnqueueBuffer(handle); - if (n-- > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY * Math.min(1, AL.getSourcef(handle, AL.PITCH)), buffer; + while (processed-- > 0) { + if (streamEnded || --n < 0) unusedBuffers.push(AL.sourceUnqueueBuffer(handle)); + else if (fillBuffer(buffer = AL.sourceUnqueueBuffer(handle)) > 0) AL.sourceQueueBuffer(handle, buffer); else unusedBuffers.push(buffer); } if (!streamEnded) { if (queuedBuffers < STREAM_MAX_BUFFERS) { if (fillBuffer(buffer = buffers[requestBuffers++]) > 0) AL.sourceQueueBuffer(handle, buffer); + else requestBuffers--; + } + else if (unusedBuffers.length != 0) { + if (fillBuffer(buffer = unusedBuffers.pop()) > 0) AL.sourceQueueBuffer(handle, buffer); + else unusedBuffers.push(buffer); } - else if (unusedBuffers.length != 0 && fillBuffer(buffer = unusedBuffers.pop()) > 0) AL.sourceQueueBuffer(handle, buffer); } if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { diff --git a/source/openfl/utils/Assets.hx b/source/openfl/utils/Assets.hx index db92e530..0909bdd4 100644 --- a/source/openfl/utils/Assets.hx +++ b/source/openfl/utils/Assets.hx @@ -243,16 +243,16 @@ 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); + var buffer = AudioBuffer.fromVorbisFile(vorbisFile); + return Sound.fromAudioBuffer(buffer); + } #end + return getSound(id, useCache); } /** From 96d60cfe8c2847323f74d1dd31e2918793ed33a0 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:34:55 +0700 Subject: [PATCH 07/17] Streamed Vocals? --- source/funkin/game/PlayState.hx | 2 +- source/funkin/game/StrumLine.hx | 4 +++- source/funkin/options/Options.hx | 1 + source/funkin/options/categories/AppearanceOptions.hx | 4 ++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 346e4626..cb8103d2 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -1065,7 +1065,7 @@ class PlayState extends MusicBeatState 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(); 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/options/Options.hx b/source/funkin/options/Options.hx index b8483189..63e791f5 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -41,6 +41,7 @@ class Options 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 272074ad..1eb41258 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -47,6 +47,10 @@ class AppearanceOptions extends OptionsScreen { "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.", From 89f862b42c91b8c6e532a78666903e790f660575 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:58:10 +0700 Subject: [PATCH 08/17] Better Buffer Streamed Audio Management --- source/flixel/sound/FlxSound.hx | 62 +++------ .../backend/native/NativeAudioSource.hx | 124 +++++++++++------- 2 files changed, 92 insertions(+), 94 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 3edc4e88..ae596dab 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -3,7 +3,6 @@ package flixel.sound; import lime.media.AudioBuffer; import lime.media.AudioSource; import lime.media.AudioManager; -import lime.media.openal.AL; import openfl.Assets; import openfl.events.IEventDispatcher; @@ -283,7 +282,7 @@ class FlxSound extends FlxBasic { _cameras = null; _lastTime = null; _paused = false; - _length = _time = 0; + _offset = _length = _time = 0; _volume = _volumeAdjust = 1; _amplitudeLeft = _amplitudeRight = 0; _amplitudeUpdate = true; @@ -669,20 +668,18 @@ class FlxSound extends FlxBasic { * An internal helper function used to attempt to start playing * the sound and populate the _channel variable. */ - var _tries = 0; function startSound(startTime:Float) @:privateAccess { if (_sound == null) return; _paused = false; _time = startTime; _lastTime = FlxG.game.getTicks(); - if (_channel == null || !_channel.__isValid || _source.__backend == null || _source.__backend.disposed #if lime_openal || _source.__backend.handle == null #end) + if (_channel == null || !_channel.__isValid || _source.__backend == null #if lime_openal || _source.__backend.disposed || _source.__backend.handle == null #end) makeChannel(); if (_channel != null) { #if FLX_PITCH _timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0; - _realPitch = -1.0; pitch = _pitch; #end @@ -691,24 +688,11 @@ class FlxSound extends FlxBasic { _channel.__leftPeak = 0; _channel.__rightPeak = 0; _channel.addEventListener(Event.SOUND_COMPLETE, stopped); - _source.__backend.playing = true; + + #if lime_openal _source.__backend.playing = true; #end _source.offset = 0; - _source.currentTime = _time; - - #if lime_openal - if (_source.__backend.handle == null) return destroy(); - - var s = false; - try {s = AL.getSourcei(_source.__backend.handle, AL.SOURCE_STATE) == AL.PLAYING;} catch(e) {} - if (s) updateTransform(); - else { - if (++_tries > 2) return; - makeChannel(); - return startSound(startTime); - } - - _tries = 0; - #end + _source.currentTime = startTime - _offset; + #if !lime_openal _source.play(); #end looped = looped; loopTime = loopTime; @@ -730,7 +714,7 @@ class FlxSound extends FlxBasic { if (looped) { cleanup(false); - play(false, loopTime - _offset, endTime - _offset); + play(false, loopTime, endTime); } else cleanup(autoDestroy); } @@ -824,18 +808,9 @@ class FlxSound extends FlxBasic { return _pitch; function set_pitch(v:Float):Float { - var adjusted:Float = FlxMath.bound(v * _timeScaleAdjust, 0); - if (_channel != null && _realPitch != adjusted) { - if ((_channel.pitch = adjusted) > 0 && _realPitch <= 0) { - _realPitch = adjusted; - time = _time; - } - else { - get_time(); - _realPitch = adjusted; - } - } - return _pitch = FlxMath.bound(v, 0); + _realPitch = (_pitch = v) * _timeScaleAdjust; + if (_channel != null) _channel.pitch = _realPitch; + return _pitch; } #end @@ -854,7 +829,7 @@ class FlxSound extends FlxBasic { function set_endTime(v:Null):Null { if (playing) { - if (v != null && v > 0 && v < length) _channel.endTime = v; + if (v != null && v > 0 && v < _length) _channel.endTime = v; else _channel.endTime = null; } return endTime = v; @@ -872,7 +847,7 @@ class FlxSound extends FlxBasic { function get_time():Float { if (_channel == null || AudioManager.context == null) return _time; - final pos = _channel.position - _offset; + final pos = _channel.position + _offset; if (!playing || _realPitch <= 0) { _lastTime = null; return _time = pos; @@ -893,7 +868,7 @@ class FlxSound extends FlxBasic { } function set_time(time:Float):Float @:privateAccess { - time = FlxMath.bound(time, 0, length - 1); + time = FlxMath.bound(time, _offset, length - 1); if (_channel != null && _realPitch > 0) { if (!_channel.__isValid) { cleanup(false, true); @@ -901,7 +876,7 @@ class FlxSound extends FlxBasic { } else if (playing) { _source.offset = 0; - _source.currentTime = time + _offset; + _source.currentTime = time - _offset; } } @@ -909,11 +884,10 @@ class FlxSound extends FlxBasic { return _time = time; } - function get_offset():Float return _offset; - function set_offset(offset:Float):Float { - if (offset == _offset) return offset; - _offset = offset; - 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; } diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index ce2d6782..4b187188 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -29,7 +29,7 @@ class NativeAudioSource { private static var STREAM_BUFFER_SIZE:Int = 0x4000; private static var STREAM_MAX_BUFFERS:Int = 32; private static var STREAM_TIMER_FREQUENCY:Int = 100; - private static var STREAM_BUFFER_FREQUENCY:Int = 3; + private static var STREAM_BUFFER_FREQUENCY:Int = 6; private var parent:AudioSource; private var disposed:Bool; @@ -87,9 +87,8 @@ class NativeAudioSource { if (disposed = (handle = AL.createSource()) == null) return; AL.sourcef(handle, AL.MAX_GAIN, 10); - var vorbisFile = buffer.__srcVorbisFile; - if (streamed = vorbisFile != null) { - dataLength = (samples = getFloat(vorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); + if (streamed = buffer.__srcVorbisFile != null) { + dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); var constructor = buffer.bitsPerSample == 8 ? Int8 : Int16; buffers = AL.genBuffers(STREAM_MAX_BUFFERS); @@ -170,22 +169,24 @@ class NativeAudioSource { } private function readToBufferData(data:ArrayBufferView):Int { - var total = 0, result = 0, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2, wasEOF = false; + var total = 0, result = 0, wasEOF = false; + var bigEndian = System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2; var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize); var n:Int = size < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; var vorbisFile = parent.buffer.__srcVorbisFile; while (total < STREAM_BUFFER_SIZE) { - result = n > 0 ? vorbisFile.read(data.buffer, total, n, System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize, true) : 0; + result = n > 0 ? vorbisFile.read(data.buffer, total, n, bigEndian, wordSize, true) : 0; if (result == Vorbis.HOLE) continue; else if (result <= Vorbis.EREAD) break; else if (result == 0) { - if (wasEOF || (streamEnded = loops <= toLoop++)) break; + if (wasEOF || (streamEnded = loops <= toLoop)) break; else { wasEOF = true; + toLoop++; - var samples = getSamples(loopTime != null ? loopTime : 0); + var samples = getSamples(loopTime != null ? loopTime + parent.offset : parent.offset); streamSeek(samples); if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = STREAM_BUFFER_SIZE - total)) n = Math.floor(size); @@ -194,6 +195,7 @@ class NativeAudioSource { else { total += result; n -= result; + wasEOF = false; } } @@ -205,22 +207,26 @@ class NativeAudioSource { return total; } + private function cycleBuffer() { + var i = STREAM_MAX_BUFFERS - (queuedBuffers = requestBuffers), n = STREAM_MAX_BUFFERS - 1; + var data = bufferDatas[i], time = bufferTimes[i]; + while (i < n) { + bufferDatas[i] = bufferDatas[i + 1]; + bufferTimes[i] = bufferTimes[++i]; + } + bufferDatas[n] = data; + bufferTimes[n] = time; + } + private function fillBuffer(buffer:ALBuffer):Int { - var i = STREAM_MAX_BUFFERS - requestBuffers; - var bufferData = bufferDatas[i], bufferTime = streamTell(); - var decoded = readToBufferData(bufferData); - - if (decoded <= 0) return 0; - AL.bufferData(buffer, format, bufferData, decoded, parent.buffer.sampleRate); + var i = STREAM_MAX_BUFFERS - requestBuffers, time = streamTell(); + var decoded = readToBufferData(bufferDatas[i]); - var n = STREAM_MAX_BUFFERS - 1; - for (x in i...n) { - bufferDatas[x] = bufferDatas[x + 1]; - bufferTimes[x] = bufferTimes[x + 1]; + if (decoded > 0) { + AL.bufferData(buffer, format, bufferDatas[i], decoded, parent.buffer.sampleRate); + bufferTimes[i] = time; + cycleBuffer(); } - queuedBuffers = requestBuffers; - bufferDatas[n] = bufferData; - bufferTimes[n] = bufferTime; return decoded; } @@ -228,25 +234,35 @@ class NativeAudioSource { private function streamRun() { #if lime_vorbis if (parent == null || parent.buffer == null || handle == null) return dispose(); - if (!playing || streamEnded || disposed) stopStreamTimer(); + if (!playing || disposed) stopStreamTimer(); try { - var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY * Math.min(1, AL.getSourcef(handle, AL.PITCH)), buffer; + var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer; while (processed-- > 0) { - if (streamEnded || --n < 0) unusedBuffers.push(AL.sourceUnqueueBuffer(handle)); - else if (fillBuffer(buffer = AL.sourceUnqueueBuffer(handle)) > 0) AL.sourceQueueBuffer(handle, buffer); - else unusedBuffers.push(buffer); + buffer = AL.sourceUnqueueBuffer(handle); + if (!streamEnded && --n > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer); + else { + queuedBuffers = --requestBuffers; + unusedBuffers.push(buffer); + } } if (!streamEnded) { - if (queuedBuffers < STREAM_MAX_BUFFERS) { + 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--; } - else if (unusedBuffers.length != 0) { - if (fillBuffer(buffer = unusedBuffers.pop()) > 0) AL.sourceQueueBuffer(handle, buffer); - else unusedBuffers.push(buffer); - } } if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { @@ -254,7 +270,7 @@ class NativeAudioSource { resetTimer((getLength() - getCurrentTime()) / getPitch()); } } - catch(e) trace(e); + catch(e) trace('NativeAudioSource Bug! in streamRun $e'); #end } @@ -274,12 +290,10 @@ class NativeAudioSource { } private function timer_onRun() { - if (!streamed || !streamEnded) { - var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); - if (timeRemaining > 30 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { - resetTimer(timeRemaining); - return; - } + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); + if (timeRemaining > 30 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { + resetTimer(timeRemaining); + return; } if (loops == 0) { @@ -287,13 +301,15 @@ class NativeAudioSource { return; } - if (toLoop == 0) { + if (toLoop <= 0) { var start = loopTime != null ? loopTime : 0; setLoops(--loops); if (start > 0 || AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) setCurrentTime(start); } - else if ((loops -= toLoop) < 0) loops = 0; - toLoop = 0; + else { + if ((loops -= toLoop) < 0) loops = 0; + toLoop = 0; + } parent.onLoop.dispatch(); } @@ -320,22 +336,27 @@ class NativeAudioSource { AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); unusedBuffers.resize(0); - streamSeek(getSamples(value)); - for (i in 0...(requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY)) - if (fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); + streamSeek(getSamples(value + parent.offset)); } else - AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value)); + AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset)); if (playing) { var timeRemaining = (getLength() - value) / AL.getSourcef(handle, AL.PITCH); if (timeRemaining > 0) { - if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) AL.sourcePlay(handle); - if (streamed) resetStreamTimer(); + if (streamed) { + for (i in 0...(requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY)) + if (fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); + + resetStreamTimer(); + } resetTimer(timeRemaining); + AL.sourcePlay(handle); } - else + else { + stopStreamTimer(); complete(); + } } return value; @@ -372,8 +393,11 @@ class NativeAudioSource { 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); + if (value <= 0) stopTimer(); + else { + var timeRemaining = (getLength() - getCurrentTime()) / value; + if (timeRemaining > 0) resetTimer(timeRemaining); + } } AL.sourcef(handle, AL.PITCH, value); return value; From 3f19afd66d0c646dc5fd4924784b27675aaa6a64 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:59:09 +0700 Subject: [PATCH 09/17] EndTime, Offset in Ini's Music --- source/funkin/backend/utils/CoolUtil.hx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index f88ef235..bc6a58d3 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(); } /** From e669eb32afd82390b9f3f4e92d91832fa51c9db6 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 13 Jul 2025 19:56:26 +0700 Subject: [PATCH 10/17] Stable Reworked Audio - Reworked Internal Audio to work with Streaming Vorbis Files (for now?) - FlxSound.amplitude, FlxSound.amplitudeLeft, FlxSound.amplitudeRight, SoundChannel.leftPeak, SoundChannel.rightPeak now works. (Streamed Audio on this seems to be half-broken) - Reworked AudioAnalyzer to work with all type of audios (hopefully) and work with Streamed Audio. - Redo AudioSwitchFix to work in time instead of when switching states. gosh, and all that i hope it doesnt break anything. --- source/flixel/sound/FlxSound.hx | 23 +- .../backend/system/modules/AudioSwitchFix.hx | 11 +- source/funkin/backend/utils/AudioAnalyzer.hx | 127 +++++-- source/funkin/backend/utils/CoolUtil.hx | 1 - source/funkin/editors/charter/Charter.hx | 6 +- .../backend/native/NativeAudioSource.hx | 204 +++++----- source/lime/media/AudioBuffer.hx | 9 +- source/lime/media/vorbis/VorbisFile.hx | 356 ++++++++++++++++++ source/openfl/media/SoundChannel.hx | 126 +++---- source/openfl/utils/Assets.hx | 3 +- 10 files changed, 647 insertions(+), 219 deletions(-) create mode 100644 source/lime/media/vorbis/VorbisFile.hx diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index ae596dab..cb3afbe2 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -248,7 +248,6 @@ class FlxSound extends FlxBasic { var _amplitudeLeft:Float; var _amplitudeRight:Float; var _amplitudeUpdate:Bool; - var _amplitudeTime:Float; var _alreadyPaused:Null; public function new() { @@ -468,7 +467,7 @@ class FlxSound extends FlxBasic { function init(?looped:Null, ?autoDestroy:Null, ?onComplete:NullVoid>):FlxSound { if (looped != null) this.looped = looped; if (autoDestroy != null) this.autoDestroy = autoDestroy; - if (onComplete != null) this.onComplete = onComplete; // DEPRECATED + if (onComplete != null) this.onComplete = onComplete; if (_sound != null) makeChannel(); else _length = 0; @@ -483,6 +482,7 @@ class FlxSound extends FlxBasic { * @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; @@ -684,20 +684,19 @@ class FlxSound extends FlxBasic { #end _channel.soundTransform = _transform; - _channel.__lastPeakTime = 0; + _channel.__lastPeakSamples = 0; _channel.__leftPeak = 0; _channel.__rightPeak = 0; _channel.addEventListener(Event.SOUND_COMPLETE, stopped); #if lime_openal _source.__backend.playing = true; #end _source.offset = 0; - _source.currentTime = startTime - _offset; + _source.currentTime = startTime + _offset; #if !lime_openal _source.play(); #end looped = looped; loopTime = loopTime; endTime = endTime; - _amplitudeTime = -1; active = true; } @@ -773,13 +772,11 @@ class FlxSound extends FlxBasic { @:privateAccess return _sound != null ? _sound.__buffer : null; function update_amplitude():Void @:privateAccess { - if (_channel == null || _time == _amplitudeTime || !_amplitudeUpdate) return; - _channel.__updatePeaks(); + if (_channel == null || !_channel.__updatePeaks() || !_amplitudeUpdate) return; _amplitudeUpdate = false; _amplitudeLeft = _channel.__leftPeak; _amplitudeRight = _channel.__rightPeak; - _amplitudeTime = _time; } inline function get_amplitudeLeft():Float { @@ -794,7 +791,7 @@ class FlxSound extends FlxBasic { inline function get_amplitude():Float { update_amplitude(); - return if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft; + return Math.max(_amplitudeLeft, _amplitudeRight);//if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft; } inline function get_channels():Int @@ -845,9 +842,9 @@ class FlxSound extends FlxBasic { return _time; } function get_time():Float { - if (_channel == null || AudioManager.context == null) return _time; + if (_channel == null || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected /*CNE IMPLEMENTATION*/) return _time; - final pos = _channel.position + _offset; + final pos = _channel.position - _offset; if (!playing || _realPitch <= 0) { _lastTime = null; return _time = pos; @@ -876,7 +873,7 @@ class FlxSound extends FlxBasic { } else if (playing) { _source.offset = 0; - _source.currentTime = time - _offset; + _source.currentTime = time + _offset; } } @@ -887,7 +884,7 @@ class FlxSound extends FlxBasic { inline function get_offset():Float return _offset; inline function set_offset(offset:Float):Float { if (_offset == (_offset = offset)) return offset; - //time = time - _offset; + //time = time + _offset; return offset; } diff --git a/source/funkin/backend/system/modules/AudioSwitchFix.hx b/source/funkin/backend/system/modules/AudioSwitchFix.hx index 382fcc0c..25d904db 100644 --- a/source/funkin/backend/system/modules/AudioSwitchFix.hx +++ b/source/funkin/backend/system/modules/AudioSwitchFix.hx @@ -16,8 +16,13 @@ import haxe.Timer; class AudioSwitchFix { public static function onAudioDisconnected() @:privateAccess { var soundList:Array = [FlxG.sound.music]; - for (sound in FlxG.sound.list) if (sound.playing) soundList.push(sound); - for (source in AudioSource.activeSources) source.dispose(); + for (sound in FlxG.sound.list) if (sound.playing) { + sound.pause(); + soundList.push(sound); + } + + var i = AudioSource.activeSources.length; + while (i-- > 0) AudioSource.activeSources[i].dispose(); AudioManager.shutdown(); @@ -36,7 +41,7 @@ class AudioSwitchFix { // #end for (sound in soundList) { - sound._paused = true; + sound.makeChannel(); sound.resume(); } diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 49c5a991..1d944551 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -2,44 +2,121 @@ package funkin.backend.utils; import flixel.sound.FlxSound; import lime.media.AudioBuffer; +import lime.utils.ArrayBufferView; -// 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; +#end + +// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx +// REWRITTEN BY RALTYRO class AudioAnalyzer { 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; + buffer = sound.buffer; - __peakByte = Math.pow(2, buffer.bitsPerSample-1)-1; + __bitUnsignedSize = 1 << buffer.bitsPerSample; + __bitSize = 1 << (buffer.bitsPerSample - 1); + __toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3); - __timeMulti = 1 / (1 / buffer.sampleRate); - __timeMulti *= buffer.bitsPerSample; - __timeMulti -= __timeMulti % buffer.bitsPerSample; + #if lime_vorbis + __prepareVorbis(); + #end } 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; + if (buffer.data != null) return __analyze(startPos, endPos); + #if lime_vorbis + else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos); + #end + return 0; + } - bytesStartPos -= bytesStartPos % buffer.bitsPerSample; - bytesEndPos -= bytesEndPos % buffer.bitsPerSample; + inline function __analyze(startPos:Float, endPos:Float):Float { + var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = #if js buffer.data #else buffer.data.buffer #end; + var max = 0, b = 0; + if (__wordSize == 1) { // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0) + while (pos < end) { + if (max < (b = #if js buf[pos] #else buf.get(pos) #end - __bitSize)) max = b; + pos++; + } + } + else { + pos -= pos % __wordSize; + end -= end % __wordSize; - 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; + var w = 0; + while (pos < end) { + 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; + w = b = 0; + } } - return maxByte/__peakByte; + return max / __bitSize; } + + #if lime_vorbis // As far i know, only native supports vorbis + var __buffer:ArrayBufferView; + 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 ArrayBufferView(0x1000, buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8); + return true; + } + return false; + } + + var prevPos:Int = 0; + inline function __analyzeVorbis(startPos:Float, endPos:Float):Float { + var n = Math.floor((endPos - startPos) * __toBits); + if (n < __wordSize) return 0; + + n -= n % __wordSize; + + if (prevPos != Math.floor(startPos)) { + __vorbis.timeSeek(startPos / 1000); + prevPos = Math.floor(endPos); + } + + var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, bufferSize = __buffer.length, buf = __buffer.buffer; + var max = 0, b = 0, pos = 0, w = 0, result; + while (n > 0) { + n -= (result = __vorbis.read(buf, 0, n < bufferSize ? n : bufferSize, isBigEndian, __wordSize, true)); + + if (result == Vorbis.HOLE) continue; + else if (result <= 0) break; + + while (pos < result) { + 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; + 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 bc6a58d3..2c420393 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -823,7 +823,6 @@ class CoolUtil if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music); frontEnd.list.remove(music); frontEnd.defaultMusicGroup.add(frontEnd.music = music); - music.persist = true; } /** diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 3c6d7c68..fea17595 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -1570,10 +1570,10 @@ class Charter extends UIState { vocals.pause(); for (strumLine in strumLines.members) strumLine.vocals.pause(); } else { - FlxG.sound.music.play(Conductor.songPosition + Conductor.songOffset); - vocals.play(FlxG.sound.music.getActualTime()); + FlxG.sound.music.play(true, Conductor.songPosition + Conductor.songOffset); + vocals.play(true, FlxG.sound.music.getActualTime()); for (strumLine in strumLines.members) { - strumLine.vocals.play(FlxG.sound.music.getActualTime()); + strumLine.vocals.play(true, FlxG.sound.music.getActualTime()); } } } diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 4b187188..33368f95 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -16,7 +16,6 @@ import lime.media.vorbis.VorbisFile; import lime.math.Vector4; import lime.media.AudioBuffer; import lime.media.AudioSource; -import lime.system.System; import lime.utils.ArrayBufferView; #if !lime_debug @@ -26,7 +25,7 @@ import lime.utils.ArrayBufferView; @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) class NativeAudioSource { - private static var STREAM_BUFFER_SIZE:Int = 0x4000; + private static var STREAM_BUFFER_SIZE:Int = 0x4000; // what if it was odd channels (excluding mono). private static var STREAM_MAX_BUFFERS:Int = 32; private static var STREAM_TIMER_FREQUENCY:Int = 100; private static var STREAM_BUFFER_FREQUENCY:Int = 6; @@ -47,6 +46,7 @@ class NativeAudioSource { private var streamTimer:Timer; private var bufferDatas:Array; private var bufferTimes:Array; + private var bufferSizes:Array; private var requestBuffers:Int; private var queuedBuffers:Int; private var toLoop:Int; @@ -74,30 +74,48 @@ class NativeAudioSource { buffers = null; bufferDatas = null; bufferTimes = null; + bufferSizes = null; unusedBuffers = null; } public function init() { if (handle != null) return; - var buffer = parent.buffer; - if (buffer.channels == 1) format = buffer.bitsPerSample == 8 ? AL.FORMAT_MONO8 : AL.FORMAT_MONO16; - else format = buffer.bitsPerSample == 8 ? AL.FORMAT_STEREO8 : AL.FORMAT_STEREO16; - if (disposed = (handle = AL.createSource()) == null) return; AL.sourcef(handle, AL.MAX_GAIN, 10); - if (streamed = buffer.__srcVorbisFile != null) { - dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample == 8 ? 1 : 2); + var buffer = parent.buffer; + var channels = buffer.channels, bitsPerSample = buffer.bitsPerSample; - var constructor = buffer.bitsPerSample == 8 ? Int8 : Int16; + // 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); + } + // 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 lime_vorbis + if (buffer.__srcVorbisFile != null) { + streamed = true; + dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3); + + var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8; buffers = AL.genBuffers(STREAM_MAX_BUFFERS); bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)]; bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; + bufferSizes = [for (i in 0...STREAM_MAX_BUFFERS) 0]; unusedBuffers = []; } - else { - samples = (dataLength = getFloat(Int64.make(0, buffer.data.length))) / buffer.channels / (buffer.bitsPerSample == 8 ? 1 : 2); + else #end { + streamed = false; + samples = (dataLength = getFloat(Int64.make(0, buffer.data.length))) / 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); @@ -129,12 +147,8 @@ class NativeAudioSource { public function stop() { if (!disposed) { - if (AL.getSourcei(handle, AL.SOURCE_STATE) != AL.STOPPED) AL.sourceStop(handle); - if (streamed) { - AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); - unusedBuffers.resize(0); - } - requestBuffers = queuedBuffers = toLoop = 0; + AL.sourceStop(handle); + toLoop = 0; } playing = false; @@ -152,31 +166,29 @@ class NativeAudioSource { // just incase if we support more than 1 in the future?? which i doubt private function streamTell():Float { #if lime_vorbis - /*if (parent.buffer.__srcVorbisFile != null) */return parent.buffer.__srcVorbisFile.timeTell(); - #end + return parent.buffer.__srcVorbisFile.timeTell(); + #else return 0; + #end } private function streamSeek(samples:Int64) { #if lime_vorbis - //if (parent.buffer.__srcVorbisFile != null) { - // 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); - // break; - //} + // 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 } private function readToBufferData(data:ArrayBufferView):Int { - var total = 0, result = 0, wasEOF = false; - var bigEndian = System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize = parent.buffer.bitsPerSample == 8 ? 1 : 2; + #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 < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; - var vorbisFile = parent.buffer.__srcVorbisFile; + var total = 0, result = 0, wasEOF = false; while (total < STREAM_BUFFER_SIZE) { - result = n > 0 ? vorbisFile.read(data.buffer, total, n, bigEndian, wordSize, true) : 0; + result = n > 0 ? parent.buffer.__srcVorbisFile.read(data.buffer, total, n, isBigEndian, wordSize, true) : 0; if (result == Vorbis.HOLE) continue; else if (result <= Vorbis.EREAD) break; @@ -205,36 +217,37 @@ class NativeAudioSource { return result; } return total; - } - - private function cycleBuffer() { - var i = STREAM_MAX_BUFFERS - (queuedBuffers = requestBuffers), n = STREAM_MAX_BUFFERS - 1; - var data = bufferDatas[i], time = bufferTimes[i]; - while (i < n) { - bufferDatas[i] = bufferDatas[i + 1]; - bufferTimes[i] = bufferTimes[++i]; - } - bufferDatas[n] = data; - bufferTimes[n] = time; + #else + return 0; + #end } private function fillBuffer(buffer:ALBuffer):Int { - var i = STREAM_MAX_BUFFERS - requestBuffers, time = streamTell(); - var decoded = readToBufferData(bufferDatas[i]); + var i = STREAM_MAX_BUFFERS - requestBuffers; + var data = bufferDatas[i], time = streamTell(); + var decoded = readToBufferData(data); if (decoded > 0) { - AL.bufferData(buffer, format, bufferDatas[i], decoded, parent.buffer.sampleRate); - bufferTimes[i] = time; - cycleBuffer(); + 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 lime_vorbis if (parent == null || parent.buffer == null || handle == null) return dispose(); - if (!playing || disposed) stopStreamTimer(); + if (!playing || disposed) return stopStreamTimer(); try { var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer; @@ -264,25 +277,28 @@ class NativeAudioSource { else requestBuffers--; } } - - if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { - AL.sourcePlay(handle); - resetTimer((getLength() - getCurrentTime()) / getPitch()); - } } catch(e) trace('NativeAudioSource Bug! in streamRun $e'); - #end + + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) AL.sourcePlay(handle); + if (streamEnded && timer == null) resetTimer((getLength() - getCurrentTime()) / getPitch()); } // Timers - inline function stopStreamTimer() if (streamTimer != null) streamTimer.stop(); + 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(); + inline function stopTimer() { + if (timer != null) timer.stop(); + timer = null; + } private function resetTimer(timeRemaining:Float) { stopTimer(); @@ -290,6 +306,8 @@ class NativeAudioSource { } private function timer_onRun() { + stopTimer(); + var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); if (timeRemaining > 30 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { resetTimer(timeRemaining); @@ -317,66 +335,69 @@ class NativeAudioSource { // Get & Set Methods public function getCurrentTime():Float { if (completed) return getLength(); - else if (!disposed) { - var time = ((streamed ? bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers] : 0) + AL.getSourcef(handle, AL.SEC_OFFSET)) * 1000 - parent.offset; - if (loops > 0 && time > getLength()) { - var start = (loopTime != null ? Math.max(0, loopTime) : 0) + parent.offset; - return ((time - start) % (getLength() - start)) + start; - } - else if (time > 0) return time; + else if (disposed) return 0; + + var time = AL.getSourcef(handle, AL.SAMPLE_OFFSET) / parent.buffer.sampleRate; + if (streamed) 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 0; + + return time - parent.offset; } public function setCurrentTime(value:Float):Float { if (disposed) return value; - if (streamed) { - AL.sourceStop(handle); - AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); - - unusedBuffers.resize(0); - streamSeek(getSamples(value + parent.offset)); - } - else - AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset)); + if (streamed) AL.sourceStop(handle); + else AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset)); if (playing) { - var timeRemaining = (getLength() - value) / AL.getSourcef(handle, AL.PITCH); - if (timeRemaining > 0) { + var timeRemaining = (getLength() - value) / getPitch(); + if (completed = timeRemaining <= 0) complete(); + else { if (streamed) { + AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED)); + + unusedBuffers.resize(0); + streamSeek(getSamples(value + parent.offset)); + for (i in 0...(requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY)) if (fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); resetStreamTimer(); } - resetTimer(timeRemaining); + else + resetTimer(timeRemaining); + AL.sourcePlay(handle); } - else { - stopStreamTimer(); - complete(); - } } + if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000; + return value; } inline private function getRealLength():Float return samples / parent.buffer.sampleRate * 1000; - public function getLength():Null { - return if (length == null) getRealLength() - parent.offset; - else length - parent.offset; - } + public function getLength():Null + return if (disposed) 0; else if (length == null) getRealLength() - parent.offset; else length - parent.offset; public function setLength(value:Null):Null { if (value == length || disposed) return length = value; - var buffer = parent.buffer, wordSize = buffer.bitsPerSample == 8 ? 1 : 2; + 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) setCurrentTime(getCurrentTime()); + if (streamed) { + if (streamEnded || toLoop > 0) setCurrentTime(getCurrentTime()); + } else { var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); if (timeRemaining > 0) resetTimer(timeRemaining); @@ -392,12 +413,9 @@ class NativeAudioSource { public function setPitch(value:Float):Float { if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value; - if (playing) { - if (value <= 0) stopTimer(); - else { - var timeRemaining = (getLength() - getCurrentTime()) / value; - if (timeRemaining > 0) resetTimer(timeRemaining); - } + if (playing && (streamEnded || !streamed)) { + var timeRemaining = (getLength() - getCurrentTime()) / value; + if (timeRemaining > 0) resetTimer(timeRemaining); } AL.sourcef(handle, AL.PITCH, value); return value; @@ -454,6 +472,6 @@ class NativeAudioSource { return position; } - inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >>> 0); + 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 index 87215835..40ebfbec 100644 --- a/source/lime/media/AudioBuffer.hx +++ b/source/lime/media/AudioBuffer.hx @@ -105,10 +105,7 @@ class AudioBuffer __srcBuffer = null; #end #if lime_vorbis - if (__srcVorbisFile != null) { - __srcVorbisFile.clear(); - @:privateAccess __srcVorbisFile.handle = null; - } + if (__srcVorbisFile != null) __srcVorbisFile.clear(); __srcVorbisFile = null; #end } @@ -131,7 +128,7 @@ class AudioBuffer } var audioBuffer = new AudioBuffer(); - audioBuffer.src = new Howl({src: [base64String], html5: true, preload: false}); + audioBuffer.src = new Howl({src: [base64String], preload: false}); return audioBuffer; #elseif (lime_cffi && !macro) #if !cs @@ -177,7 +174,7 @@ class AudioBuffer #if (js && html5 && lime_howlerjs) var audioBuffer = new AudioBuffer(); - audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], html5: true, preload: false}); + audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], preload: false}); return audioBuffer; #elseif (lime_cffi && !macro) 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/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index bccb6138..c5c042af 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -82,7 +82,7 @@ import lime.media.openal.AL; @:noCompletion private var __sound:Sound; @:noCompletion private var __isValid:Bool; @:noCompletion private var __soundTransform:SoundTransform; - @:noCompletion private var __lastPeakTime:Float; + @:noCompletion private var __lastPeakSamples:Float; @:noCompletion private var __leftPeak:Float; @:noCompletion private var __rightPeak:Float; #if lime @@ -151,86 +151,66 @@ import lime.media.openal.AL; this.soundTransform = soundTransform; } - // hi i made these - raltyro - // took me 8 months to figure this out - @:noCompletion inline private static var scanSamples = 480; - - @:noCompletion private function __checkUpdatePeaks(time:Float):Bool + @:noCompletion private function __updatePeaks():Bool { - if (Math.abs(__lastPeakTime - time) < Math.max(1, pitch * 8)) return false; - __lastPeakTime = time; - return true; - } + var backend = __source.__backend; + var samples = AL.getSourcef(backend.handle, AL.SAMPLE_OFFSET); + if (__lastPeakSamples == samples) return false; - /*@:noCompletion private function __updateVorbisPeaks():Void - { - if (__source.buffer == null || !__checkUpdatePeaks(position)) return; - - var index = AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET), bufferDatas = __source.__backend.bufferDatas; - var bufferi = NativeAudioSource.STREAM_MAX_BUFFERS - __source.__backend.queuedBuffers, bytes = bufferDatas[bufferi].buffer; - - var channels = __source.buffer.channels, todo = scanSamples, sample; - var lfilled = false, rfilled = channels < 2; - - while(todo > 0 && (!lfilled || !rfilled)) { - if (index >= NativeAudioSource.STREAM_BUFFER_SIZE) { - if ((bytes = bufferDatas[++bufferi].buffer) == null) break; - index = 0; - } - - if (!lfilled) { - if ((sample = bytes.getUInt16(index * channels * 2) / 65535) > .5) sample = -(sample - 1); - if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; - } - - if (!rfilled) { - if ((sample = bytes.getUInt16(index * channels * 2 + 2) / 65535) > .5) sample = -(sample - 1); - if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; - } - - index++; - todo--; - } - } - - @:noCompletion private function __updatePeaks():Void - { + __lastPeakSamples = samples; __leftPeak = __rightPeak = 0; - - if (!__isValid) return; - - #if lime_vorbis if (__source.__backend.streamed) return __updateVorbisPeaks(); #end + #if (lime_cffi && !macro) + if (!__isValid) return false; var buffer = __source.buffer; - if (buffer == null || buffer.data == null || !__checkUpdatePeaks(position)) return; + var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1); + var pos = Math.floor(samples * buffer.channels * wordSize); + var n = Math.floor(Math.min(buffer.sampleRate / 80, 600)), b = 0, w = 0, c = 0, i = 0, size = 0, buf; - var index = Int64.make(0, AL.getSourcei(__source.__backend.handle, AL.SAMPLE_OFFSET)); - var bytes = buffer.data.buffer, length = __source.__backend.samples; - if (index >= length) return; - - var temp = index + scanSamples; - if (temp < length) length = temp; - - var channels = buffer.channels, sample; - var lfilled = false, rfilled = channels < 2; - - while(index < length && (!lfilled || !rfilled)) { - if (!lfilled) { - if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2))) / 65535) > .5) sample = -(sample - 1); - if (sample > __leftPeak) lfilled = (__leftPeak = sample) >= 1; - } - if (!rfilled) { - if ((sample = bytes.getUInt16(Int64.toInt(index * (channels * 2) + 2)) / 65535) > .5) sample = -(sample - 1); - if (sample > __rightPeak) rfilled = (__rightPeak = sample) >= 1; - } - index++; + if (backend.streamed) { + buf = backend.bufferDatas[i = NativeAudioSource.STREAM_MAX_BUFFERS - backend.queuedBuffers] #if !js .buffer #end; + pos %= (size = NativeAudioSource.STREAM_BUFFER_SIZE * wordSize); + } + else { + buf = buffer.data #if !js .buffer #end; + size = #if js buf.byteLength #else buf.length #end; } - }*/ - @:noCompletion private function __updatePeaks():Void - { - __leftPeak = __rightPeak = 0; - if (!__isValid) return; + pos -= pos % wordSize; + + while (n > 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 (!backend.streamed || ++i >= backend.bufferDatas.length) break; + buf = backend.bufferDatas[i] #if !js .buffer #end; + pos = 0; + } + + if (++c > buffer.channels) { + c = 0; + n--; + } + 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 diff --git a/source/openfl/utils/Assets.hx b/source/openfl/utils/Assets.hx index 0909bdd4..eef85212 100644 --- a/source/openfl/utils/Assets.hx +++ b/source/openfl/utils/Assets.hx @@ -248,8 +248,7 @@ class Assets 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); + if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile)); } #end return getSound(id, useCache); From 2ead99cdf4d6b912acb8afe4a46af338327c6f43 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 13 Jul 2025 21:10:42 +0700 Subject: [PATCH 11/17] Fix Mismatching Audio Streamed Buffer Size --- source/funkin/backend/utils/AudioAnalyzer.hx | 12 ++++++------ .../_internal/backend/native/NativeAudioSource.hx | 15 +++++++++------ source/openfl/media/SoundChannel.hx | 9 ++++----- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 1d944551..8d1d08e2 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -2,11 +2,11 @@ package funkin.backend.utils; import flixel.sound.FlxSound; import lime.media.AudioBuffer; -import lime.utils.ArrayBufferView; #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 @@ -72,12 +72,12 @@ class AudioAnalyzer { } #if lime_vorbis // As far i know, only native supports vorbis - var __buffer:ArrayBufferView; + var __buffer:ArrayBuffer; 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 ArrayBufferView(0x1000, buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8); + __buffer = new ArrayBuffer(0x400 * buffer.channels * (buffer.bitsPerSample >> 3)); return true; } return false; @@ -95,17 +95,17 @@ class AudioAnalyzer { prevPos = Math.floor(endPos); } - var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, bufferSize = __buffer.length, buf = __buffer.buffer; + var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, bufferSize = __buffer.length; var max = 0, b = 0, pos = 0, w = 0, result; while (n > 0) { - n -= (result = __vorbis.read(buf, 0, n < bufferSize ? n : bufferSize, isBigEndian, __wordSize, true)); + 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) { while (w < buffer.bitsPerSample) { - b |= #if js buf[pos] #else buf.get(pos) #end << w; + b |= #if js __buffer[pos] #else __buffer.get(pos) #end << w; w += 8; pos++; } diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 33368f95..3ef6b002 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -25,7 +25,7 @@ import lime.utils.ArrayBufferView; @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) class NativeAudioSource { - private static var STREAM_BUFFER_SIZE:Int = 0x4000; // what if it was odd channels (excluding mono). + private static var STREAM_BUFFER_SIZE: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; @@ -47,6 +47,7 @@ class NativeAudioSource { 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; @@ -107,11 +108,13 @@ class NativeAudioSource { dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3); var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8; + bufferSize = STREAM_BUFFER_SIZE * buffer.channels * (constructor == Int8 ? buffer.bitsPerSample >> 3 : 1); buffers = AL.genBuffers(STREAM_MAX_BUFFERS); - bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(STREAM_BUFFER_SIZE, constructor)]; + 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 #end { streamed = false; @@ -184,10 +187,10 @@ class NativeAudioSource { #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 < STREAM_BUFFER_SIZE ? Math.floor(size) : STREAM_BUFFER_SIZE; + var n:Int = size < bufferSize ? Math.floor(size) : bufferSize; var total = 0, result = 0, wasEOF = false; - while (total < STREAM_BUFFER_SIZE) { + while (total < bufferSize) { result = n > 0 ? parent.buffer.__srcVorbisFile.read(data.buffer, total, n, isBigEndian, wordSize, true) : 0; if (result == Vorbis.HOLE) continue; @@ -200,7 +203,7 @@ class NativeAudioSource { var samples = getSamples(loopTime != null ? loopTime + parent.offset : parent.offset); streamSeek(samples); - if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = STREAM_BUFFER_SIZE - total)) + if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = bufferSize - total)) n = Math.floor(size); } } @@ -211,7 +214,7 @@ class NativeAudioSource { } } - if (total < STREAM_BUFFER_SIZE) data.buffer.fill(total, n, 0); + if (total < bufferSize) data.buffer.fill(total, n, 0); if (result < 0) { trace('NativeAudioSource Streaming Bug! reading result is $result'); return result; diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index c5c042af..32f24c58 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -164,20 +164,19 @@ import lime.media.openal.AL; var buffer = __source.buffer; var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1); - var pos = Math.floor(samples * buffer.channels * wordSize); - var n = Math.floor(Math.min(buffer.sampleRate / 80, 600)), b = 0, w = 0, c = 0, i = 0, size = 0, buf; + var pos = Math.floor(samples * buffer.channels * wordSize), size = 0, i = 0, buf; + pos -= pos % wordSize; if (backend.streamed) { buf = backend.bufferDatas[i = NativeAudioSource.STREAM_MAX_BUFFERS - backend.queuedBuffers] #if !js .buffer #end; - pos %= (size = NativeAudioSource.STREAM_BUFFER_SIZE * wordSize); + size = backend.bufferSize; } else { buf = buffer.data #if !js .buffer #end; size = #if js buf.byteLength #else buf.length #end; } - pos -= pos % wordSize; - + var n = Math.floor(Math.min(buffer.sampleRate / 80, 512)), b = 0, w = 0, c = 0; while (n > 0) { if (wordSize == 1) b = #if js buf[pos] #else buf.get(pos) #end - bitSize; else { From a3ae8a70d4b52631ac1c40371dd6a8739bf8a5a2 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:03:11 +0700 Subject: [PATCH 12/17] Allow AudioAnalyzer to have seperate outputs --- source/funkin/backend/utils/AudioAnalyzer.hx | 36 ++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 8d1d08e2..95f0b174 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -34,20 +34,29 @@ class AudioAnalyzer { #end } - public function analyze(startPos:Float, endPos:Float):Float { - if (buffer.data != null) return __analyze(startPos, endPos); + public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float { + if (buffer.data != null) return __analyze(startPos, endPos, outSeperate); #if lime_vorbis - else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos); + else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate); #end return 0; } - inline function __analyze(startPos:Float, endPos:Float):Float { + inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float { var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = #if js buffer.data #else buffer.data.buffer #end; - var max = 0, b = 0; + var max = 0, b = 0, c = 0, useSeperate = outSeperate != null; + if (useSeperate) { + while (c < outSeperate.length) outSeperate[c++] *= __bitSize; + while (c++ < buffer.channels) outSeperate.push(0); + c = 0; + } if (__wordSize == 1) { // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0) while (pos < end) { if (max < (b = #if js buf[pos] #else buf.get(pos) #end - __bitSize)) max = b; + if (useSeperate) { + if (outSeperate[c] < b) outSeperate[c] = b; + if (++c > buffer.channels) c = 0; + } pos++; } } @@ -64,6 +73,10 @@ class AudioAnalyzer { } 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; } } @@ -84,7 +97,7 @@ class AudioAnalyzer { } var prevPos:Int = 0; - inline function __analyzeVorbis(startPos:Float, endPos:Float):Float { + inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array):Float { var n = Math.floor((endPos - startPos) * __toBits); if (n < __wordSize) return 0; @@ -96,7 +109,12 @@ class AudioAnalyzer { } var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, bufferSize = __buffer.length; - var max = 0, b = 0, pos = 0, w = 0, result; + var max = 0, b = 0, c = 0, useSeperate = outSeperate != null, pos = 0, w = 0, result; + if (useSeperate) { + while (c < outSeperate.length) outSeperate[c++] *= __bitSize; + while (c++ < buffer.channels) outSeperate.push(0); + c = 0; + } while (n > 0) { n -= (result = __vorbis.read(__buffer, 0, n < bufferSize ? n : bufferSize, isBigEndian, __wordSize, true)); @@ -111,6 +129,10 @@ class AudioAnalyzer { } 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; From 17ffdcd6ef1976f2e25b4c3d40a92dbb15b7e1e2 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:41:41 +0700 Subject: [PATCH 13/17] Fix AudioAnalyzer --- source/flixel/sound/FlxSound.hx | 4 ++-- source/funkin/backend/utils/AudioAnalyzer.hx | 4 ++-- source/openfl/media/SoundChannel.hx | 24 ++++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index cb3afbe2..fdf59233 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -684,7 +684,7 @@ class FlxSound extends FlxBasic { #end _channel.soundTransform = _transform; - _channel.__lastPeakSamples = 0; + _channel.__lastPeakTime = -10; _channel.__leftPeak = 0; _channel.__rightPeak = 0; _channel.addEventListener(Event.SOUND_COMPLETE, stopped); @@ -772,7 +772,7 @@ class FlxSound extends FlxBasic { @:privateAccess return _sound != null ? _sound.__buffer : null; function update_amplitude():Void @:privateAccess { - if (_channel == null || !_channel.__updatePeaks() || !_amplitudeUpdate) return; + if (_channel == null || !_channel.__updatePeaks(get_time()) || !_amplitudeUpdate) return; _amplitudeUpdate = false; _amplitudeLeft = _channel.__leftPeak; diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 95f0b174..e3a0fc63 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -48,7 +48,7 @@ class AudioAnalyzer { if (useSeperate) { while (c < outSeperate.length) outSeperate[c++] *= __bitSize; while (c++ < buffer.channels) outSeperate.push(0); - c = 0; + c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize); } if (__wordSize == 1) { // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0) while (pos < end) { @@ -113,7 +113,7 @@ class AudioAnalyzer { if (useSeperate) { while (c < outSeperate.length) outSeperate[c++] *= __bitSize; while (c++ < buffer.channels) outSeperate.push(0); - c = 0; + c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize); } while (n > 0) { n -= (result = __vorbis.read(__buffer, 0, n < bufferSize ? n : bufferSize, isBigEndian, __wordSize, true)); diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 32f24c58..9de0928e 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -82,7 +82,7 @@ import lime.media.openal.AL; @:noCompletion private var __sound:Sound; @:noCompletion private var __isValid:Bool; @:noCompletion private var __soundTransform:SoundTransform; - @:noCompletion private var __lastPeakSamples:Float; + @:noCompletion private var __lastPeakTime:Float; @:noCompletion private var __leftPeak:Float; @:noCompletion private var __rightPeak:Float; #if lime @@ -151,32 +151,32 @@ import lime.media.openal.AL; this.soundTransform = soundTransform; } - @:noCompletion private function __updatePeaks():Bool + @:noCompletion private function __updatePeaks(time:Float):Bool { - var backend = __source.__backend; - var samples = AL.getSourcef(backend.handle, AL.SAMPLE_OFFSET); - if (__lastPeakSamples == samples) return false; + if (Math.abs(time - __lastPeakTime) < 8) return false; - __lastPeakSamples = samples; + __lastPeakTime = time; __leftPeak = __rightPeak = 0; #if (lime_cffi && !macro) if (!__isValid) return false; - var buffer = __source.buffer; + var backend = __source.__backend, buffer = __source.buffer; var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1); - var pos = Math.floor(samples * buffer.channels * wordSize), size = 0, i = 0, buf; - pos -= pos % wordSize; + var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize), size = 0, i = 0, buf; if (backend.streamed) { buf = backend.bufferDatas[i = NativeAudioSource.STREAM_MAX_BUFFERS - backend.queuedBuffers] #if !js .buffer #end; size = backend.bufferSize; + pos -= Math.floor(backend.bufferTimes[i] * buffer.sampleRate * buffer.channels * wordSize); } else { buf = buffer.data #if !js .buffer #end; size = #if js buf.byteLength #else buf.length #end; } - var n = Math.floor(Math.min(buffer.sampleRate / 80, 512)), b = 0, w = 0, c = 0; + var n = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0; + pos -= pos % wordSize; + while (n > 0) { if (wordSize == 1) b = #if js buf[pos] #else buf.get(pos) #end - bitSize; else { @@ -378,13 +378,13 @@ import lime.media.openal.AL; @:noCompletion private function get_leftPeak():Float { - __updatePeaks(); + __updatePeaks(get_position()); return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume); } @:noCompletion private function get_rightPeak():Float { - __updatePeaks(); + __updatePeaks(get_position()); return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume); } From a708c204e71d7fb9326ca543820029e8bf3acaf4 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:58:14 +0700 Subject: [PATCH 14/17] Little fix on AudioAnalyzer --- source/funkin/backend/utils/AudioAnalyzer.hx | 53 +++++++++----------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index e3a0fc63..6364a0ee 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -43,42 +43,34 @@ class AudioAnalyzer { } inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float { - var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = #if js buffer.data #else buffer.data.buffer #end; - var max = 0, b = 0, c = 0, useSeperate = outSeperate != null; + 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); } - if (__wordSize == 1) { // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0) - while (pos < end) { - if (max < (b = #if js buf[pos] #else buf.get(pos) #end - __bitSize)) max = b; - if (useSeperate) { - if (outSeperate[c] < b) outSeperate[c] = b; - if (++c > buffer.channels) c = 0; - } - pos++; - } - } - else { - pos -= pos % __wordSize; - end -= end % __wordSize; - var w = 0; - while (pos < end) { + 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; } + 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; @@ -113,7 +105,7 @@ class AudioAnalyzer { if (useSeperate) { while (c < outSeperate.length) outSeperate[c++] *= __bitSize; while (c++ < buffer.channels) outSeperate.push(0); - c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize); + c = 0; } while (n > 0) { n -= (result = __vorbis.read(__buffer, 0, n < bufferSize ? n : bufferSize, isBigEndian, __wordSize, true)); @@ -122,12 +114,15 @@ class AudioAnalyzer { else if (result <= 0) break; while (pos < result) { - while (w < buffer.bitsPerSample) { - b |= #if js __buffer[pos] #else __buffer.get(pos) #end << w; - w += 8; - pos++; + 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 (b > __bitSize) b -= __bitUnsignedSize; if (max < b) max = b; if (useSeperate) { if (outSeperate[c] < b) outSeperate[c] = b; From 7f3d976ac6dcda28fdbcb4700c066ff477c2a319 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Tue, 15 Jul 2025 01:22:53 +0700 Subject: [PATCH 15/17] Fix FlxSound not completing correctly, & callbacks --- source/flixel/sound/FlxSound.hx | 29 ++++--- .../backend/native/NativeAudioSource.hx | 80 +++++++++++-------- source/openfl/media/Sound.hx | 24 +----- 3 files changed, 70 insertions(+), 63 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index fdf59233..a1719197 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -296,8 +296,9 @@ class FlxSound extends FlxBasic { active = false; _lastTime = null; - if (_channel != null) { - _channel.removeEventListener(Event.SOUND_COMPLETE, stopped); + if (_source != null) { + _source.onComplete.remove(stopped); + _source.onLoop.remove(source_looped); } if (destroySound) { @@ -496,8 +497,8 @@ class FlxSound extends FlxBasic { _transform.pan = 0; _length = _source.length; - _source.onComplete.add(_channel.source_onComplete); - _source.onLoop.add(_channel.source_onLoop); + _source.onComplete.add(stopped); + _source.onLoop.add(source_looped); _channel.__soundTransform = _transform; _channel.__isValid = true; } @@ -674,7 +675,7 @@ class FlxSound extends FlxBasic { _paused = false; _time = startTime; _lastTime = FlxG.game.getTicks(); - if (_channel == null || !_channel.__isValid || _source.__backend == null #if lime_openal || _source.__backend.disposed || _source.__backend.handle == null #end) + if (_channel == null || !_channel.__isValid || _source.__backend == null #if lime_cffi || _source.__backend.disposed || _source.__backend.handle == null #end) makeChannel(); if (_channel != null) { @@ -687,12 +688,11 @@ class FlxSound extends FlxBasic { _channel.__lastPeakTime = -10; _channel.__leftPeak = 0; _channel.__rightPeak = 0; - _channel.addEventListener(Event.SOUND_COMPLETE, stopped); - #if lime_openal _source.__backend.playing = true; #end + #if lime_cffi _source.__backend.playing = true; #end _source.offset = 0; _source.currentTime = startTime + _offset; - #if !lime_openal _source.play(); #end + #if !lime_cffi _source.play(); #end looped = looped; loopTime = loopTime; @@ -706,7 +706,7 @@ class FlxSound extends FlxBasic { } } - function stopped(?_) { + function stopped() { onFinish.dispatch(); if (onComplete != null) onComplete(); @@ -718,6 +718,17 @@ class FlxSound extends FlxBasic { 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). */ diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 3ef6b002..75832ee4 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -25,7 +25,7 @@ import lime.utils.ArrayBufferView; @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) class NativeAudioSource { - private static var STREAM_BUFFER_SIZE:Int = 0x1000; + 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; @@ -102,13 +102,22 @@ class NativeAudioSource { 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 - if (buffer.__srcVorbisFile != null) { + else if (buffer.__srcVorbisFile != null) { streamed = true; dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3); var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8; - bufferSize = STREAM_BUFFER_SIZE * buffer.channels * (constructor == Int8 ? buffer.bitsPerSample >> 3 : 1); + 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]; @@ -116,15 +125,7 @@ class NativeAudioSource { unusedBuffers = []; bufferSize *= constructor == Int8 ? 1 : buffer.bitsPerSample >> 3; } - else #end { - streamed = false; - samples = (dataLength = getFloat(Int64.make(0, buffer.data.length))) / 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); - } + #end if (dataLength == 0) { trace('NativeAudioSource Bug! dataLength is 0'); @@ -188,6 +189,7 @@ class NativeAudioSource { 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); var total = 0, result = 0, wasEOF = false; while (total < bufferSize) { @@ -203,8 +205,8 @@ class NativeAudioSource { 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); + if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = bufferSize - total)) n = Math.floor(size); + n -= n % (parent.buffer.channels * wordSize); } } else { @@ -216,7 +218,7 @@ class NativeAudioSource { if (total < bufferSize) data.buffer.fill(total, n, 0); if (result < 0) { - trace('NativeAudioSource Streaming Bug! reading result is $result'); + trace('NativeAudioSource Streaming Bug! reading result is $result, streamEnded: $streamEnded, total: $total, n: $n'); return result; } return total; @@ -283,8 +285,10 @@ class NativeAudioSource { } catch(e) trace('NativeAudioSource Bug! in streamRun $e'); - if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) AL.sourcePlay(handle); - if (streamEnded && timer == null) resetTimer((getLength() - getCurrentTime()) / getPitch()); + if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + AL.sourcePlay(handle); + resetTimer((getLength() - getCurrentTime()) / getPitch()); + } } // Timers @@ -309,10 +313,8 @@ class NativeAudioSource { } private function timer_onRun() { - stopTimer(); - var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); - if (timeRemaining > 30 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) { + if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING && (!streamed || !streamEnded)) { resetTimer(timeRemaining); return; } @@ -322,14 +324,16 @@ class NativeAudioSource { return; } + var start = loopTime != null ? loopTime : 0; if (toLoop <= 0) { - var start = loopTime != null ? loopTime : 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(); @@ -337,11 +341,18 @@ class NativeAudioSource { // Get & Set Methods public function getCurrentTime():Float { - if (completed) return getLength(); - else if (disposed) return 0; + if (disposed) return 0; + else if (completed) return getLength(); var time = AL.getSourcef(handle, AL.SAMPLE_OFFSET) / parent.buffer.sampleRate; - if (streamed) time += bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers]; + if (streamed) { + if (playing && streamEnded && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) { + complete(); + return getLength(); + } + else + time += bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers]; + } time *= 1000; var length = if (length == null) getRealLength(); else length; @@ -361,22 +372,25 @@ class NativeAudioSource { if (playing) { var timeRemaining = (getLength() - value) / getPitch(); - if (completed = timeRemaining <= 0) complete(); + 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)); - for (i in 0...(requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY)) - if (fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); + requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY; + for (i in 0...queuedBuffers) { + if (!streamEnded && fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]); + else queuedBuffers = --requestBuffers; + } - resetStreamTimer(); + if (!streamEnded) resetStreamTimer(); } - else - resetTimer(timeRemaining); + resetTimer(timeRemaining); AL.sourcePlay(handle); } } @@ -398,9 +412,7 @@ class NativeAudioSource { else dataLength = Math.ffloor(Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate) * buffer.channels * wordSize; if (playing) { - if (streamed) { - if (streamEnded || toLoop > 0) setCurrentTime(getCurrentTime()); - } + if (streamed && (streamEnded || toLoop > 0)) setCurrentTime(getCurrentTime()); else { var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch(); if (timeRemaining > 0) resetTimer(timeRemaining); @@ -416,7 +428,7 @@ class NativeAudioSource { public function setPitch(value:Float):Float { if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value; - if (playing && (streamEnded || !streamed)) { + if (playing) { var timeRemaining = (getLength() - getCurrentTime()) / value; if (timeRemaining > 0) resetTimer(timeRemaining); } diff --git a/source/openfl/media/Sound.hx b/source/openfl/media/Sound.hx index 1b4a8d5e..808399c3 100644 --- a/source/openfl/media/Sound.hx +++ b/source/openfl/media/Sound.hx @@ -672,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 } From bffb147cc2a013264f65ec29227a242b773f7d08 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Tue, 15 Jul 2025 01:23:52 +0700 Subject: [PATCH 16/17] Polish AudioAnalyzer & fix 8-bit detection --- source/funkin/backend/utils/AudioAnalyzer.hx | 93 +++++++++++++++----- source/openfl/media/SoundChannel.hx | 32 ++++--- 2 files changed, 88 insertions(+), 37 deletions(-) diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 6364a0ee..0ad9af3d 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -12,6 +12,7 @@ import lime.utils.ArrayBuffer; // 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 __toBits:Float; var __wordSize:Int; @@ -23,18 +24,19 @@ class AudioAnalyzer { #end public function new(sound:FlxSound) { - buffer = sound.buffer; + this.sound = sound; + __check(); + } + inline function __check() if (sound.buffer != buffer) { + __bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1); __bitUnsignedSize = 1 << buffer.bitsPerSample; - __bitSize = 1 << (buffer.bitsPerSample - 1); __toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3); - - #if lime_vorbis - __prepareVorbis(); - #end + __vorbis = null; } 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); @@ -56,7 +58,7 @@ class AudioAnalyzer { 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; + 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; @@ -78,43 +80,88 @@ class AudioAnalyzer { #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(0x400 * buffer.channels * (buffer.bitsPerSample >> 3)); + __buffer = new ArrayBuffer(__bufferSize = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3)); return true; } return false; } - var prevPos:Int = 0; - inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array):Float { + inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array):Float @:privateAccess { var n = Math.floor((endPos - startPos) * __toBits); - if (n < __wordSize) return 0; + if ((n -= n % __wordSize) < __wordSize) return 0; - n -= n % __wordSize; - - if (prevPos != Math.floor(startPos)) { - __vorbis.timeSeek(startPos / 1000); - prevPos = Math.floor(endPos); - } - - var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, bufferSize = __buffer.length; - var max = 0, b = 0, c = 0, useSeperate = outSeperate != null, pos = 0, w = 0, result; + 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); - c = 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)); + 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; + if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize; else { while (w < buffer.bitsPerSample) { b |= __buffer.get(pos) << w; diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index 9de0928e..6ccbf8ac 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -8,8 +8,6 @@ import openfl.events.EventDispatcher; #if lime import lime.media.AudioSource; import lime.media.openal.AL; - -#if lime_cffi import lime._internal.backend.native.NativeAudioSource; #end #end /** @@ -157,28 +155,32 @@ import lime.media.openal.AL; __lastPeakTime = time; __leftPeak = __rightPeak = 0; - #if (lime_cffi && !macro) + + #if !macro if (!__isValid) return false; - var backend = __source.__backend, buffer = __source.buffer; + 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, i = 0, buf; + 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) { - buf = backend.bufferDatas[i = NativeAudioSource.STREAM_MAX_BUFFERS - backend.queuedBuffers] #if !js .buffer #end; - size = backend.bufferSize; + 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 { + else + #end { buf = buffer.data #if !js .buffer #end; size = #if js buf.byteLength #else buf.length #end; } - var n = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0; + 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 (n > 0) { - if (wordSize == 1) b = #if js buf[pos] #else buf.get(pos) #end - bitSize; + 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; @@ -189,15 +191,17 @@ import lime.media.openal.AL; } ((c % 2 == 0) ? (if (__leftPeak < b) __leftPeak = b) : (if (__rightPeak < b) __rightPeak = b)); - if (pos >= size) { + if (pos >= size) #if lime_cffi { if (!backend.streamed || ++i >= backend.bufferDatas.length) break; - buf = backend.bufferDatas[i] #if !js .buffer #end; + size = backend.bufferSizes[i]; + buf = backend.bufferDatas[i].buffer; pos = 0; } + #else break; #end if (++c > buffer.channels) { c = 0; - n--; + s--; } w = b = 0; } From f2a5719c578db91da786eb60a1aab30ab05e855a Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Tue, 15 Jul 2025 01:39:10 +0700 Subject: [PATCH 17/17] Whoops --- source/flixel/sound/FlxSound.hx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index a1719197..0716dc06 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -296,11 +296,6 @@ class FlxSound extends FlxBasic { active = false; _lastTime = null; - if (_source != null) { - _source.onComplete.remove(stopped); - _source.onLoop.remove(source_looped); - } - if (destroySound) { onFinish.removeAll(); @@ -310,7 +305,11 @@ class FlxSound extends FlxBasic { _channel.stop(); _channel = null; } - if (_source != null) _source.dispose(); + if (_source != null) { + _source.onComplete.remove(stopped); + _source.onLoop.remove(source_looped); + _source.dispose(); + } _source = null; _sound = null;