Streamed Music (Rewritten Internal Audio) PR Merge

- Reworked Internal Audio (FlxSound, NativeAudioSource, SoundChannel) to work with Streaming Vorbis Files (for now?)
  - FlxSound.amplitude now works. (This is implemented to avoid the lagspike on using the AudioAnalyzer on streamed audios as possible)
  - Implemented loopTime, and endTime into the internal instead of FlxSound for better looping (only for Streamed Audio)
  - Implemented Interpolation into time (only in FlxSound specifically, does not affect the actual internal audio classes)
  - Implemented variables such as offset, muted, pan, proximityPan, channels, stereo, loaded, streamed, onFinish (to seperate complete callback from looping callback), timeScaleBased (for pitch based in FlxG.timeScale)

- Reworked AudioAnalyzer to work much more faster with all type of audios (hopefully) and work with Streamed Audio. (NOTE: Using it on Streamed Audio is pretty expensive as it constantly seeking each calls, unless if the next time using it is the expected time decoder to seek to.)
- Redo AudioSwitchFix to work in time instead of when switching states.
- Improved the off-sync vocals detection
- Implemented LoopTime, EndTime, Offset into Ini's Music Assets
This commit is contained in:
Ralty
2025-07-15 01:41:10 +07:00
committed by GitHub
19 changed files with 2828 additions and 783 deletions
+917
View File
@@ -0,0 +1,917 @@
package flixel.sound;
import lime.media.AudioBuffer;
import lime.media.AudioSource;
import lime.media.AudioManager;
import openfl.Assets;
import openfl.events.IEventDispatcher;
import openfl.events.Event;
import openfl.media.Sound;
import openfl.media.SoundChannel;
import openfl.media.SoundTransform;
import openfl.media.SoundMixer;
import openfl.net.URLRequest;
import openfl.utils.AssetType;
#if flash11
import openfl.utils.ByteArray;
#end
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.tweens.FlxTween;
import flixel.util.FlxSignal;
import flixel.util.FlxStringUtil;
import flixel.FlxBasic;
import flixel.FlxObject;
import flixel.FlxG;
@:access(flixel.FlxGame)
class FlxSound extends FlxBasic {
/**
* The x position of this sound in world coordinates.
* Only really matters if you are doing proximity/panning stuff.
*/
public var x:Float;
/**
* The y position of this sound in world coordinates.
* Only really matters if you are doing proximity/panning stuff.
*/
public var y:Float;
/**
* Whether or not this sound should be automatically destroyed when you switch states.
*/
public var persist:Bool;
/**
* Whether or not the sound is currently playing.
*/
public var playing(get, never):Bool;
/**
* Set volume to a value between 0 and 1* to change how this sound is.
*/
public var volume(get, set):Float;
/**
* Whether to make this sound muted or not.
*/
public var muted(default, set):Bool;
#if FLX_PITCH
/**
* Set pitch, which also alters the playback speed. Default is 1.
* @since 5.0.0
*/
public var pitch(get, set):Float;
/**
* Alters the pitch of the sound depends on the current FlxG.timeScale. Default is true.
* @since raltyMod
*/
public var timeScaleBased:Bool;
#end
/**
* Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this.
*/
public var pan(get, set):Float;
/**
* The position in runtime of the music playback in milliseconds.
* If set while paused, changes only come into effect after a `resume()` call.
*/
public var time(get, set):Float;
/**
* The offset for this FlxSound.
* Useful for just generally offsetting this sound without affecting time.
* @since raltyMod
*/
public var offset(get, set):Float;
/**
* The length of the sound in milliseconds.
* @since 4.2.0
*/
public var length(get, never):Float;
/**
* The latency of the sound in milliseconds.
* @since raltyMod
*/
//public var latency(get, never):Float;
/**
* Whether or not this sound should loop.
*/
public var looped(default, set):Bool;
/**
* In case of looping, the point (in milliseconds) from where to restart the sound when it loops back
* @since 4.1.0
*/
public var loopTime(default, set):Float;
/**
* At which point to stop playing the sound, in milliseconds.
* If not set / `null`, the sound completes normally.
* @since 4.2.0
*/
public var endTime(default, set):Null<Float>;
/**
* The sound's "target" (for proximity and panning).
* @since raltyMod
*/
public var target:Null<FlxObject>;
/**
* The maximum effective radius of this sound (for proximity and panning).
* @since raltyMod
*/
public var radius:Float;
/**
* Whether the proximity alters the pan or not
* @since raltyMod
*/
public var proximityPan:Bool;
/**
* Stores for how much channels are in the loaded sound.
* @since raltyMod
*/
public var channels(get, never):Int;
/**
* Whether or not this sound is stereo instead of mono.
* @since raltyMod
*/
public var stereo(get, never):Bool;
/**
* Whether or not this sound is loaded yet.
* @since raltyMod
*/
public var loaded(get, never):Bool;
/**
* Whether or not this sound is streamed, only vorbis though.
* @since raltyMod
*/
public var streamed(get, never):Bool;
/**
* Stores the average wave amplitude of both stereo channels.
* @since raltyMod
*/
public var amplitude(get, never):Float;
/**
* Just the amplitude of the left stereo channel
* @since raltyMod
*/
public var amplitudeLeft(get, never):Float;
/**
* Just the amplitude of the right stereo channel
* @since raltyMod
*/
public var amplitudeRight(get, never):Float;
/**
* The ID3 song name. Defaults to null. Currently only works for MP3 streamed sounds.
*/
public var name(default, null):String;
/**
* The ID3 artist name. Defaults to null. Currently only works for MP3 streamed sounds.
*/
public var artist(default, null):String;
/**
* Whether to call `destroy()` when the sound has finished playing.
*/
public var autoDestroy:Bool;
/**
* Signal that is dispatched on sound complete.
* Seperates the looping from onComplete.
*/
public final onFinish:FlxSignal;
/**
* Tracker for sound complete callback. If assigned, will be called
* each time when sound reaches its end.
*/
//@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.")
public var onComplete:Void->Void;
/**
* The sound group this sound belongs to
*/
public var group(default, set):FlxSoundGroup;
/**
* Stores the sound lime AudioBuffer if exists.
* @since raltyMod
*/
public var buffer(get, never):AudioBuffer;
/**
* The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`)
* @since 4.1.0
*/
public var fadeTween:FlxTween;
@:allow(flixel.system.frontEnds.SoundFrontEnd.load) var _sound:Sound;
var _transform:SoundTransform;
var _channel:SoundChannel;
var _source:AudioSource;
var _paused:Bool;
var _volume:Float;
var _volumeAdjust:Float;
var _time:Float;
var _offset:Float;
var _timeInterpolation:Float;
var _lastTime:Null<Float>; // FlxG.game.getTicks(), in MS
var _length:Float;
#if FLX_PITCH
var _pitch:Float;
var _timeScaleAdjust:Float;
var _realPitch:Float;
#end
var _amplitudeLeft:Float;
var _amplitudeRight:Float;
var _amplitudeUpdate:Bool;
var _alreadyPaused:Null<Bool>;
public function new() {
super();
onFinish = new FlxSignal();
revive();
}
/**
* Resets this FlxSound properties.
*
* @param clean Whether if this FlxSound also needs to be cleaned up too.
*/
public function reset():Void {
if (_source != null) stop();
x = y = 0;
muted = false;
looped = false;
loopTime = 0;
endTime = null;
autoDestroy = false;
visible = false;
target = null;
radius = 0;
proximityPan = true;
onComplete = null;
timeScaleBased = true;
_alreadyPaused = null;
_cameras = null;
_lastTime = null;
_paused = false;
_offset = _length = _time = 0;
_volume = _volumeAdjust = 1;
_amplitudeLeft = _amplitudeRight = 0;
_amplitudeUpdate = true;
#if FLX_PITCH _pitch = _realPitch = _timeScaleAdjust = 1; #end
}
/**
* Internal cleanup function for cleaning up this FlxSound.
* @since raltyMod
*/
function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess {
active = false;
_lastTime = null;
if (destroySound) {
onFinish.removeAll();
if (group != null) group.remove(this);
if (_channel != null) {
_channel.stop();
_channel = null;
}
if (_source != null) {
_source.onComplete.remove(stopped);
_source.onLoop.remove(source_looped);
_source.dispose();
}
_source = null;
_sound = null;
reset();
}
else if (_channel != null && _channel.__isValid) {
if (resetPosition) {
_source.stop();
_time = 0;
_paused = false;
}
else if (!_paused) {
get_time();
_source.pause();
}
}
}
/**
* Handles fade out, fade in, panning, proximity, and amplitude operations each frame.
*/
override function update(elapsed:Float):Void {
if (!playing) return;
var timeScaleTarget = timeScaleBased ? FlxG.timeScale : 1.0;
if (_timeScaleAdjust != timeScaleTarget) {
_timeScaleAdjust = timeScaleTarget;
pitch = _pitch;
if (_channel == null) return;
}
_amplitudeUpdate = true;
var radialMultiplier = 1.0;
// Distance-based volume control (TODO for Ralty: REDO THIS)
if (target != null) {
var targetPosition = target.getPosition();
radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / radius;
targetPosition.put();
radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1);
if (proximityPan && _transform != null) {
var d:Float = (x - target.x) / radius;
_transform.pan = FlxMath.bound(d, -1, 1);
}
}
_volumeAdjust = radialMultiplier;
updateTransform();
}
override function revive() {
reset();
super.revive();
}
override function kill() {
super.kill();
reset();
}
override function destroy() {
super.destroy();
cleanup(true);
}
/**
* One of the main setup functions for sounds, this function loads a sound from an embedded MP3.
*
* @param embeddedSound An embedded Class object representing an MP3 file.
* @param looped Whether or not this sound should loop endlessly.
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
* @param onComplete Called when the sound finished playing
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function loadEmbedded(embeddedSound:FlxSoundAsset, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
if (!exists || embeddedSound == null) return this;
cleanup(true);
if ((embeddedSound is Sound)) _sound = embeddedSound;
else if ((embeddedSound is Class)) _sound = Type.createInstance(embeddedSound, []);
else if ((embeddedSound is String)) {
if (Assets.exists(embeddedSound, AssetType.MUSIC) && this == FlxG.sound.music)
_sound = Assets.getMusic(embeddedSound);
else if (Assets.exists(embeddedSound, AssetType.SOUND))
_sound = Assets.getSound(embeddedSound);
else
FlxG.log.error('Could not find a Sound asset with an ID of \'$embeddedSound\'.');
}
return init(looped, autoDestroy, onComplete);
}
/**
* One of the main setup functions for sounds, this function loads a sound from a URL.
*
* @param soundURL A string representing the URL of the MP3 file you want to play.
* @param looped Whether or not this sound should loop endlessly.
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
* @param onComplete Called when the sound finished playing
* @param onLoad Called when the sound finished loading.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function loadStream(soundURL:String, looped = false, autoDestroy = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound {
if (!exists) return this;
cleanup(true);
_sound = new Sound();
_sound.addEventListener(Event.ID3, gotID3);
var loadCallback:Event->Void = null;
loadCallback = function(e:Event)
{
(e.target : IEventDispatcher).removeEventListener(e.type, loadCallback);
// Check if the sound was destroyed before calling. Weak ref doesn't guarantee GC.
if (_sound == e.target)
{
_length = _sound.length;
if (onLoad != null)
onLoad();
}
}
// Use a weak reference so this can be garbage collected if destroyed before loading.
_sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true);
_sound.load(new URLRequest(soundURL));
return init(looped, autoDestroy, onComplete);
}
#if flash11
/**
* One of the main setup functions for sounds, this function loads a sound from a ByteArray.
*
* @param bytes A ByteArray object.
* @param looped Whether or not this sound should loop endlessly.
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function loadByteArray(bytes:ByteArray, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
if (!exists) return this;
cleanup(true);
_sound = new Sound();
_sound.addEventListener(Event.ID3, gotID3);
_sound.loadCompressedDataFromByteArray(bytes, bytes.length);
return init(looped, autoDestroy, onComplete);
}
#end
function init(?looped:Null<Bool>, ?autoDestroy:Null<Bool>, ?onComplete:Null<Void->Void>):FlxSound {
if (looped != null) this.looped = looped;
if (autoDestroy != null) this.autoDestroy = autoDestroy;
if (onComplete != null) this.onComplete = onComplete;
if (_sound != null) makeChannel();
else _length = 0;
endTime = null;
return this;
}
/**
* Call inbetween init function if a sound asset does exists.
* @since raltyMod
*/
function makeChannel() @:privateAccess {
if (_sound.__buffer == null) return;
if (_source != null) _source.dispose();
if (_channel == null) (_source = (_channel = new SoundChannel(null)).__source = new AudioSource(_sound.__buffer)).gain = 0;
else {
(_source = new AudioSource(_sound.__buffer)).gain = 0;
_channel.__dispose();
_channel.__source = _source;
SoundMixer.__registerSoundChannel(_channel);
}
if (_transform == null) _transform = new SoundTransform();
_transform.pan = 0;
_length = _source.length;
_source.onComplete.add(stopped);
_source.onLoop.add(source_looped);
_channel.__soundTransform = _transform;
_channel.__isValid = true;
}
/**
* Call after adjusting the volume to update the sound channel's settings.
*/
@:allow(flixel.sound.FlxSoundGroup)
function updateTransform() {
if (_transform == null) {
_transform = new SoundTransform();
pan = pan;
}
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || muted) ? 0 : 1) * FlxG.sound.volume * #end
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
if (_channel != null) _channel.soundTransform = _transform;
}
/**
* Call this function if you want this sound's volume to change
* based on distance from a particular FlxObject.
*
* @param X The X position of the sound.
* @param Y The Y position of the sound.
* @param TargetObject The object you want to track.
* @param Radius The maximum distance this sound can travel.
* @param Pan Whether panning should be used in addition to the volume changes.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function proximity(x = 0.0, y = 0.0, ?targetObject:FlxObject, ?radius:Float, pan = true):FlxSound {
setPosition(x, y);
if (targetObject != null) this.target = targetObject;
if (radius != null) this.radius = radius;
proximityPan = pan;
return this;
}
/**
* Helper function to set the coordinates of this object.
* Sound positioning is used in conjunction with proximity/panning.
*
* @param x The new x position
* @param y The new y position
*/
public inline function setPosition(x = 0.0, y = 0.0):Void {
this.x = x;
this.y = y;
}
/**
* Call this function to play the sound - also works on paused sounds.
*
* @param forceRestart Whether to start the sound over or not.
* Default value is false, meaning if the sound is already playing or was
* paused when you call play(), it will continue playing from its current
* position, NOT start again from the beginning.
* @param startTime At which point to start playing the sound, in milliseconds.
* @param endTime At which point to stop playing the sound, in milliseconds.
* If not set / `null`, the sound completes normally.
*/
public function play(forceRestart = false, startTime = 0.0, ?endTime:Null<Float>):FlxSound {
if (!exists) return this;
if (forceRestart) cleanup(false, true);
else if (playing) return this;
if (endTime != null) this.endTime = endTime;
if (_paused) resume();
else startSound(startTime);
return this;
}
/**
* Unpause a sound. Only works on sounds that have been paused.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function resume():FlxSound {
if (_paused) startSound(_time);
return this;
}
/**
* Call this function to pause this sound.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function pause():FlxSound {
if (!playing) return this;
cleanup(false, false);
_paused = true;
return this;
}
/**
* Call this function to stop this sound.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function stop():FlxSound {
cleanup(autoDestroy, true);
return this;
}
/**
* Helper function that tweens this sound's volume.
*
* @param duration The amount of time the fade-out operation should take.
* @param to The volume to tween to, 0 by default.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void):FlxSound {
if (fadeTween != null) fadeTween.cancel();
fadeTween = FlxTween.num(volume, to, duration, {onComplete: onComplete}, volumeTween);
return this;
}
/**
* Helper function that tweens this sound's volume.
* If the sound wasn't playing at all, it'll play before the tween starts.
*
* @param duration The amount of time the fade-in operation should take.
* @param from The volume to tween from, 0 by default.
* @param to The volume to tween to, 1 by default.
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
*/
public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void):FlxSound {
if (!playing) play();
if (fadeTween != null) fadeTween.cancel();
fadeTween = FlxTween.num(from, to, duration, {onComplete: onComplete}, volumeTween);
return this;
}
function volumeTween(f:Float) volume = f;
/**
* Returns the currently selected "real" volume of the sound (takes fades and proximity into account).
*
* @return The adjusted volume of the sound.
*/
public inline function getActualVolume():Float
return _volume * _volumeAdjust;
#if FLX_PITCH
/**
* Returns the currently selected "real" pitch of the sound.
*
* @return The adjusted pitch of the sound.
*/
public inline function getActualPitch():Float
return _realPitch;
#end
/**
* Returns the actual time coming from the internal, can be used for detecting sync error.
*
* @return The actual time of the sound.
*/
public inline function getActualTime():Float {
get_time();
return _time;
}
/**
* An internal helper function used to attempt to start playing
* the sound and populate the _channel variable.
*/
function startSound(startTime:Float) @:privateAccess {
if (_sound == null) return;
_paused = false;
_time = startTime;
_lastTime = FlxG.game.getTicks();
if (_channel == null || !_channel.__isValid || _source.__backend == null #if lime_cffi || _source.__backend.disposed || _source.__backend.handle == null #end)
makeChannel();
if (_channel != null) {
#if FLX_PITCH
_timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0;
pitch = _pitch;
#end
_channel.soundTransform = _transform;
_channel.__lastPeakTime = -10;
_channel.__leftPeak = 0;
_channel.__rightPeak = 0;
#if lime_cffi _source.__backend.playing = true; #end
_source.offset = 0;
_source.currentTime = startTime + _offset;
#if !lime_cffi _source.play(); #end
looped = looped;
loopTime = loopTime;
endTime = endTime;
active = true;
}
else {
exists = false;
active = false;
}
}
function stopped() {
onFinish.dispatch();
if (onComplete != null) onComplete();
if (looped) {
cleanup(false);
play(false, loopTime, endTime);
}
else cleanup(autoDestroy);
}
function source_looped() {
if (onComplete != null) onComplete();
if (!looped) {
cleanup(autoDestroy);
_lastTime = FlxG.game.getTicks();
_time = loopTime;
}
else _channel.loops = 999;
}
/**
* Internal event handler for ID3 info (i.e. fetching the song name).
*/
function gotID3(_) {
name = _sound.id3.songName;
artist = _sound.id3.artist;
_sound.removeEventListener(Event.ID3, gotID3);
}
#if FLX_SOUND_SYSTEM
@:allow(flixel.system.frontEnds.SoundFrontEnd)
function onFocus() if (!_alreadyPaused) {
resume();
_alreadyPaused = null;
}
@:allow(flixel.system.frontEnds.SoundFrontEnd)
function onFocusLost() if (_alreadyPaused == null && !(_alreadyPaused = _paused)) pause();
#end
function set_group(value:FlxSoundGroup):FlxSoundGroup {
if (value != null) value.add(this);
else group.remove(this);
return group;
}
inline function get_playing():Bool @:privateAccess
return _channel != null && _channel.__isValid && _source.playing;
inline function get_volume():Float
return _volume;
inline function set_volume(v:Float):Float {
_volume = FlxMath.bound(v, 0, 10);
updateTransform();
return _volume;
}
inline function set_muted(v:Bool):Bool {
muted = v;
updateTransform();
return muted;
}
inline function get_loaded():Bool
return buffer != null;
inline function get_streamed():Bool
@:privateAccess return #if lime_vorbis _sound != null && _sound.__buffer.__srcVorbisFile != null #else false #end;
inline function get_buffer():AudioBuffer
@:privateAccess return _sound != null ? _sound.__buffer : null;
function update_amplitude():Void @:privateAccess {
if (_channel == null || !_channel.__updatePeaks(get_time()) || !_amplitudeUpdate) return;
_amplitudeUpdate = false;
_amplitudeLeft = _channel.__leftPeak;
_amplitudeRight = _channel.__rightPeak;
}
inline function get_amplitudeLeft():Float {
update_amplitude();
return _amplitudeLeft;
}
inline function get_amplitudeRight():Float {
update_amplitude();
return _amplitudeRight;
}
inline function get_amplitude():Float {
update_amplitude();
return Math.max(_amplitudeLeft, _amplitudeRight);//if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft;
}
inline function get_channels():Int
@:privateAccess return (buffer != null) ? buffer.channels : 0;
inline function get_stereo():Bool
return channels > 1;
#if FLX_PITCH
inline function get_pitch():Float
return _pitch;
function set_pitch(v:Float):Float {
_realPitch = (_pitch = v) * _timeScaleAdjust;
if (_channel != null) _channel.pitch = _realPitch;
return _pitch;
}
#end
function set_looped(v:Bool):Bool {
if (playing) {
if (v) _channel.loops = 999;
else _channel.loops = 0;
}
return looped = v;
}
function set_loopTime(v:Float):Float {
if (playing) _channel.loopTime = v;
return loopTime = v;
}
function set_endTime(v:Null<Float>):Null<Float> {
if (playing) {
if (v != null && v > 0 && v < _length) _channel.endTime = v;
else _channel.endTime = null;
}
return endTime = v;
}
inline function get_pan():Float return _transform.pan;
inline function set_pan(pan:Float):Float return _transform.pan = pan;
inline function getFakeTime():Float {
if (@:privateAccess _channel.__isValid && _source.playing && _realPitch > 0 && _lastTime != null)
return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation;
else
return _time;
}
function get_time():Float {
if (_channel == null || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected /*CNE IMPLEMENTATION*/) return _time;
final pos = _channel.position - _offset;
if (!playing || _realPitch <= 0) {
_lastTime = null;
return _time = pos;
}
final fakeTime = getFakeTime();
if (pos != _time) {
_lastTime = FlxG.game.getTicks();
if ((_timeInterpolation = 1 - Math.min(fakeTime - pos, 1000) * 0.001) < 1 && _timeInterpolation > .9)
return _time = fakeTime;
else {
_timeInterpolation = 1;
return _time = pos;
}
}
else
return fakeTime;
}
function set_time(time:Float):Float @:privateAccess {
time = FlxMath.bound(time, _offset, length - 1);
if (_channel != null && _realPitch > 0) {
if (!_channel.__isValid) {
cleanup(false, true);
startSound(time);
}
else if (playing) {
_source.offset = 0;
_source.currentTime = time + _offset;
}
}
_lastTime = null;
return _time = time;
}
inline function get_offset():Float return _offset;
inline function set_offset(offset:Float):Float {
if (_offset == (_offset = offset)) return offset;
//time = time + _offset;
return offset;
}
inline function get_length():Float return _length - _offset;
/*function get_latency():Float {
if (_channel != null) return _source.latency;
return 0;
}*/
override function toString():String {
return FlxStringUtil.getDebugString([
LabelValuePair.weak("playing", playing),
LabelValuePair.weak("time", _time),
LabelValuePair.weak("length", length),
LabelValuePair.weak("volume", volume),
LabelValuePair.weak("pitch", pitch)
]);
}
}
+3 -10
View File
@@ -159,16 +159,9 @@ class Conductor
}
private static function __updateSongPos(elapsed:Float) {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) {
lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.time - songOffset : -songOffset;
return;
}
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) {
// update conductor
songPosition = lastSongPos;
} else {
songPosition += songOffset + elapsed * 1000;
if (FlxG.sound.music != null) { // CNE FlxSound is Interpolated.
lastSongPos = FlxG.sound.music.time - songOffset;
if (FlxG.sound.music.playing) songPosition = FlxG.sound.music.time;
}
}
@@ -4,6 +4,8 @@ import flixel.FlxState;
import flixel.sound.FlxSound;
import funkin.backend.utils.NativeAPI;
import lime.media.AudioManager;
import lime.media.AudioSource;
import haxe.Timer;
/**
* if you are stealing this keep this comment at least please lol
@@ -12,58 +14,45 @@ import lime.media.AudioManager;
*/
@:dox(hide)
class AudioSwitchFix {
@:noCompletion
private static function onStateSwitch(state:FlxState):Void {
#if windows
if (Main.audioDisconnected) {
public static function onAudioDisconnected() @:privateAccess {
var soundList:Array<FlxSound> = [FlxG.sound.music];
for (sound in FlxG.sound.list) if (sound.playing) {
sound.pause();
soundList.push(sound);
}
var playingList:Array<PlayingSound> = [];
for(e in FlxG.sound.list) {
if (e.playing) {
playingList.push({
sound: e,
time: e.time
});
e.stop();
}
}
if (FlxG.sound.music != null)
FlxG.sound.music.stop();
var i = AudioSource.activeSources.length;
while (i-- > 0) AudioSource.activeSources[i].dispose();
AudioManager.shutdown();
AudioManager.init();
// #if !lime_doc_gen
// if (AudioManager.context.type == OPENAL)
// {
// var alc = AudioManager.context.openal;
AudioManager.shutdown();
// var device = alc.openDevice();
// var ctx = alc.createContext(device);
// alc.makeContextCurrent(ctx);
// alc.processContext(ctx);
// }
// #end
Main.changeID++;
AudioManager.init();
Main.changeID++;
// #if !lime_doc_gen
// if (AudioManager.context.type == OPENAL)
// {
// var alc = AudioManager.context.openal;
for(e in playingList) {
e.sound.play(e.time);
}
// var device = alc.openDevice();
// var ctx = alc.createContext(device);
// alc.makeContextCurrent(ctx);
// alc.processContext(ctx);
// }
// #end
Main.audioDisconnected = false;
}
#end
for (sound in soundList) {
sound.makeChannel();
sound.resume();
}
Main.audioDisconnected = false;
}
private static var timer:Timer;
private static function onRun() if (Main.audioDisconnected) onAudioDisconnected();
public static function init() {
#if windows
NativeAPI.registerAudio();
FlxG.signals.preStateCreate.add(onStateSwitch);
#end
if (timer == null) (timer = new Timer(1000)).run = onRun;
}
}
@:dox(hide)
typedef PlayingSound = {
var sound:FlxSound;
var time:Float;
}
+169 -28
View File
@@ -3,43 +3,184 @@ package funkin.backend.utils;
import flixel.sound.FlxSound;
import lime.media.AudioBuffer;
// Thank you yosh :DDD -lunar
// (https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx)
#if lime_vorbis
import lime.media.vorbis.Vorbis;
import lime.media.vorbis.VorbisFile;
import lime.utils.ArrayBuffer;
#end
// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx
// REWRITTEN BY RALTYRO
class AudioAnalyzer {
public var sound:FlxSound;
public var buffer:AudioBuffer;
var __peakByte:Float = 0;
var __timeMulti:Float = 0;
var __toBits:Float;
var __wordSize:Int;
var __bitUnsignedSize:Int;
var __bitSize:Int;
#if lime_vorbis
var __vorbis:VorbisFile;
#end
public function new(sound:FlxSound) {
@:privateAccess buffer = sound._sound.__buffer;
__peakByte = Math.pow(2, buffer.bitsPerSample-1)-1;
__timeMulti = 1 / (1 / buffer.sampleRate);
__timeMulti *= buffer.bitsPerSample;
__timeMulti -= __timeMulti % buffer.bitsPerSample;
this.sound = sound;
__check();
}
public function analyze(startPos:Float, endPos:Float):Float {
var bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
inline function __check() if (sound.buffer != buffer) {
__bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1);
__bitUnsignedSize = 1 << buffer.bitsPerSample;
__toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3);
__vorbis = null;
}
bytesStartPos -= bytesStartPos % buffer.bitsPerSample;
bytesEndPos -= bytesEndPos % buffer.bitsPerSample;
public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
__check();
if (buffer.data != null) return __analyze(startPos, endPos, outSeperate);
#if lime_vorbis
else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate);
#end
return 0;
}
var maxByte:Int = 0;
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) {
#if js
var byte:Int = buffer.data[bytesStartPos + (i * buffer.bitsPerSample)]
| (buffer.data[bytesStartPos + (i * buffer.bitsPerSample) + 1] << 8);
#else
var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample))
| (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 1) << 8);
#end
if (byte > 256 * 128) byte -= 256 * 256;
if (maxByte < byte) maxByte = byte;
inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end;
var max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
if (useSeperate) {
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
while (c++ < buffer.channels) outSeperate.push(0);
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
}
return maxByte/__peakByte;
pos -= pos % __wordSize;
end -= end % __wordSize;
while (pos < end) {
// 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0)
if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize;
else {
while (w < buffer.bitsPerSample) {
b |= #if js buf[pos] #else buf.get(pos) #end << w;
w += 8;
pos++;
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
if (max < b) max = b;
if (useSeperate) {
if (outSeperate[c] < b) outSeperate[c] = b;
if (++c > buffer.channels) c = 0;
}
w = b = 0;
}
return max / __bitSize;
}
#if lime_vorbis // As far i know, only native supports vorbis
var __buffer:ArrayBuffer;
var __bufferSize:Int;
function __prepareVorbis():Bool @:privateAccess {
if (buffer.__srcVorbisFile == null) return __vorbis != null;
if (__vorbis != null) return true;
if ((__vorbis = buffer.__srcVorbisFile.clone()) != null) { // IM HOPING IT HAVE A GC CLOSURE.
__buffer = new ArrayBuffer(__bufferSize = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3));
return true;
}
return false;
}
inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float @:privateAccess {
var n = Math.floor((endPos - startPos) * __toBits);
if ((n -= n % __wordSize) < __wordSize) return 0;
var pos = 0, max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
if (useSeperate) {
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
while (c++ < buffer.channels) outSeperate.push(0);
}
// IT SHOULD BE ALWAYS BACKEND.STREAMED IF THIS GETS CALLED.
var backend = sound._source != null ? sound._source.__backend : null;
if (backend != null && backend.streamTimer != null) {
var i = backend.bufferSizes.length - backend.queuedBuffers;
var time = backend.bufferTimes[i] * 1000;
if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) {
var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i];
pos = Math.floor((startPos - time) * __toBits);
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
pos -= pos % __wordSize;
while (n > 0) {
if (__wordSize == 1) {
b = buf.get(pos++) - __bitSize;
n--;
}
else {
while (w < buffer.bitsPerSample) {
b |= buf.get(pos) << w;
w += 8;
pos++;
n--;
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
if (max < b) max = b;
if (useSeperate) {
if (outSeperate[c] < b) outSeperate[c] = b;
if (++c > buffer.channels) c = 0;
}
if (pos >= size) {
if (++i >= backend.bufferDatas.length) break;
size = backend.bufferSizes[i];
buf = backend.bufferDatas[i].buffer;
pos = 0;
}
w = b = 0;
}
startPos = pos / __toBits + time;
// Cannot inline a not final return
}
}
if (n == 0) return max / __bitSize;
if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) {
if (startPos < 1) __vorbis.rawSeek(0);
else __vorbis.timeSeek(startPos / 1000);
}
pos = c = 0;
var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, result;
while (n > 0) {
n -= (result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true));
if (result == Vorbis.HOLE) continue;
else if (result <= 0) break;
while (pos < result) {
if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize;
else {
while (w < buffer.bitsPerSample) {
b |= __buffer.get(pos) << w;
w += 8;
pos++;
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
if (max < b) max = b;
if (useSeperate) {
if (outSeperate[c] < b) outSeperate[c] = b;
if (++c > buffer.channels) c = 0;
}
w = b = 0;
}
pos = 0;
}
return max / __bitSize;
}
#end
}
+18 -11
View File
@@ -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<Null<Float>> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)];
var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE;
@@ -446,6 +450,8 @@ class CoolUtil
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
} else
Conductor.changeBPM(DefaultBPM);
FlxG.sound.music.play();
}
/**
@@ -812,10 +818,11 @@ class CoolUtil
* @param music Music
*/
public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) {
if (frontEnd.music != null)
@:privateAccess frontEnd.destroySound(frontEnd.music);
if (frontEnd.music == music) return;
if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music);
frontEnd.list.remove(music);
frontEnd.music = music;
frontEnd.defaultMusicGroup.add(frontEnd.music = music);
}
/**
+6 -13
View File
@@ -616,22 +616,17 @@ class Charter extends UIState {
updateWaveforms();
}
inline function isSoundLoaded(sound:FlxSound) {
@:privateAccess
return sound != null && sound._sound != null && sound._sound.length > 0;
}
public function updateWaveforms() {
var wavesToGenerate:Array<{name:String, sound:FlxSound}> = [];
if(isSoundLoaded(FlxG.sound.music))
if (FlxG.sound.music.loaded)
wavesToGenerate.push({name: "Inst.ogg", sound: FlxG.sound.music});
if (PlayState.SONG.meta.needsVoices != false && isSoundLoaded(vocals))
if (PlayState.SONG.meta.needsVoices != false && vocals.loaded)
wavesToGenerate.push({name: "Voices.ogg", sound: vocals});
for (strumLine in strumLines)
if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && isSoundLoaded(strumLine.vocals))
if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && strumLine.vocals.loaded)
wavesToGenerate.push({
name: 'Voices${strumLine.strumLine.vocalsSuffix}.ogg',
sound: strumLine.vocals
@@ -1575,12 +1570,10 @@ class Charter extends UIState {
vocals.pause();
for (strumLine in strumLines.members) strumLine.vocals.pause();
} else {
FlxG.sound.music.play();
vocals.play();
vocals.time = FlxG.sound.music.time = Conductor.songPosition + Conductor.songOffset * 2;
FlxG.sound.music.play(true, Conductor.songPosition + Conductor.songOffset);
vocals.play(true, FlxG.sound.music.getActualTime());
for (strumLine in strumLines.members) {
strumLine.vocals.play();
strumLine.vocals.time = vocals.time;
strumLine.vocals.play(true, FlxG.sound.music.getActualTime());
}
}
}
+29 -55
View File
@@ -509,7 +509,7 @@ class PlayState extends MusicBeatState
@:noCompletion @:dox(hide) private var _endSongCalled:Bool = false;
@:dox(hide)
var __vocalOffsetViolation:Float = 0;
var __vocalSyncTimer:Float = 0;
private function get_accuracy():Float {
if (accuracyPressedNotes <= 0) return -1;
@@ -999,25 +999,10 @@ class PlayState extends MusicBeatState
inst.onComplete = endSong;
if (!paused) {
FlxG.sound.setMusic(inst);
FlxG.sound.music.play();
}
vocals.play();
vocals.pause();
inst.pause();
for (strumLine in strumLines.members) {
strumLine.vocals.play();
strumLine.vocals.pause();
}
inst.time = vocals.time = (chartingMode && Charter.startHere) ? Charter.startTime : 0;
for (strumLine in strumLines.members) {
strumLine.vocals.time = vocals.time;
strumLine.vocals.play();
}
vocals.play();
inst.play();
var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0;
for (strumLine in strumLines.members) strumLine.vocals.play(true, time);
vocals.play(true, time);
inst.play(true, time);
updateDiscordPresence();
@@ -1028,15 +1013,14 @@ class PlayState extends MusicBeatState
scripts.call("destroy");
for(g in __cachedGraphics)
g.useCount--;
@:privateAccess
for (strumLine in strumLines.members)
FlxG.sound.destroySound(strumLine.vocals);
super.destroy();
scripts = FlxDestroyUtil.destroy(scripts);
@:privateAccess {
FlxG.sound.destroySound(inst);
for (strumLine in strumLines.members) FlxG.sound.destroySound(strumLine.vocals);
if (FlxG.sound.music != inst) FlxG.sound.destroySound(inst);
FlxG.sound.destroySound(vocals);
}
scripts = FlxDestroyUtil.destroy(scripts);
super.destroy();
WindowUtils.resetAffixes();
SaveWarning.reset();
@@ -1079,15 +1063,14 @@ class PlayState extends MusicBeatState
curSong = songData.meta.name.toLowerCase();
curSongID = curSong.replace(" ", "-");
inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty));
FlxG.sound.setMusic(inst = FlxG.sound.load(Assets.getMusic(Paths.inst(SONG.meta.name, difficulty))));
if (SONG.meta.needsVoices != false && Assets.exists(Paths.voices(SONG.meta.name, difficulty))) // null or true
vocals = FlxG.sound.load(Paths.voices(SONG.meta.name, difficulty));
vocals = FlxG.sound.load(Options.streamedVocals ? Assets.getMusic(Paths.voices(SONG.meta.name, difficulty)) : Paths.voices(SONG.meta.name, difficulty));
else
vocals = new FlxSound();
inst.group = FlxG.sound.defaultMusicGroup;
vocals.group = FlxG.sound.defaultMusicGroup;
inst.persist = vocals.persist = false;
vocals.group = FlxG.sound.defaultMusicGroup;
vocals.persist = false;
generatedMusic = true;
}
@@ -1188,17 +1171,11 @@ class PlayState extends MusicBeatState
@:dox(hide)
function resyncVocals():Void
{
vocals.pause();
for (strumLine in strumLines.members) strumLine.vocals.pause();
var time = Conductor.songPosition + Conductor.songOffset;
for (strumLine in strumLines.members) strumLine.vocals.play(true, time);
vocals.play(true, time);
inst.play(true, time);
FlxG.sound.music.play();
Conductor.songPosition = FlxG.sound.music.time;
vocals.time = Conductor.songPosition + Conductor.songOffset;
for (strumLine in strumLines.members) {
strumLine.vocals.time = vocals.time;
strumLine.vocals.play();
}
vocals.play();
gameAndCharsCall("onVocalsResync");
}
@@ -1333,22 +1310,19 @@ class PlayState extends MusicBeatState
if (Conductor.songPosition >= 0)
startSong();
}
} else if (FlxG.sound.music != null) {
var instTime = FlxG.sound.music.time;
var isOffsync:Bool = vocals.time != instTime;
if(!isOffsync) {
for(strumLine in strumLines.members) {
if(strumLine.vocals.time != instTime) {
isOffsync = true;
break;
}
}
else if (FlxG.sound.music != null && (__vocalSyncTimer -= elapsed) < 0) {
__vocalSyncTimer = 1;
var instTime = FlxG.sound.music.getActualTime();
var isOffsync:Bool = vocals.loaded && Math.abs(instTime - vocals.getActualTime()) > 30;
if (!isOffsync) {
for (strumLine in strumLines.members) {
if ((isOffsync = strumLine.vocals.loaded && Math.abs(instTime - strumLine.vocals.getActualTime()) > 30)) break;
}
}
__vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (isOffsync ? elapsed : -elapsed / 2));
if (__vocalOffsetViolation > Flags.VOCAL_OFFSET_VIOLATION_THRESHOLD) {
resyncVocals();
__vocalOffsetViolation = 0;
}
if (isOffsync) resyncVocals();
}
while(events.length > 0 && events.last().time <= Conductor.songPosition)
+3 -1
View File
@@ -114,7 +114,9 @@ class StrumLine extends FlxTypedGroup<Strum> {
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;
}
+1 -1
View File
@@ -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)));
+2
View File
@@ -40,6 +40,8 @@ class Options
public static var songOffset:Float = 0;
public static var framerate:Int = 120;
public static var gpuOnlyBitmaps:Bool = #if (mac || web) false #else true #end; // causes issues on mac and web
public static var streamedMusic:Bool = true;
public static var streamedVocals:Bool = true;
public static var lastLoadedMod:String = null;
@@ -43,6 +43,14 @@ class AppearanceOptions extends OptionsScreen {
"gpuOnlyBitmaps"));
}
#end
add(new Checkbox(
"Streamed Music",
"If checked, only musics will have streamed audio, ALSO freeing a LOT of memory with the downside of higher cpu usage if more audio are being streamed at once (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on longer songs. If you aren't sure, leave this on.",
"streamedMusic"));
add(new Checkbox(
"Streamed Vocals",
"If checked, vocals will also be streamed if Streamed Music is checked, will impact the performance if the song uses alot of vocals (EXPERIMENTAL). If you aren't sure, leave this on.",
"streamedVocals"));
add(new Checkbox(
"Auto Pause",
"If checked, switching windows will pause the game.",
@@ -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<Float>;
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<Float>
{
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<Float>):Null<Float>
{
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;
}
}
}
@@ -1,584 +1,492 @@
// ALL REWRITTEN FROM SCRATCH!!!! -raltyro
package lime._internal.backend.native;
import haxe.Int64;
import haxe.Timer;
import lime.math.Vector4;
import haxe.Int64;
import lime.media.openal.AL;
import lime.media.openal.ALBuffer;
import lime.media.openal.ALSource;
#if lime_vorbis
import lime.media.vorbis.Vorbis;
import lime.media.vorbis.VorbisFile;
import lime.media.AudioManager;
#end
import lime.math.Vector4;
import lime.media.AudioBuffer;
import lime.media.AudioSource;
import lime.utils.UInt8Array;
import lime.utils.ArrayBufferView;
#if !lime_debug
@:fileXml('tags="haxe,release"')
@:noDebug
#end
@:access(lime.media.AudioBuffer)
class NativeAudioSource
{
public static var initBuffers:Array<NativeAudioSource> = [];
@:access(lime.utils.ArrayBufferView)
class NativeAudioSource {
private static var STREAM_BUFFER_SAMPLES:Int = 0x1000;
private static var STREAM_MAX_BUFFERS:Int = 32;
private static var STREAM_TIMER_FREQUENCY:Int = 100;
private static var STREAM_BUFFER_FREQUENCY:Int = 6;
private static var STREAM_BUFFER_SIZE = 48000;
#if (native_audio_buffers && !macro)
private static var STREAM_NUM_BUFFERS = Std.parseInt(haxe.macro.Compiler.getDefine("native_audio_buffers"));
#else
private static var STREAM_NUM_BUFFERS = 3;
#end
private static var STREAM_TIMER_FREQUENCY = 100;
private var buffers:Array<ALBuffer>;
private var bufferTimeBlocks:Array<Float>;
private var completed:Bool;
private var dataLength:Int;
private var format:Int;
private var handle:ALSource;
private var length:Null<Float>;
private var loops:Int;
private var parent:AudioSource;
private var disposed:Bool;
private var streamed:Bool;
private var playing:Bool;
private var position:Vector4;
private var samples:Float;
private var stream:Bool;
private var streamTimer:Timer;
private var completed:Bool;
private var handle:ALSource;
private var buffers:Array<ALBuffer>;
private var unusedBuffers:Array<ALBuffer>;
private var timer:Timer;
private var format:Int;
private var samples:Float;
private var pcmTotal:Int64;
private var sampleRate:Int;
private var streamTimer:Timer;
private var bufferDatas:Array<ArrayBufferView>;
private var bufferTimes:Array<Float>;
private var bufferSizes:Array<Int>;
private var bufferSize:Int;
private var requestBuffers:Int;
private var queuedBuffers:Int;
private var toLoop:Int;
private var streamEnded:Bool;
private var dataLength:Float;
var disposed:Bool = false;
private var position:Vector4 = new Vector4();
private var length:Null<Float>;
private var loopTime:Null<Float>;
private var loops:Int;
public function new(parent:AudioSource)
{
this.parent = parent;
public function new(parent:AudioSource) this.parent = parent;
position = new Vector4();
}
public function dispose():Void
{
if (handle != null)
{
stop();
AL.sourcei(handle, AL.BUFFER, null);
AL.deleteSource(handle);
if (buffers != null)
{
for (buffer in buffers)
{
AL.deleteBuffer(buffer);
}
buffers = null;
}
handle = null;
}
public function dispose() {
disposed = true;
initBuffers.remove(this);
stop();
if (handle != null) {
AL.sourcei(handle, AL.BUFFER, AL.NONE);
AL.deleteSource(handle);
}
handle = null;
if (buffers != null) AL.deleteBuffers(buffers);
buffers = null;
bufferDatas = null;
bufferTimes = null;
bufferSizes = null;
unusedBuffers = null;
}
public function init():Void
{
dataLength = 0;
format = 0;
public function init() {
if (handle != null) return;
if (parent.buffer.channels == 1)
{
if (parent.buffer.bitsPerSample == 8)
{
format = AL.FORMAT_MONO8;
}
else if (parent.buffer.bitsPerSample == 16)
{
format = AL.FORMAT_MONO16;
}
if (disposed = (handle = AL.createSource()) == null) return;
AL.sourcef(handle, AL.MAX_GAIN, 10);
var buffer = parent.buffer;
var channels = buffer.channels, bitsPerSample = buffer.bitsPerSample;
// Default is just AL.FORMAT_MONO8 if it doesn't match any to avoid yo ears getting blasted
var isCreativeXFi = AL.getString(AL.RENDERER) == "X-Fi";
if (channels > 2 && (isCreativeXFi || AL.isExtensionPresent("AL_EXT_MCFORMATS"))) { // https://github.com/openalext/openalext/blob/master/AL_EXT_MCFORMATS.txt
if (channels == 3) format = bitsPerSample == 32 ? 0x1209 : (bitsPerSample == 16 ? 0x1208 : 0x1207);
else if (channels == 4) format = bitsPerSample == 32 ? 0x1206 : (bitsPerSample == 16 ? 0x1205 : 0x1204);
else if (channels == 6) format = bitsPerSample == 32 ? 0x120C : (bitsPerSample == 16 ? 0x120B : 0x120A);
else if (channels == 7) format = bitsPerSample == 32 ? 0x120F : (bitsPerSample == 16 ? 0x120E : 0x120D);
else if (channels == 8) format = bitsPerSample == 32 ? 0x1212 : (bitsPerSample == 16 ? 0x1211 : 0x1210);
}
else if (parent.buffer.channels == 2)
{
if (parent.buffer.bitsPerSample == 8)
{
format = AL.FORMAT_STEREO8;
}
else if (parent.buffer.bitsPerSample == 16)
{
format = AL.FORMAT_STEREO16;
}
// The 32 bit for STEREO and MONO are undocumented? https://github.com/kcat/openal-soft/issues/934, though it only works in X-Fi OpenAL??
else if (bitsPerSample == 32 && isCreativeXFi) format = channels == 2 ? 0x1203 : 0x1202;
else if (channels == 2) format = bitsPerSample == 16 ? AL.FORMAT_STEREO16 : AL.FORMAT_STEREO8;
else format = bitsPerSample == 16 ? AL.FORMAT_MONO16 : AL.FORMAT_MONO8;
if (buffer.data != null) {
streamed = false;
samples = (dataLength = (buffer.data.length >> 0)) / buffer.channels / (buffer.bitsPerSample >> 3);
if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null)
AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate);
AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer);
}
#if lime_vorbis
else if (buffer.__srcVorbisFile != null) {
streamed = true;
dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3);
if (parent.buffer.__srcVorbisFile != null)
{
stream = true;
var vorbisFile = parent.buffer.__srcVorbisFile;
pcmTotal = vorbisFile.pcmTotal();
var info = vorbisFile.info();
sampleRate = info.rate;
var _dataLength = pcmTotal * Int64.ofInt(parent.buffer.channels) * (Int64.ofInt(parent.buffer.bitsPerSample) / Int64.ofInt(8));
dataLength = Int64.toInt(_dataLength);
buffers = new Array();
bufferTimeBlocks = new Array();
for (i in 0...STREAM_NUM_BUFFERS)
{
buffers.push(AL.createBuffer());
bufferTimeBlocks.push(0);
}
handle = AL.createSource();
samples = Int64.toInt(pcmTotal);
var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8;
bufferSize = STREAM_BUFFER_SAMPLES * buffer.channels * (constructor == Int8 ? buffer.bitsPerSample >> 3 : 1);
buffers = AL.genBuffers(STREAM_MAX_BUFFERS);
bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(bufferSize, constructor)];
bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0];
bufferSizes = [for (i in 0...STREAM_MAX_BUFFERS) 0];
unusedBuffers = [];
bufferSize *= constructor == Int8 ? 1 : buffer.bitsPerSample >> 3;
}
else
{
if (parent.buffer.__srcBuffer == null)
{
parent.buffer.__srcBuffer = AL.createBuffer();
#end
if (parent.buffer.__srcBuffer != null)
{
AL.bufferData(parent.buffer.__srcBuffer, format, parent.buffer.data, parent.buffer.data.length, parent.buffer.sampleRate);
}
}
dataLength = parent.buffer.data.length;
handle = AL.createSource();
if (handle != null)
{
AL.sourcei(handle, AL.BUFFER, parent.buffer.__srcBuffer);
}
samples = Int64.toInt((Int64.make(0, dataLength) * Int64.ofInt(8)) / Int64.ofInt(parent.buffer.channels * parent.buffer.bitsPerSample));
if (dataLength == 0) {
trace('NativeAudioSource Bug! dataLength is 0');
dispose();
}
initBuffers.push(this);
}
public function play():Void
{
if (disposed) {
trace("TRIED TO PLAY DISPOSED?? WTFFF");
trace(haxe.CallStack.toString(haxe.CallStack.callStack()));
}
if (playing || handle == null)
{
return;
}
public function play() {
if (playing || disposed) return;
playing = true;
if (stream)
{
setCurrentTime(getCurrentTime());
streamTimer = new Timer(STREAM_TIMER_FREQUENCY);
streamTimer.run = streamTimer_onRun;
}
else
{
var time = completed ? 0 : getCurrentTime();
setCurrentTime(time);
}
if (completed) setCurrentTime(0);
else setCurrentTime(getCurrentTime());
}
public function pause():Void
{
public function pause() {
if (!disposed) AL.sourcePause(handle);
playing = false;
if (handle == null) return;
AL.sourcePause(handle);
if (streamTimer != null)
{
streamTimer.stop();
}
if (timer != null)
{
timer.stop();
}
stopStreamTimer();
stopTimer();
}
private function readVorbisFileBuffer(vorbisFile:VorbisFile, length:Int):UInt8Array
{
#if lime_vorbis
var buffer = new UInt8Array(length);
var read = 0, total = 0, readMax;
for (i in 0...STREAM_NUM_BUFFERS-1)
{
bufferTimeBlocks[i] = bufferTimeBlocks[i + 1];
}
bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell();
while (total < length)
{
readMax = 4096;
if (readMax > length - total)
{
readMax = length - total;
}
read = vorbisFile.read(buffer.buffer, total, readMax);
if (read > 0)
{
total += read;
}
else
{
break;
}
}
return buffer;
#else
return null;
#end
}
private function refillBuffers(buffers:Array<ALBuffer> = null):Void
{
#if lime_vorbis
var vorbisFile = null;
var position = 0;
if (buffers == null)
{
var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED);
if (buffersProcessed > 0)
{
vorbisFile = parent.buffer.__srcVorbisFile;
position = Int64.toInt(vorbisFile.pcmTell());
if (position < dataLength)
{
buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed);
}
}
}
if (buffers != null)
{
if (vorbisFile == null)
{
vorbisFile = parent.buffer.__srcVorbisFile;
position = Int64.toInt(vorbisFile.pcmTell());
}
var numBuffers = 0;
var data;
for (buffer in buffers)
{
if (dataLength - position >= STREAM_BUFFER_SIZE)
{
data = readVorbisFileBuffer(vorbisFile, STREAM_BUFFER_SIZE);
AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate);
position += STREAM_BUFFER_SIZE;
numBuffers++;
}
else if (position < dataLength)
{
data = readVorbisFileBuffer(vorbisFile, dataLength - position);
AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate);
numBuffers++;
break;
}
}
AL.sourceQueueBuffers(handle, numBuffers, buffers);
// OpenAL can unexpectedly stop playback if the buffers run out
// of data, which typically happens if an operation (such as
// resizing a window) freezes the main thread.
// If AL is supposed to be playing but isn't, restart it here.
if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED)
{
AL.sourcePlay(handle);
}
}
#end
}
public function stop():Void
{
if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING)
{
public function stop() {
if (!disposed) {
AL.sourceStop(handle);
toLoop = 0;
}
playing = false;
if (streamTimer != null)
{
streamTimer.stop();
}
if (timer != null)
{
timer.stop();
}
setCurrentTime(0);
stopStreamTimer();
stopTimer();
}
// Event Handlers
private function streamTimer_onRun():Void
{
refillBuffers();
}
private function timer_onRun():Void
{
if (loops > 0)
{
playing = false;
loops--;
setCurrentTime(0);
play();
return;
}
else
{
stop();
}
private function complete() {
completed = true;
stop();
parent.onComplete.dispatch();
}
// Get & Set Methods
public function getCurrentTime():Float
{
if (completed)
{
return getLength();
}
else if (handle != null)
{
if (stream)
{
var time = (Std.int(bufferTimeBlocks[0] * 1000) + Std.int(AL.getSourcef(handle, AL.SEC_OFFSET) * 1000)) - parent.offset;
if (time < 0) return 0;
return time;
}
else
{
var offset:Int = AL.getSourcei(handle, AL.BYTE_OFFSET);
var ratio = (offset / dataLength);
var totalSeconds = samples / parent.buffer.sampleRate;
var time = totalSeconds * ratio * 1000 - parent.offset;
// var time = Std.int (AL.getSourcef (handle, AL.SEC_OFFSET) * 1000) - parent.offset;
if (time < 0) return 0;
return time;
}
}
// Streaming, atleast for vorbis for now.
// just incase if we support more than 1 in the future?? which i doubt
private function streamTell():Float {
#if lime_vorbis
return parent.buffer.__srcVorbisFile.timeTell();
#else
return 0;
#end
}
public function setCurrentTime(value:Float):Float
{
// `setCurrentTime()` has side effects and is never safe to skip.
/* if (value == getCurrentTime())
{
return value;
} */
private function streamSeek(samples:Int64) {
#if lime_vorbis
// There's apparently a bug in libvorbis <= 1.3.4, hhttps://github.com/xiph/vorbis/blob/master/CHANGES#L39
// But it seems to also happen in this lime libvorbis version which is 1.3.7...? -ralty
if (samples < 4) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples);
#end
}
if (handle != null)
{
if (stream)
{
AL.sourceStop(handle);
private function readToBufferData(data:ArrayBufferView):Int {
#if lime_vorbis
var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize = parent.buffer.bitsPerSample >> 3;
var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize);
var n:Int = size < bufferSize ? Math.floor(size) : bufferSize;
n -= n % (parent.buffer.channels * wordSize);
parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000);
AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS);
refillBuffers(buffers);
var total = 0, result = 0, wasEOF = false;
while (total < bufferSize) {
result = n > 0 ? parent.buffer.__srcVorbisFile.read(data.buffer, total, n, isBigEndian, wordSize, true) : 0;
if (playing) AL.sourcePlay(handle);
if (result == Vorbis.HOLE) continue;
else if (result <= Vorbis.EREAD) break;
else if (result == 0) {
if (wasEOF || (streamEnded = loops <= toLoop)) break;
else {
wasEOF = true;
toLoop++;
var samples = getSamples(loopTime != null ? loopTime + parent.offset : parent.offset);
streamSeek(samples);
if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = bufferSize - total)) n = Math.floor(size);
n -= n % (parent.buffer.channels * wordSize);
}
}
else if (parent.buffer != null)
{
AL.sourceRewind(handle);
// AL.sourcef (handle, AL.SEC_OFFSET, (value + parent.offset) / 1000);
var secondOffset = (value + parent.offset) / 1000;
var totalSeconds = samples / parent.buffer.sampleRate;
if (secondOffset < 0) secondOffset = 0;
else if (secondOffset > totalSeconds) secondOffset = totalSeconds;
var ratio = (secondOffset / totalSeconds);
var totalOffset = Std.int(dataLength * ratio);
AL.sourcei(handle, AL.BYTE_OFFSET, totalOffset);
if (playing) AL.sourcePlay(handle);
else {
total += result;
n -= result;
wasEOF = false;
}
}
if (playing)
{
if (timer != null)
{
timer.stop();
if (total < bufferSize) data.buffer.fill(total, n, 0);
if (result < 0) {
trace('NativeAudioSource Streaming Bug! reading result is $result, streamEnded: $streamEnded, total: $total, n: $n');
return result;
}
return total;
#else
return 0;
#end
}
private function fillBuffer(buffer:ALBuffer):Int {
var i = STREAM_MAX_BUFFERS - requestBuffers;
var data = bufferDatas[i], time = streamTell();
var decoded = readToBufferData(data);
if (decoded > 0) {
AL.bufferData(buffer, format, data, decoded, parent.buffer.sampleRate);
var n = STREAM_MAX_BUFFERS - 1;
while (i < n) {
bufferDatas[i] = bufferDatas[i + 1];
bufferTimes[i] = bufferTimes[i + 1];
bufferSizes[i] = bufferSizes[++i];
}
queuedBuffers = requestBuffers;
bufferDatas[n] = data;
bufferTimes[n] = time;
bufferSizes[n] = decoded;
}
return decoded;
}
private function streamRun() {
if (parent == null || parent.buffer == null || handle == null) return dispose();
if (!playing || disposed) return stopStreamTimer();
try {
var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer;
while (processed-- > 0) {
buffer = AL.sourceUnqueueBuffer(handle);
if (!streamEnded && --n > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer);
else {
queuedBuffers = --requestBuffers;
unusedBuffers.push(buffer);
}
}
var timeRemaining = (getLength() - value) / getPitch();
if (!streamEnded) {
if (unusedBuffers.length != 0) {
requestBuffers++;
if (fillBuffer(buffer = unusedBuffers.pop()) > 0) {
queuedBuffers = requestBuffers;
AL.sourceQueueBuffer(handle, buffer);
}
else {
requestBuffers--;
unusedBuffers.push(buffer);
}
}
else if (queuedBuffers < STREAM_MAX_BUFFERS) {
if (fillBuffer(buffer = buffers[requestBuffers++]) > 0) AL.sourceQueueBuffer(handle, buffer);
else requestBuffers--;
}
}
}
catch(e) trace('NativeAudioSource Bug! in streamRun $e');
if (timeRemaining > 0)
{
completed = false;
timer = new Timer(timeRemaining);
timer.run = timer_onRun;
if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) {
AL.sourcePlay(handle);
resetTimer((getLength() - getCurrentTime()) / getPitch());
}
}
// Timers
inline function stopStreamTimer() {
if (streamTimer != null) streamTimer.stop();
streamTimer = null;
}
private function resetStreamTimer() {
stopStreamTimer();
if (streamed) (streamTimer = new Timer(STREAM_TIMER_FREQUENCY)).run = streamRun;
}
inline function stopTimer() {
if (timer != null) timer.stop();
timer = null;
}
private function resetTimer(timeRemaining:Float) {
stopTimer();
(timer = new Timer(timeRemaining)).run = timer_onRun;
}
private function timer_onRun() {
var timeRemaining = (getLength() - getCurrentTime()) / getPitch();
if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING && (!streamed || !streamEnded)) {
resetTimer(timeRemaining);
return;
}
if (loops == 0) {
complete();
return;
}
var start = loopTime != null ? loopTime : 0;
if (toLoop <= 0) {
setLoops(--loops);
if (start > 0 || AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) setCurrentTime(start);
else resetTimer((getLength() - start) / getPitch());
}
else {
if ((loops -= toLoop) < 0) loops = 0;
toLoop = 0;
resetTimer((getLength() - start) / getPitch());
}
parent.onLoop.dispatch();
}
// Get & Set Methods
public function getCurrentTime():Float {
if (disposed) return 0;
else if (completed) return getLength();
var time = AL.getSourcef(handle, AL.SAMPLE_OFFSET) / parent.buffer.sampleRate;
if (streamed) {
if (playing && streamEnded && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) {
complete();
return getLength();
}
else
{
playing = false;
completed = true;
time += bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers];
}
time *= 1000;
var length = if (length == null) getRealLength(); else length;
if (loops > 0 && time > length) {
var start = loopTime != null ? Math.max(0, loopTime) : 0;
return ((time - start) % (length - start)) + start - parent.offset;
}
return time - parent.offset;
}
public function setCurrentTime(value:Float):Float {
if (disposed) return value;
if (streamed) AL.sourceStop(handle);
else AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset));
if (playing) {
var timeRemaining = (getLength() - value) / getPitch();
if (timeRemaining < 8) complete();
else {
completed = streamEnded = false;
if (streamed) {
AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED));
unusedBuffers.resize(0);
streamSeek(getSamples(value + parent.offset));
requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY;
for (i in 0...queuedBuffers) {
if (!streamEnded && fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]);
else queuedBuffers = --requestBuffers;
}
if (!streamEnded) resetStreamTimer();
}
resetTimer(timeRemaining);
AL.sourcePlay(handle);
}
}
if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000;
return value;
}
public function getGain():Float
{
if (handle != null)
{
return AL.getSourcef(handle, AL.GAIN);
}
else
{
return 1;
}
}
inline private function getRealLength():Float return samples / parent.buffer.sampleRate * 1000;
public function getLength():Null<Float>
return if (disposed) 0; else if (length == null) getRealLength() - parent.offset; else length - parent.offset;
public function setGain(value:Float):Float
{
if (handle != null)
{
AL.sourcef(handle, AL.GAIN, value);
}
public function setLength(value:Null<Float>):Null<Float> {
if (value == length || disposed) return length = value;
var buffer = parent.buffer, wordSize = buffer.bitsPerSample >> 3;
if ((length = value) == null) dataLength = streamed ? samples * buffer.channels * wordSize : getFloat(Int64.make(0, buffer.data.length));
else dataLength = Math.ffloor(Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate) * buffer.channels * wordSize;
if (playing) {
if (streamed && (streamEnded || toLoop > 0)) setCurrentTime(getCurrentTime());
else {
var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch();
if (timeRemaining > 0) resetTimer(timeRemaining);
}
}
return value;
}
public function getLength():Float
{
if (length != null)
{
return length;
public function getPitch():Float {
return if (disposed) 1;
else AL.getSourcef(handle, AL.PITCH);
}
public function setPitch(value:Float):Float {
if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value;
if (playing) {
var timeRemaining = (getLength() - getCurrentTime()) / value;
if (timeRemaining > 0) resetTimer(timeRemaining);
}
return (samples / parent.buffer.sampleRate * 1000) - parent.offset;
AL.sourcef(handle, AL.PITCH, value);
return value;
}
public function setLength(value:Float):Float
{
if (playing && length != value)
{
if (timer != null)
{
timer.stop();
}
var timeRemaining = (value - getCurrentTime()) / getPitch();
if (timeRemaining > 0)
{
timer = new Timer(timeRemaining);
timer.run = timer_onRun;
}
}
return length = value;
public function getGain():Float {
if (disposed) return 1;
return AL.getSourcef(handle, AL.GAIN);
}
public function getLoops():Int
{
return loops;
public function setGain(value:Float):Float {
if (!disposed) AL.sourcef(handle, AL.GAIN, value);
return value;
}
public function setLoops(value:Int):Int
{
inline public function getLoops():Int return loops;
inline public function setLoops(value:Int):Int {
if (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE);
else if (loops == 0 && value > 0) setCurrentTime(getCurrentTime());
if (value < 0) return loops = 0;
return loops = value;
}
public function getPitch():Float
{
if (handle != null)
{
return AL.getSourcef(handle, AL.PITCH);
}
else
{
return 1;
}
inline public function getLoopTime():Float return loopTime;
inline public function setLoopTime(value:Float):Float {
if (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE);
return loopTime = value;
}
public function setPitch(value:Float):Float
{
if (playing && value != getPitch())
{
if (timer != null)
{
timer.stop();
}
var timeRemaining = (getLength() - getCurrentTime()) / value;
if (timeRemaining > 0)
{
timer = new Timer(timeRemaining);
timer.run = timer_onRun;
}
public function getLatency():Float {
/*#if (lime >= "8.3.0")
if (AL.isExtensionPresent("AL_SOFT_source_latency")) {
final offsets = AL.getSourcedvSOFT(handle, AL.SEC_OFFSET_LATENCY_SOFT, 2);
if (offsets != null) return offsets[1] * 1000;
}
if (handle != null)
{
AL.sourcef(handle, AL.PITCH, value);
}
return value;
#end*/
return 0;
}
public function getPosition():Vector4
{
if (handle != null)
{
#if !emscripten
var value = AL.getSource3f(handle, AL.POSITION);
position.x = value[0];
position.y = value[1];
position.z = value[2];
#end
}
public function getPosition():Vector4 return position;
return position;
}
public function setPosition(value:Vector4):Vector4
{
public function setPosition(value:Vector4):Vector4 {
position.x = value.x;
position.y = value.y;
position.z = value.z;
position.w = value.w;
if (handle != null)
{
if (!disposed) {
AL.distanceModel(AL.NONE);
AL.source3f(handle, AL.POSITION, position.x, position.y, position.z);
}
return position;
}
}
inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >> 0);
inline private function getSamples(ms:Float):Int64 return Int64.fromFloat(Math.max(0, Math.min(ms / 1000 * parent.buffer.sampleRate, samples)));
}
+491
View File
@@ -0,0 +1,491 @@
package lime.media;
import haxe.io.Bytes;
import haxe.io.Path;
import lime._internal.backend.native.NativeCFFI;
import lime._internal.format.Base64;
import lime.app.Future;
import lime.app.Promise;
import lime.media.openal.AL;
import lime.media.openal.ALBuffer;
import lime.media.vorbis.VorbisFile;
import lime.net.HTTPRequest;
import lime.utils.Log;
import lime.utils.UInt8Array;
#if lime_howlerjs
import lime.media.howlerjs.Howl;
#end
#if (js && html5)
import js.html.Audio;
#elseif flash
import flash.media.Sound;
import flash.net.URLRequest;
#end
@:access(lime._internal.backend.native.NativeCFFI)
@:access(lime.utils.Assets)
#if hl
@:keep
#end
#if !lime_debug
@:fileXml('tags="haxe,release"')
@:noDebug
#end
/**
The `AudioBuffer` class represents a buffer of audio data that can be played back using an `AudioSource`.
It supports a variety of audio formats and platforms, providing a consistent API for loading and managing audio data.
Depending on the platform, the audio backend may differ, but the class provides a unified interface for accessing
audio data, whether it's stored in memory, loaded from a file, or streamed.
@see lime.media.AudioSource
**/
class AudioBuffer
{
/**
The number of bits per sample in the audio data.
**/
public var bitsPerSample:Int;
/**
The number of audio channels (e.g., 1 for mono, 2 for stereo).
**/
public var channels:Int;
/**
The raw audio data stored as a `UInt8Array`.
**/
public var data:UInt8Array;
/**
The sample rate of the audio data, in Hz.
**/
public var sampleRate:Int;
/**
The source of the audio data. This can be an `Audio`, `Sound`, `Howl`, or other platform-specific object.
**/
public var src(get, set):Dynamic;
@:noCompletion private var __srcAudio:#if (js && html5) Audio #else Dynamic #end;
@:noCompletion private var __srcBuffer:#if lime_cffi ALBuffer #else Dynamic #end;
@:noCompletion private var __srcCustom:Dynamic;
@:noCompletion private var __srcHowl:#if lime_howlerjs Howl #else Dynamic #end;
@:noCompletion private var __srcSound:#if flash Sound #else Dynamic #end;
@:noCompletion private var __srcVorbisFile:#if lime_vorbis VorbisFile #else Dynamic #end;
#if commonjs
private static function __init__()
{
var p = untyped AudioBuffer.prototype;
untyped Object.defineProperties(p,
{
"src": {get: p.get_src, set: p.set_src}
});
}
#end
/**
Creates a new, empty `AudioBuffer` instance.
**/
public function new() {}
/**
Disposes of the resources used by this `AudioBuffer`, such as unloading any associated audio data.
**/
public function dispose():Void
{
#if (js && html5 && lime_howlerjs)
if (__srcHowl != null) __srcHowl.unload();
__srcHowl = null;
#end
#if lime_cffi
if (__srcBuffer != null) AL.deleteBuffer(__srcBuffer);
__srcBuffer = null;
#end
#if lime_vorbis
if (__srcVorbisFile != null) __srcVorbisFile.clear();
__srcVorbisFile = null;
#end
}
/**
Creates an `AudioBuffer` from a Base64-encoded string.
@param base64String The Base64-encoded audio data.
@return An `AudioBuffer` instance with the decoded audio data.
**/
public static function fromBase64(base64String:String):AudioBuffer
{
if (base64String == null) return null;
#if (js && html5 && lime_howlerjs)
// if base64String doesn't contain codec data, add it.
if (base64String.indexOf(",") == -1)
{
base64String = "data:" + __getCodec(Base64.decode(base64String)) + ";base64," + base64String;
}
var audioBuffer = new AudioBuffer();
audioBuffer.src = new Howl({src: [base64String], preload: false});
return audioBuffer;
#elseif (lime_cffi && !macro)
#if !cs
// if base64String contains codec data, strip it then decode it.
var base64StringSplit = base64String.split(",");
var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1];
var bytes:Bytes = Base64.decode(base64StringNoEncoding);
var audioBuffer = new AudioBuffer();
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer);
#else
// if base64String contains codec data, strip it then decode it.
var base64StringSplit = base64String.split(",");
var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1];
var bytes:Bytes = Base64.decode(base64StringNoEncoding);
var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null);
if (data != null)
{
var audioBuffer = new AudioBuffer();
audioBuffer.bitsPerSample = data.bitsPerSample;
audioBuffer.channels = data.channels;
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
audioBuffer.sampleRate = data.sampleRate;
return audioBuffer;
}
#end
#end
return null;
}
/**
Creates an `AudioBuffer` from a `Bytes` object.
@param bytes The `Bytes` object containing the audio data.
@return An `AudioBuffer` instance with the decoded audio data.
**/
public static function fromBytes(bytes:Bytes):AudioBuffer
{
if (bytes == null) return null;
#if (js && html5 && lime_howlerjs)
var audioBuffer = new AudioBuffer();
audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], preload: false});
return audioBuffer;
#elseif (lime_cffi && !macro)
#if !cs
var audioBuffer = new AudioBuffer();
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer);
#else
var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null);
if (data != null)
{
var audioBuffer = new AudioBuffer();
audioBuffer.bitsPerSample = data.bitsPerSample;
audioBuffer.channels = data.channels;
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
audioBuffer.sampleRate = data.sampleRate;
return audioBuffer;
}
#end
#end
return null;
}
/**
Creates an `AudioBuffer` from a file.
@param path The file path to the audio data.
@return An `AudioBuffer` instance with the audio data loaded from the file.
**/
public static function fromFile(path:String #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer
{
if (path == null) return null;
#if (js && html5 && lime_howlerjs)
var audioBuffer = new AudioBuffer();
#if force_html5_audio
audioBuffer.__srcHowl = new Howl({src: [path], html5: true, preload: false});
#else
audioBuffer.__srcHowl = new Howl({src: [path], html5: howlHtml5, preload: false});
#end
return audioBuffer;
#elseif flash
switch (Path.extension(path))
{
case "ogg", "wav":
return null;
default:
}
var audioBuffer = new AudioBuffer();
audioBuffer.__srcSound = new Sound(new URLRequest(path));
return audioBuffer;
#elseif (lime_cffi && !macro)
#if !cs
var audioBuffer = new AudioBuffer();
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
//audioBuffer = NativeCFFI.lime_audio_load_file(path, audioBuffer);
//if (audioBuffer != null) audioBuffer.initBuffer();
//return audioBuffer;
return NativeCFFI.lime_audio_load_file(path, audioBuffer);
#else
var data:Dynamic = NativeCFFI.lime_audio_load_file(path, null);
if (data != null)
{
var audioBuffer = new AudioBuffer();
audioBuffer.bitsPerSample = data.bitsPerSample;
audioBuffer.channels = data.channels;
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
audioBuffer.sampleRate = data.sampleRate;
//audioBuffer.initBuffer();
return audioBuffer;
}
return null;
#end
#else
return null;
#end
}
/**
Creates an `AudioBuffer` from an array of file paths.
@param paths An array of file paths to search for audio data.
@return An `AudioBuffer` instance with the audio data loaded from the first valid file found.
**/
public static function fromFiles(paths:Array<String> #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<AudioBuffer>
{
#if (flash || (js && html5))
var promise = new Promise<AudioBuffer>();
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<AudioBuffer>();
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<String>):Future<AudioBuffer>
{
#if (js && html5 && lime_howlerjs)
var promise = new Promise<AudioBuffer>();
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
}
}
+130 -27
View File
@@ -1,29 +1,100 @@
package lime.media;
import lime.app.Event;
import lime.media.openal.AL;
import lime.media.openal.ALSource;
import lime.math.Vector4;
#if !lime_debug
@:fileXml('tags="haxe,release"')
@:noDebug
#end
/**
The `AudioSource` class provides a way to control audio playback in a Lime application.
It allows for playing, pausing, and stopping audio, as well as controlling various
audio properties such as gain, pitch, and looping.
Depending on the platform, the audio backend may vary, but the API remains consistent.
@see lime.media.AudioBuffer
**/
class AudioSource
{
private static var activeSources:Array<AudioSource> = [];
/**
An event that is dispatched when the audio playback is complete.
**/
public var onComplete = new Event<Void->Void>();
/**
An event that is dispatched when the audio playback looped.
**/
public var onLoop = new Event<Void->Void>();
/**
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<Float>;
/**
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<Int> = 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<Float> = null, loops:Int = 0)
{
this.buffer = buffer;
this.offset = offset;
@@ -35,104 +106,136 @@ class AudioSource
this.length = length;
}
this.loops = loops;
if (buffer != null)
{
init();
}
this.loops = loops;
}
public function dispose():Void
/**
Releases any resources used by this `AudioSource`.
**/
inline public function dispose():Void
{
__backend.dispose();
activeSources.remove(this);
}
@:noCompletion private function init():Void
@:noCompletion inline private function init():Void
{
__backend.init();
activeSources.push(this);
}
public function play():Void
/**
Starts or resumes audio playback.
**/
inline public function play():Void
{
__backend.play();
}
public function pause():Void
/**
Pauses audio playback.
**/
inline public function pause():Void
{
__backend.pause();
}
public function stop():Void
/**
Stops audio playback and resets the playback position to the beginning.
**/
inline public function stop():Void
{
__backend.stop();
}
// Get & Set Methods
@:noCompletion private function get_currentTime():Float
@:noCompletion inline private function get_playing():Bool
{
@:privateAccess return __backend.playing;
}
@:noCompletion inline private function get_currentTime():Float
{
return __backend.getCurrentTime();
}
@:noCompletion private function set_currentTime(value:Float):Float
@:noCompletion inline private function set_currentTime(value:Float):Float
{
return __backend.setCurrentTime(value);
}
@:noCompletion private function get_gain():Float
@:noCompletion inline private function get_gain():Float
{
return __backend.getGain();
}
@:noCompletion private function set_gain(value:Float):Float
@:noCompletion inline private function set_gain(value:Float):Float
{
return __backend.setGain(value);
}
@:noCompletion private function get_length():Float
@:noCompletion inline private function get_length():Null<Float>
{
return __backend.getLength();
}
@:noCompletion private function set_length(value:Float):Float
@:noCompletion inline private function set_length(value:Null<Float>):Null<Float>
{
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;
+356
View File
@@ -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
+4 -38
View File
@@ -599,17 +599,6 @@ class Sound extends EventDispatcher
#end
}
var changeID:Int = 0;
public function regen() {
var audioBuffer = new AudioBuffer();
audioBuffer.bitsPerSample = __buffer.bitsPerSample;
audioBuffer.channels = __buffer.channels;
audioBuffer.data = __buffer.data;
audioBuffer.sampleRate = __buffer.sampleRate;
__buffer = audioBuffer;
}
/**
Generates a new SoundChannel object to play back the sound. This method
returns a SoundChannel object, which you access to stop the sound and to
@@ -636,13 +625,6 @@ class Sound extends EventDispatcher
return null;
}
#if !macro
if (changeID < funkin.backend.system.Main.changeID) {
changeID = funkin.backend.system.Main.changeID;
regen();
}
#end
if (sndTransform == null)
{
sndTransform = new SoundTransform();
@@ -690,26 +672,10 @@ class Sound extends EventDispatcher
#if (js && html5 && howlerjs)
return __buffer.src.duration() * 1000;
#else
if (__buffer.data != null)
{
// var samples = (__buffer.data.length * 8) / (__buffer.channels * __buffer.bitsPerSample);
// return Std.int(samples / __buffer.sampleRate * 1000);
var samples = (Int64.make(0, __buffer.data.length) * Int64.ofInt(8)) / Int64.ofInt(__buffer.channels * __buffer.bitsPerSample);
var div = Int64.divMod(samples * Int64.ofInt(1000), Int64.ofInt(__buffer.sampleRate));
var value = (div.quotient + (div.modulus / __buffer.sampleRate));
return Int64.toInt(value);
}
else if (__buffer.__srcVorbisFile != null)
{
//var samples = Int64.toInt(__buffer.__srcVorbisFile.pcmTotal());
//return Std.int(samples / __buffer.sampleRate * 1000);
var samples = __buffer.__srcVorbisFile.pcmTotal();
var value = Int64.fromFloat(__buffer.__srcVorbisFile.timeTotal()) * 1000;
return Int64.toInt(value);
}
else
{
return 0;
if (__buffer.data != null) return (__buffer.data.length >> 0) / __buffer.channels / (__buffer.bitsPerSample >> 3) / __buffer.sampleRate * 1000;
else if (__buffer.__srcVorbisFile != null) {
var x = __buffer.__srcVorbisFile.pcmTotal();
return (x.high * 4294967296. + (x.low >> 0)) / __buffer.sampleRate * 1000;
}
#end
}
+225 -57
View File
@@ -1,10 +1,13 @@
package openfl.media;
#if !flash
import haxe.Int64;
import openfl.events.Event;
import openfl.events.EventDispatcher;
#if lime
import lime.media.AudioSource;
import lime.media.openal.AL;
#end
/**
@@ -16,11 +19,19 @@ import lime.media.AudioSource;
object to the channel.
@event soundComplete Dispatched when a sound has finished playing.
@see [Playing sounds](https://books.openfl.org/openfl-developers-guide/working-with-sound/playing-sounds.html)
@see `openfl.media.Sound`
**/
#if !openfl_debug
@:fileXml('tags="haxe,release"')
@:noDebug
#end
#if (lime && lime_cffi)
@:access(lime._internal.backend.native.NativeAudioSource)
@:access(lime.media.AudioSource)
#end
@:access(openfl.media.Sound)
@:access(openfl.media.SoundMixer)
@:final @:keep class SoundChannel extends EventDispatcher
{
@@ -28,7 +39,13 @@ import lime.media.AudioSource;
The current amplitude(volume) of the left channel, from 0(silent) to 1
(full amplitude).
**/
public var leftPeak(default, null):Float;
public var leftPeak(get, null):Float;
/**
The current amplitude(volume) of the right channel, from 0(silent) to 1
(full amplitude).
**/
public var rightPeak(get, null):Float;
/**
When the sound is playing, the `position` property indicates in
@@ -45,12 +62,6 @@ import lime.media.AudioSource;
**/
public var position(get, set):Float;
/**
The current amplitude(volume) of the right channel, from 0(silent) to 1
(full amplitude).
**/
public var rightPeak(default, null):Float;
/**
The SoundTransform object assigned to the sound channel. A SoundTransform
object includes properties for setting volume, panning, left speaker
@@ -58,13 +69,23 @@ import lime.media.AudioSource;
**/
public var soundTransform(get, set):SoundTransform;
/**
self explanatory
*/
public var loopTime(get, set):Float;
public var endTime(get, set):Null<Float>;
public var pitch(get, set):Float;
public var loops(get, set):Int;
@:noCompletion private var __sound:Sound;
@:noCompletion private var __isValid:Bool;
@:noCompletion private var __soundTransform:SoundTransform;
@:noCompletion private var __lastPeakTime:Float;
@:noCompletion private var __leftPeak:Float;
@:noCompletion private var __rightPeak:Float;
#if lime
@:noCompletion private var __source:AudioSource;
@:noCompletion private var __audioSource(get, set):AudioSource;
@:noCompletion private var __audioSource(get, set):AudioSource; // forward??? compatibility??
#end
#if openfljs
@@ -87,28 +108,10 @@ import lime.media.AudioSource;
{
super(this);
leftPeak = 1;
rightPeak = 1;
if (soundTransform != null) __soundTransform = soundTransform;
else __soundTransform = new SoundTransform();
if (soundTransform != null)
{
__soundTransform = soundTransform;
}
else
{
__soundTransform = new SoundTransform();
}
#if lime
if (source != null)
{
__source = source;
__source.onComplete.add(source_onComplete);
__isValid = true;
__source.play();
}
#end
__initAudioSource(source);
SoundMixer.__registerSoundChannel(this);
}
@@ -134,6 +137,7 @@ import lime.media.AudioSource;
#if lime
__source.onComplete.remove(source_onComplete);
__source.onLoop.remove(source_onLoop);
__source.dispose();
__source = null;
#end
@@ -145,6 +149,90 @@ import lime.media.AudioSource;
this.soundTransform = soundTransform;
}
@:noCompletion private function __updatePeaks(time:Float):Bool
{
if (Math.abs(time - __lastPeakTime) < 8) return false;
__lastPeakTime = time;
__leftPeak = __rightPeak = 0;
#if !macro
if (!__isValid) return false;
var buffer = __source.buffer;
var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1);
var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize), size = 0, buf;
#if lime_cffi
var backend = __source.__backend, i = 0;
if (backend.streamed) {
size = backend.bufferSizes[i = backend.bufferSizes.length - backend.queuedBuffers];
buf = backend.bufferDatas[i].buffer;
pos -= Math.floor(backend.bufferTimes[i] * buffer.sampleRate * buffer.channels * wordSize);
}
else
#end {
buf = buffer.data #if !js .buffer #end;
size = #if js buf.byteLength #else buf.length #end;
}
var s = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0;
pos -= pos % wordSize;
while (s > 0) {
if (wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - bitSize;
else {
while (w < buffer.bitsPerSample) {
b |= #if js buf[pos] #else buf.get(pos) #end << w;
w += 8;
pos++;
}
if (b > bitSize) b -= bitUnsignedSize;
}
((c % 2 == 0) ? (if (__leftPeak < b) __leftPeak = b) : (if (__rightPeak < b) __rightPeak = b));
if (pos >= size) #if lime_cffi {
if (!backend.streamed || ++i >= backend.bufferDatas.length) break;
size = backend.bufferSizes[i];
buf = backend.bufferDatas[i].buffer;
pos = 0;
}
#else break; #end
if (++c > buffer.channels) {
c = 0;
s--;
}
w = b = 0;
}
if (buffer.channels == 1) __rightPeak = __leftPeak / bitSize;
else {
__leftPeak = __leftPeak / bitSize;
__rightPeak = __rightPeak / bitSize;
}
#end
return true;
}
@:noCompletion private function __initAudioSource(source:#if lime AudioSource #else Dynamic #end):Void
{
#if lime
__source = source;
if (__source == null)
{
return;
}
__source.onComplete.add(source_onComplete);
__source.onLoop.add(source_onLoop);
__isValid = true;
__source.play();
#end
}
// Get & Set Methods
@:noCompletion private function get_position():Float
{
@@ -167,27 +255,6 @@ import lime.media.AudioSource;
return value;
}
@:noCompletion private function get_pitch():Float
{
if (!__isValid) return 1;
#if lime
return __source.pitch;
#else
return 1;
#end
}
@:noCompletion private function set_pitch(value:Float):Float
{
if (!__isValid) return 1;
#if lime
__source.pitch = value;
#end
return value;
}
@:noCompletion private function get_soundTransform():SoundTransform
{
return __soundTransform.clone();
@@ -203,7 +270,7 @@ import lime.media.AudioSource;
var pan = SoundMixer.__soundTransform.pan + __soundTransform.pan;
if (pan < -1) pan = -1;
else if (pan > 1) pan = 1;
if (pan > 1) pan = 1;
var volume = SoundMixer.__soundTransform.volume * __soundTransform.volume;
@@ -225,6 +292,107 @@ import lime.media.AudioSource;
return value;
}
@:noCompletion private function get_pitch():Float
{
if (!__isValid) return 1;
#if lime
return __source.pitch;
#else
return 0;
#end
}
@:noCompletion private function set_pitch(value:Float):Float
{
if (!__isValid) return 1;
#if lime
return __source.pitch = value;
#else
return 0;
#end
}
@:noCompletion private function get_loopTime():Float
{
if (!__isValid) return -1;
#if lime
return __source.loopTime;
#else
return -1;
#end
}
@:noCompletion private function set_loopTime(value:Float):Float
{
if (!__isValid) return -1;
#if lime
return __source.loopTime = value;
#else
return -1;
#end
}
@:noCompletion private function get_endTime():Null<Float>
{
if (!__isValid) return null;
#if lime
return __source.length;
#else
return null;
#end
}
@:noCompletion private function set_endTime(value:Null<Float>):Null<Float>
{
if (!__isValid) return null;
#if lime
return __source.length = value;
#else
return null;
#end
}
@:noCompletion private function get_loops():Int
{
if (!__isValid) return 0;
#if lime
return __source.loops;
#else
return 0;
#end
}
@:noCompletion private function set_loops(value:Int):Int
{
if (!__isValid) return 0;
#if lime
return __source.loops = value;
#else
return 0;
#end
}
@:noCompletion private function get_leftPeak():Float
{
__updatePeaks(get_position());
return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume);
}
@:noCompletion private function get_rightPeak():Float
{
__updatePeaks(get_position());
return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume);
}
// Event Handlers
@:noCompletion private function source_onComplete():Void
{
@@ -234,14 +402,14 @@ import lime.media.AudioSource;
dispatchEvent(new Event(Event.SOUND_COMPLETE));
}
function set___audioSource(value:AudioSource):AudioSource {
return __source = value;
@:noCompletion private function source_onLoop():Void
{
//dispatchEvent(new Event(Event.SOUND_LOOP));
}
function get___audioSource():AudioSource {
return __source;
}
@:noCompletion private function get___audioSource():AudioSource return __source;
@:noCompletion private function set___audioSource(source:AudioSource):AudioSource return __source = source;
}
#else
typedef SoundChannel = flash.media.SoundChannel;
#end
#end
+8 -9
View File
@@ -243,16 +243,15 @@ class Assets
public static function getMusic(id:String, useCache:Bool = true):Sound
{
#if (lime_vorbis && lime > "7.9.0")
var path = getPath(id);
// TODO: What if it is a WAV or non-Vorbis file?
var vorbisFile = VorbisFile.fromFile(path);
var buffer = AudioBuffer.fromVorbisFile(vorbisFile);
return Sound.fromAudioBuffer(buffer);
#else
// TODO: Streaming sound
return getSound(id, useCache);
#if (lime_vorbis && lime > "7.9.0" && !macro)
if (Options.streamedMusic) {
var path = getPath(id);
// TODO: What if it is a WAV or non-Vorbis file?
var vorbisFile = VorbisFile.fromFile(path);
if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile));
}
#end
return getSound(id, useCache);
}
/**