509 lines
12 KiB
ActionScript
509 lines
12 KiB
ActionScript
package org.flixel
|
|
{
|
|
import flash.events.Event;
|
|
import flash.media.Sound;
|
|
import flash.media.SoundChannel;
|
|
import flash.media.SoundTransform;
|
|
import flash.net.URLRequest;
|
|
|
|
import flash.events.SampleDataEvent;
|
|
import flash.utils.ByteArray;
|
|
|
|
|
|
/**
|
|
* This is the universal flixel sound object, used for streaming, music, and sound effects.
|
|
*
|
|
* EMBEDDED SEAMLESS LOOPING MODIFICATION BY MAX "GETI" CAHILL.
|
|
*/
|
|
public class FlxSound extends FlxObject
|
|
{
|
|
/**
|
|
* Whether or not this sound should be automatically destroyed when you switch states.
|
|
*/
|
|
public var survive:Boolean;
|
|
/**
|
|
* Whether the sound is currently playing or not.
|
|
*/
|
|
public var playing:Boolean;
|
|
/**
|
|
* The ID3 song name. Defaults to null. Currently only works for streamed sounds.
|
|
*/
|
|
public var name:String;
|
|
/**
|
|
* The ID3 artist name. Defaults to null. Currently only works for streamed sounds.
|
|
*/
|
|
public var artist:String;
|
|
public var fixed:Boolean;
|
|
/*
|
|
* A bunch of looping variables follow.
|
|
*/
|
|
protected const MAGIC_DELAY:Number = 2766.0; //THE MAGIC NUMBER
|
|
protected const bufferSize:int = 4096; //this gives stable playback
|
|
protected var samplesTotal:int = 0; //this _must_ be known about the song to be looped
|
|
public var loop_start:int = 0;
|
|
protected var samplesPosition:int = 0; //helper for reading the sound
|
|
|
|
protected var _streaming:Boolean; //whether we're streaming the audio or not
|
|
|
|
/*
|
|
* The default FlxSound variables
|
|
*/
|
|
|
|
protected var _init:Boolean;
|
|
public var _sound:Sound;
|
|
protected var _in:Sound;
|
|
public var _channel:SoundChannel;
|
|
protected var _transform:SoundTransform;
|
|
public var _position:Number;
|
|
protected var _volume:Number;
|
|
protected var _volumeAdjust:Number;
|
|
protected var _looped:Boolean;
|
|
protected var _core:FlxObject;
|
|
protected var _radius:Number;
|
|
protected var _pan:Boolean;
|
|
protected var _fadeOutTimer:Number;
|
|
protected var _fadeOutTotal:Number;
|
|
protected var _pauseOnFadeOut:Boolean;
|
|
protected var _fadeInTimer:Number;
|
|
protected var _fadeInTotal:Number;
|
|
protected var _point2:FlxPoint;
|
|
|
|
|
|
/**
|
|
* The FlxSound constructor gets all the variables initialized, but NOT ready to play a sound yet.
|
|
*/
|
|
public function FlxSound()
|
|
{
|
|
|
|
super();
|
|
_point2 = new FlxPoint();
|
|
_transform = new SoundTransform();
|
|
init();
|
|
fixed = true; //no movement usually
|
|
}
|
|
|
|
/**
|
|
* An internal function for clearing all the variables used by sounds.
|
|
*/
|
|
protected function init():void
|
|
{
|
|
_transform.pan = 0;
|
|
_sound = null;
|
|
_in = null;
|
|
_position = 0;
|
|
_volume = 1.0;
|
|
_volumeAdjust = 1.0;
|
|
_looped = false;
|
|
_core = null;
|
|
_radius = 0;
|
|
_pan = false;
|
|
_fadeOutTimer = 0;
|
|
_fadeOutTotal = 0;
|
|
_pauseOnFadeOut = false;
|
|
_fadeInTimer = 0;
|
|
_fadeInTotal = 0;
|
|
active = false;
|
|
visible = false;
|
|
solid = false;
|
|
playing = false;
|
|
name = null;
|
|
artist = null;
|
|
}
|
|
|
|
/**
|
|
* One of two 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 totalSamples If looped is true, the number of samples is needed.
|
|
*
|
|
* @return This <code>FlxSound</code> instance (nice for chaining stuff together, if you're into that).
|
|
*/
|
|
public function loadEmbedded(EmbeddedSound:Class, Looped:Boolean=false, totalSamples:int = 0, _loop_start:int=0):FlxSound
|
|
{
|
|
stop();
|
|
init();
|
|
if (Looped)
|
|
{
|
|
_in = new EmbeddedSound;
|
|
_sound = new Sound();
|
|
_sound.addEventListener( SampleDataEvent.SAMPLE_DATA, sampleData );
|
|
samplesTotal = totalSamples - MAGIC_DELAY; //prevents any delay at the end of the track as well.
|
|
loop_start = _loop_start;
|
|
}
|
|
else _sound = new EmbeddedSound;
|
|
//NOTE: can't pull ID3 info from embedded sound currently
|
|
_streaming = false;
|
|
_looped = Looped;
|
|
updateTransform();
|
|
active = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* One of two main setup functions for sounds, this function loads a sound from a URL.
|
|
*
|
|
* @param EmbeddedSound A string representing the URL of the MP3 file you want to play.
|
|
* @param Looped Whether or not this sound should loop endlessly.
|
|
*
|
|
* @return This <code>FlxSound</code> instance (nice for chaining stuff together, if you're into that).
|
|
*/
|
|
public function loadStream(SoundURL:String, Looped:Boolean=false):FlxSound
|
|
{
|
|
stop();
|
|
init();
|
|
_sound = new Sound();
|
|
_sound.addEventListener(Event.ID3, gotID3);
|
|
_sound.load(new URLRequest(SoundURL));
|
|
_streaming = true;
|
|
_looped = Looped;
|
|
updateTransform();
|
|
active = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Call this function if you want this sound's volume to change
|
|
* based on distance from a particular FlxCore object.
|
|
*
|
|
* @param X The X position of the sound.
|
|
* @param Y The Y position of the sound.
|
|
* @param Core The object you want to track.
|
|
* @param Radius The maximum distance this sound can travel.
|
|
*
|
|
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
|
*/
|
|
public function proximity(X:Number,Y:Number,Core:FlxObject,Radius:Number,Pan:Boolean=true):FlxSound
|
|
{
|
|
x = X;
|
|
y = Y;
|
|
_core = Core;
|
|
_radius = Radius;
|
|
_pan = Pan;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Call this function to play the sound.
|
|
*/
|
|
public function play():void
|
|
{
|
|
volume = FlxG.volume;
|
|
if(_position < 0)
|
|
return;
|
|
if(_looped)
|
|
{
|
|
if (!_streaming)
|
|
{
|
|
if (_channel == null)
|
|
|
|
_channel = _sound.play(0,9999,_transform);
|
|
if(_channel == null)
|
|
active = false;
|
|
}
|
|
else
|
|
{
|
|
if(_position == 0)
|
|
{
|
|
if(_channel == null)
|
|
_channel = _sound.play(0,9999,_transform);
|
|
if(_channel == null)
|
|
active = false;
|
|
}
|
|
else
|
|
{
|
|
_channel = _sound.play(_position,0,_transform);
|
|
if(_channel == null)
|
|
active = false;
|
|
else
|
|
_channel.addEventListener(Event.SOUND_COMPLETE, looped);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(_position == 0)
|
|
{
|
|
if(_channel == null)
|
|
{
|
|
_channel = _sound.play(0,0,_transform);
|
|
if(_channel == null)
|
|
active = false;
|
|
else
|
|
_channel.addEventListener(Event.SOUND_COMPLETE, stopped);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_channel = _sound.play(_position,0,_transform);
|
|
if(_channel == null)
|
|
active = false;
|
|
}
|
|
}
|
|
playing = (_channel != null);
|
|
_position = 0;
|
|
}
|
|
|
|
/**
|
|
* Call this function to pause this sound.
|
|
*/
|
|
public function pause():void
|
|
{
|
|
if(_channel == null)
|
|
{
|
|
_position = -1;
|
|
return;
|
|
}
|
|
_position = _channel.position;
|
|
_channel.stop();
|
|
if(_looped)
|
|
{
|
|
while(_position >= _sound.length)
|
|
_position -= _sound.length;
|
|
}
|
|
_channel = null;
|
|
playing = false;
|
|
}
|
|
|
|
/**
|
|
* Call this function to stop this sound.
|
|
*/
|
|
public function stop():void
|
|
{
|
|
_position = 0;
|
|
if(_channel != null)
|
|
{
|
|
_channel.stop();
|
|
stopped();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call this function to make this sound fade out over a certain time interval.
|
|
*
|
|
* @param Seconds The amount of time the fade out operation should take.
|
|
* @param PauseInstead Tells the sound to pause on fadeout, instead of stopping.
|
|
*/
|
|
public function fadeOut(Seconds:Number,PauseInstead:Boolean=false):void
|
|
{
|
|
_pauseOnFadeOut = PauseInstead;
|
|
_fadeInTimer = 0;
|
|
_fadeOutTimer = Seconds;
|
|
_fadeOutTotal = _fadeOutTimer;
|
|
}
|
|
|
|
/**
|
|
* Call this function to make a sound fade in over a certain
|
|
* time interval (calls <code>play()</code> automatically).
|
|
*
|
|
* @param Seconds The amount of time the fade-in operation should take.
|
|
*/
|
|
public function fadeIn(Seconds:Number):void
|
|
{
|
|
_fadeOutTimer = 0;
|
|
_fadeInTimer = Seconds;
|
|
_fadeInTotal = _fadeInTimer;
|
|
play();
|
|
}
|
|
|
|
/**
|
|
* Set <code>volume</code> to a value between 0 and 1 to change how this sound is.
|
|
*/
|
|
public function get volume():Number
|
|
{
|
|
return _volume;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
public function set volume(Volume:Number):void
|
|
{
|
|
_volume = Volume;
|
|
if(_volume < 0)
|
|
_volume = 0;
|
|
else if(_volume > 1)
|
|
_volume = 1;
|
|
updateTransform();
|
|
}
|
|
|
|
/**
|
|
* Internal function that performs the actual logical updates to the sound object.
|
|
* Doesn't do much except optional proximity and fade calculations.
|
|
*/
|
|
protected function updateSound():void
|
|
{
|
|
if(_position != 0)
|
|
return;
|
|
|
|
var radial:Number = 1.0;
|
|
var fade:Number = 1.0;
|
|
|
|
//Distance-based volume control
|
|
if(_core != null)
|
|
{
|
|
var _point:FlxPoint = new FlxPoint();
|
|
var _point2:FlxPoint = new FlxPoint();
|
|
_core.getScreenXY(_point);
|
|
getScreenXY(_point2);
|
|
var dx:Number = _point.x - _point2.x;
|
|
var dy:Number = _point.y - _point2.y;
|
|
radial = (_radius - Math.sqrt(dx*dx + dy*dy))/_radius;
|
|
if(radial < 0) radial = 0;
|
|
if(radial > 1) radial = 1;
|
|
|
|
if(_pan)
|
|
{
|
|
var d:Number = -dx/_radius;
|
|
if(d < -1) d = -1;
|
|
else if(d > 1) d = 1;
|
|
_transform.pan = d;
|
|
}
|
|
}
|
|
|
|
//Cross-fading volume control
|
|
if(_fadeOutTimer > 0)
|
|
{
|
|
_fadeOutTimer -= FlxG.elapsed;
|
|
if(_fadeOutTimer <= 0)
|
|
{
|
|
if(_pauseOnFadeOut)
|
|
pause();
|
|
else
|
|
stop();
|
|
}
|
|
fade = _fadeOutTimer/_fadeOutTotal;
|
|
if(fade < 0) fade = 0;
|
|
}
|
|
else if(_fadeInTimer > 0)
|
|
{
|
|
_fadeInTimer -= FlxG.elapsed;
|
|
fade = _fadeInTimer/_fadeInTotal;
|
|
if(fade < 0) fade = 0;
|
|
fade = 1 - fade;
|
|
}
|
|
|
|
_volumeAdjust = radial*fade;
|
|
updateTransform();
|
|
}
|
|
|
|
/**
|
|
* The basic game loop update function. Just calls <code>updateSound()</code>.
|
|
*/
|
|
override public function update():void
|
|
{
|
|
super.update();
|
|
updateSound();
|
|
}
|
|
|
|
/**
|
|
* The basic class destructor, stops the music and removes any leftover events.
|
|
*/
|
|
override public function destroy():void
|
|
{
|
|
if(active)
|
|
stop();
|
|
}
|
|
|
|
/**
|
|
* An internal function used to help organize and change the volume of the sound.
|
|
*/
|
|
internal function updateTransform():void
|
|
{
|
|
//_transform.volume = FlxG.getMuteValue()*FlxG.volume*_volume*_volumeAdjust;
|
|
_transform.volume = FlxG.volume*_volume*_volumeAdjust;
|
|
if(_channel != null)
|
|
_channel.soundTransform = _transform;
|
|
}
|
|
|
|
/**
|
|
* An internal helper function used to help Flash resume playing a looped sound.
|
|
*
|
|
* @param event An <code>Event</code> object.
|
|
*/
|
|
protected function looped(event:Event=null):void
|
|
{
|
|
if (_channel == null)
|
|
return;
|
|
_channel.removeEventListener(Event.SOUND_COMPLETE,looped);
|
|
_channel = null;
|
|
play();
|
|
}
|
|
|
|
/**
|
|
* An internal helper function used to help Flash clean up and re-use finished sounds.
|
|
*
|
|
* @param event An <code>Event</code> object.
|
|
*/
|
|
protected function stopped(event:Event=null):void
|
|
{
|
|
if(!_looped)
|
|
_channel.removeEventListener(Event.SOUND_COMPLETE,stopped);
|
|
else
|
|
_channel.removeEventListener(Event.SOUND_COMPLETE,looped);
|
|
_channel = null;
|
|
active = false;
|
|
playing = false;
|
|
}
|
|
|
|
/**
|
|
* Internal event handler for ID3 info (i.e. fetching the song name).
|
|
*
|
|
* @param event An <code>Event</code> object.
|
|
*/
|
|
protected function gotID3(event:Event=null):void
|
|
{
|
|
FlxG.log("got ID3 info!");
|
|
if(_sound.id3.songName.length > 0)
|
|
name = _sound.id3.songName;
|
|
if(_sound.id3.artist.length > 0)
|
|
artist = _sound.id3.artist;
|
|
_sound.removeEventListener(Event.ID3, gotID3);
|
|
}
|
|
|
|
//this is just forwarding the event and bufferSize to the extraction function.
|
|
protected function sampleData( event:SampleDataEvent ):void
|
|
{
|
|
extract( event.data, bufferSize );
|
|
}
|
|
|
|
/**
|
|
* This methods extracts audio data from the mp3 and wraps it automatically with respect to encoder delay
|
|
*
|
|
* @param target The ByteArray where to write the audio data
|
|
* @param length The amount of samples to be read
|
|
*/
|
|
protected function extract( target: ByteArray, length:int ):void
|
|
{
|
|
if (samplesTotal == 0) return;
|
|
while( 0 < length )
|
|
{
|
|
if( samplesPosition + length > samplesTotal )
|
|
{
|
|
var read: int = samplesTotal - samplesPosition;
|
|
|
|
_in.extract( target, read, samplesPosition + MAGIC_DELAY );
|
|
|
|
samplesPosition += read;
|
|
|
|
length -= read;
|
|
}
|
|
else
|
|
{
|
|
_in.extract( target, length, samplesPosition + MAGIC_DELAY );
|
|
|
|
samplesPosition += length;
|
|
|
|
length = 0;
|
|
}
|
|
|
|
if( samplesPosition == samplesTotal ) // WE ARE AT THE END OF THE LOOP > WRAP
|
|
{
|
|
samplesPosition = loop_start;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|