diff --git a/buildnumber.txt b/buildnumber.txt index 95bae2dc..1e27e32d 100644 --- a/buildnumber.txt +++ b/buildnumber.txt @@ -1 +1 @@ -446 \ No newline at end of file +478 \ No newline at end of file diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 6b155b22..6b2f4f68 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -46,7 +46,7 @@ class MainMenuState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - if (!FlxG.sound.music.playing) + if (FlxG.sound.music == null || !FlxG.sound.music.playing) { FlxG.sound.playMusic(Paths.music('freakyMenu')); } diff --git a/source/funkin/system/Main.hx b/source/funkin/system/Main.hx index f6105cf7..5d44b559 100644 --- a/source/funkin/system/Main.hx +++ b/source/funkin/system/Main.hx @@ -97,22 +97,45 @@ class Main extends Sprite @:dox(hide) public static var audioDisconnected:Bool = false; + public static var changeID:Int = 0; @:dox(hide) @:noCompletion - private #if !flash override #end function __enterFrame(deltaTime:Int):Void { - super.__enterFrame(deltaTime); + private function onStateSwitch(state:FlxState):Void { #if windows if (audioDisconnected) { - lime.media.AudioManager.suspend(); - audioDisconnected = false; - lime.media.AudioManager.init(); - // @:privateAccess - // for(e in openfl.media.SoundMixer.__soundChannels) { - // @:privateAccess - // e.__source.__backend.parent.buffer.__srcBuffer = null; - // @:privateAccess - // e.__source.__backend.init(); + // for(e in lime.media.openal.AL.buffers) { + // lime.media.openal.AL.deleteBuffer(e); // } + // lime.media.openal.AL.buffers = []; + // var buffers = [for(e in lime._internal.backend.native.NativeAudioSource.initBuffers) e]; + // trace(buffers.length); + + for(e in FlxG.sound.list) { + e.stop(); + } + if (FlxG.sound.music != null) + FlxG.sound.music.stop(); + + #if !lime_doc_gen + if (lime.media.AudioManager.context.type == OPENAL) + { + var alc = lime.media.AudioManager.context.openal; + + var device = alc.openDevice(); + var ctx = alc.createContext(device); + alc.makeContextCurrent(ctx); + alc.processContext(ctx); + } + #end + changeID++; + + // for(e in buffers) { + // @:privateAccess + // e.init(); + // } + + // lime._internal.backend.native.NativeAudioSource.initBuffers = []; + audioDisconnected = false; } #end } @@ -142,6 +165,8 @@ class Main extends Sprite FlxG.fixedTimestep = false; FlxG.autoPause = false; + FlxG.signals.preStateCreate.add(onStateSwitch); + Options.load(); WindowsAPI.registerAudio(); diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx new file mode 100644 index 00000000..a4c6fdff --- /dev/null +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -0,0 +1,603 @@ +package lime._internal.backend.native; + +import haxe.Int64; +import haxe.Timer; +import lime.math.Vector4; +import lime.media.openal.AL; +import lime.media.openal.ALBuffer; +import lime.media.openal.ALSource; +import lime.media.vorbis.VorbisFile; +import lime.media.AudioManager; +import lime.media.AudioSource; +import lime.utils.UInt8Array; + +#if !lime_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(lime.media.AudioBuffer) +class NativeAudioSource +{ + public static var initBuffers:Array = []; + + private static var STREAM_BUFFER_SIZE = 48000; + #if (native_audio_buffers && !macro) + private static var STREAM_NUM_BUFFERS = Std.parseInt(haxe.macro.Compiler.getDefine("native_audio_buffers")); + #else + private static var STREAM_NUM_BUFFERS = 3; + #end + private static var STREAM_TIMER_FREQUENCY = 100; + + private var buffers:Array; + private var bufferTimeBlocks:Array; + private var completed:Bool; + private var dataLength:Int; + private var format:Int; + private var handle:ALSource; + private var length:Null; + private var loops:Int; + private var parent:AudioSource; + private var playing:Bool; + private var position:Vector4; + private var samples:Int; + private var stream:Bool; + private var streamTimer:Timer; + private var timer:Timer; + + var disposed:Bool = false; + + 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; + } + disposed = true; + initBuffers.remove(this); + } + + public function init():Void + { + dataLength = 0; + format = 0; + + if (parent.buffer.channels == 1) + { + if (parent.buffer.bitsPerSample == 8) + { + format = AL.FORMAT_MONO8; + } + else if (parent.buffer.bitsPerSample == 16) + { + format = AL.FORMAT_MONO16; + } + } + else if (parent.buffer.channels == 2) + { + if (parent.buffer.bitsPerSample == 8) + { + format = AL.FORMAT_STEREO8; + } + else if (parent.buffer.bitsPerSample == 16) + { + format = AL.FORMAT_STEREO16; + } + } + + if (parent.buffer.__srcVorbisFile != null) + { + stream = true; + + var vorbisFile = parent.buffer.__srcVorbisFile; + dataLength = Std.int(Int64.toInt(vorbisFile.pcmTotal()) * parent.buffer.channels * (parent.buffer.bitsPerSample / 8)); + + buffers = new Array(); + bufferTimeBlocks = new Array(); + + for (i in 0...STREAM_NUM_BUFFERS) + { + buffers.push(AL.createBuffer()); + bufferTimeBlocks.push(0); + } + + handle = AL.createSource(); + } + else + { + if (parent.buffer.__srcBuffer == null) + { + parent.buffer.__srcBuffer = AL.createBuffer(); + + 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 = Std.int((dataLength * 8) / (parent.buffer.channels * parent.buffer.bitsPerSample)); + + initBuffers.push(this); + } + + public function play():Void + { + /*var pitch:Float = AL.getSourcef (handle, AL.PITCH); + trace(pitch); + AL.sourcef (handle, AL.PITCH, pitch*0.9); + pitch = AL.getSourcef (handle, AL.PITCH); + trace(pitch); */ + /*var pos = getPosition(); + trace(AL.DISTANCE_MODEL); + AL.distanceModel(AL.INVERSE_DISTANCE); + trace(AL.DISTANCE_MODEL); + AL.sourcef(handle, AL.ROLLOFF_FACTOR, 5); + setPosition(new Vector4(10, 10, -100)); + pos = getPosition(); + trace(pos); */ + /*var filter = AL.createFilter(); + trace(AL.getErrorString()); + + AL.filteri(filter, AL.FILTER_TYPE, AL.FILTER_LOWPASS); + trace(AL.getErrorString()); + + AL.filterf(filter, AL.LOWPASS_GAIN, 0.5); + trace(AL.getErrorString()); + + AL.filterf(filter, AL.LOWPASS_GAINHF, 0.5); + trace(AL.getErrorString()); + + AL.sourcei(handle, AL.DIRECT_FILTER, filter); + trace(AL.getErrorString()); */ + + if (disposed) { + trace("TRIED TO PLAY DISPOSED?? WTFFF"); + trace(haxe.CallStack.toString(haxe.CallStack.callStack())); + } + if (playing || handle == null) + { + 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); + } + } + + public function pause():Void + { + playing = false; + + if (handle == null) return; + AL.sourcePause(handle); + + if (streamTimer != null) + { + streamTimer.stop(); + } + + if (timer != null) + { + timer.stop(); + } + } + + private function readVorbisFileBuffer(vorbisFile:VorbisFile, length:Int):UInt8Array + { + #if lime_vorbis + var buffer = new UInt8Array(length); + var read = 0, total = 0, readMax; + + for (i in 0...STREAM_NUM_BUFFERS-1) + { + bufferTimeBlocks[i] = bufferTimeBlocks[i + 1]; + } + bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell(); + + while (total < length) + { + readMax = 4096; + + if (readMax > length - total) + { + readMax = length - total; + } + + read = vorbisFile.read(buffer.buffer, total, readMax); + + if (read > 0) + { + total += read; + } + else + { + break; + } + } + + return buffer; + #else + return null; + #end + } + + private function refillBuffers(buffers:Array = null):Void + { + #if lime_vorbis + var vorbisFile = null; + var position = 0; + + if (buffers == null) + { + var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED); + + if (buffersProcessed > 0) + { + vorbisFile = parent.buffer.__srcVorbisFile; + position = Int64.toInt(vorbisFile.pcmTell()); + + if (position < dataLength) + { + buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed); + } + } + } + + if (buffers != null) + { + if (vorbisFile == null) + { + vorbisFile = parent.buffer.__srcVorbisFile; + position = Int64.toInt(vorbisFile.pcmTell()); + } + + var numBuffers = 0; + var data; + + for (buffer in buffers) + { + if (dataLength - position >= STREAM_BUFFER_SIZE) + { + data = readVorbisFileBuffer(vorbisFile, STREAM_BUFFER_SIZE); + AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); + position += STREAM_BUFFER_SIZE; + numBuffers++; + } + else if (position < dataLength) + { + data = readVorbisFileBuffer(vorbisFile, dataLength - position); + AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate); + numBuffers++; + break; + } + } + + AL.sourceQueueBuffers(handle, numBuffers, buffers); + + // OpenAL can unexpectedly stop playback if the buffers run out + // of data, which typically happens if an operation (such as + // resizing a window) freezes the main thread. + // If AL is supposed to be playing but isn't, restart it here. + if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) + { + AL.sourcePlay(handle); + } + } + #end + } + + public function stop():Void + { + if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING) + { + AL.sourceStop(handle); + } + + playing = false; + + if (streamTimer != null) + { + streamTimer.stop(); + } + + if (timer != null) + { + timer.stop(); + } + + setCurrentTime(0); + } + + // 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(); + } + + completed = true; + parent.onComplete.dispatch(); + } + + // Get & Set Methods + public function getCurrentTime():Int + { + 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 = AL.getSourcei(handle, AL.BYTE_OFFSET); + var ratio = (offset / dataLength); + var totalSeconds = samples / parent.buffer.sampleRate; + + var time = Std.int(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; + } + } + + return 0; + } + + public function setCurrentTime(value:Int):Int + { + // `setCurrentTime()` has side effects and is never safe to skip. + /* if (value == getCurrentTime()) + { + return value; + } */ + + if (handle != null) + { + if (stream) + { + AL.sourceStop(handle); + + parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000); + AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS); + refillBuffers(buffers); + + if (playing) AL.sourcePlay(handle); + } + else if (parent.buffer != null) + { + AL.sourceRewind(handle); + + // AL.sourcef (handle, AL.SEC_OFFSET, (value + parent.offset) / 1000); + + var secondOffset = (value + parent.offset) / 1000; + var totalSeconds = samples / parent.buffer.sampleRate; + + if (secondOffset < 0) secondOffset = 0; + 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); + } + } + + if (playing) + { + if (timer != null) + { + timer.stop(); + } + + var timeRemaining = Std.int((getLength() - value) / getPitch()); + + if (timeRemaining > 0) + { + completed = false; + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + else + { + playing = false; + completed = true; + } + } + + return value; + } + + public function getGain():Float + { + if (handle != null) + { + return AL.getSourcef(handle, AL.GAIN); + } + else + { + return 1; + } + } + + public function setGain(value:Float):Float + { + if (handle != null) + { + AL.sourcef(handle, AL.GAIN, value); + } + + return value; + } + + public function getLength():Int + { + if (length != null) + { + return length; + } + + return Std.int(samples / parent.buffer.sampleRate * 1000) - parent.offset; + } + + public function setLength(value:Int):Int + { + if (playing && length != value) + { + if (timer != null) + { + timer.stop(); + } + + var timeRemaining = Std.int((value - getCurrentTime()) / getPitch()); + + if (timeRemaining > 0) + { + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + } + + return length = value; + } + + public function getLoops():Int + { + return loops; + } + + public function setLoops(value:Int):Int + { + return loops = value; + } + + public function getPitch():Float + { + if (handle != null) + { + return AL.getSourcef(handle, AL.PITCH); + } + else + { + return 1; + } + } + + public function setPitch(value:Float):Float + { + if (playing && value != getPitch()) + { + if (timer != null) + { + timer.stop(); + } + + var timeRemaining = Std.int((getLength() - getCurrentTime()) / value); + + if (timeRemaining > 0) + { + timer = new Timer(timeRemaining); + timer.run = timer_onRun; + } + } + + if (handle != null) + { + AL.sourcef(handle, AL.PITCH, value); + } + + return value; + } + + 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 + } + + return position; + } + + 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) + { + AL.distanceModel(AL.NONE); + AL.source3f(handle, AL.POSITION, position.x, position.y, position.z); + } + + return position; + } +} diff --git a/source/lime/media/AudioSource.hx b/source/lime/media/AudioSource.hx new file mode 100644 index 00000000..ab0992e4 --- /dev/null +++ b/source/lime/media/AudioSource.hx @@ -0,0 +1,139 @@ +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 +class AudioSource +{ + public var onComplete = new EventVoid>(); + public var buffer:AudioBuffer; + public var currentTime(get, set):Int; + public var gain(get, set):Float; + public var length(get, set):Int; + public var loops(get, set):Int; + public var pitch(get, set):Float; + public var offset:Int; + public var position(get, set):Vector4; + + @:noCompletion private var __backend:AudioSourceBackend; + + public function new(buffer:AudioBuffer = null, offset:Int = 0, length:Null = null, loops:Int = 0) + { + this.buffer = buffer; + this.offset = offset; + + __backend = new AudioSourceBackend(this); + + if (length != null && length != 0) + { + this.length = length; + } + + this.loops = loops; + + if (buffer != null) + { + init(); + } + } + + public function dispose():Void + { + __backend.dispose(); + } + + @:noCompletion private function init():Void + { + __backend.init(); + } + + public function play():Void + { + __backend.play(); + } + + public function pause():Void + { + __backend.pause(); + } + + public function stop():Void + { + __backend.stop(); + } + + // Get & Set Methods + @:noCompletion private function get_currentTime():Int + { + return __backend.getCurrentTime(); + } + + @:noCompletion private function set_currentTime(value:Int):Int + { + return __backend.setCurrentTime(value); + } + + @:noCompletion private function get_gain():Float + { + return __backend.getGain(); + } + + @:noCompletion private function set_gain(value:Float):Float + { + return __backend.setGain(value); + } + + @:noCompletion private function get_length():Int + { + return __backend.getLength(); + } + + @:noCompletion private function set_length(value:Int):Int + { + return __backend.setLength(value); + } + + @:noCompletion private function get_loops():Int + { + return __backend.getLoops(); + } + + @:noCompletion private function set_loops(value:Int):Int + { + return __backend.setLoops(value); + } + + @:noCompletion private function get_pitch():Float + { + return __backend.getPitch(); + } + + @:noCompletion private function set_pitch(value:Float):Float + { + return __backend.setPitch(value); + } + + @:noCompletion private function get_position():Vector4 + { + return __backend.getPosition(); + } + + @:noCompletion private function set_position(value:Vector4):Vector4 + { + return __backend.setPosition(value); + } +} + +#if flash +@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.flash.FlashAudioSource; +#elseif (js && html5) +@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.html5.HTML5AudioSource; +#else +@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.native.NativeAudioSource; +#end diff --git a/source/openfl/media/Sound.hx b/source/openfl/media/Sound.hx new file mode 100644 index 00000000..1ecffcd2 --- /dev/null +++ b/source/openfl/media/Sound.hx @@ -0,0 +1,730 @@ +package openfl.media; + +#if !flash +import haxe.Int64; +import openfl.events.Event; +import openfl.events.EventDispatcher; +import openfl.events.IOErrorEvent; +import openfl.net.URLRequest; +import openfl.utils.ByteArray; +import openfl.utils.Future; +#if lime +import openfl.utils._internal.UInt8Array; +import lime.media.AudioBuffer; +import lime.media.AudioSource; +#end + +/** + The Sound class lets you work with sound in an application. The Sound class + lets you create a Sound object, load and play an external MP3 file into + that object, close the sound stream, and access data about the sound, such + as information about the number of bytes in the stream and ID3 metadata. + More detailed control of the sound is performed through the sound source + - the SoundChannel or Microphone object for the sound - and through the + properties in the SoundTransform class that control the output of the sound + to the computer's speakers. + + In Flash Player 10 and later and AIR 1.5 and later, you can also use + this class to work with sound that is generated dynamically. In this case, + the Sound object uses the function you assign to a `sampleData` + event handler to poll for sound data. The sound is played as it is + retrieved from a ByteArray object that you populate with sound data. You + can use `Sound.extract()` to extract sound data from a Sound + object, after which you can manipulate it before writing it back to the + stream for playback. + + To control sounds that are embedded in a SWF file, use the properties in + the SoundMixer class. + + **Note**: The ActionScript 3.0 Sound API differs from ActionScript + 2.0. In ActionScript 3.0, you cannot take sound objects and arrange them in + a hierarchy to control their properties. + + When you use this class, consider the following security model: + + + * Loading and playing a sound is not allowed if the calling file is in + a network sandbox and the sound file to be loaded is local. + * By default, loading and playing a sound is not allowed if the calling + file is local and tries to load and play a remote sound. A user must grant + explicit permission to allow this type of access. + * Certain operations dealing with sound are restricted. The data in a + loaded sound cannot be accessed by a file in a different domain unless you + implement a cross-domain policy file. Sound-related APIs that fall under + this restriction are `Sound.id3`, + `SoundMixer.computeSpectrum()`, + `SoundMixer.bufferTime`, and the `SoundTransform` + class. + + + However, in Adobe AIR, content in the `application` security + sandbox(content installed with the AIR application) are not restricted by + these security limitations. + + For more information related to security, see the Flash Player Developer + Center Topic: [Security](http://www.adobe.com/go/devnet_security_en). + + @event complete Dispatched when data has loaded successfully. + @event id3 Dispatched by a Sound object when ID3 data is available + for an MP3 sound. + @event ioError Dispatched when an input/output error occurs that causes + a load operation to fail. + @event open Dispatched when a load operation starts. + @event progress Dispatched when data is received as a load operation + progresses. + @event sampleData Dispatched when the runtime requests new audio data. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(lime.media.AudioBuffer) +@:access(lime.utils.AssetLibrary) +@:access(openfl.media.SoundMixer) +@:access(openfl.media.SoundChannel.new) +@:autoBuild(openfl.utils._internal.AssetsMacro.embedSound()) +class Sound extends EventDispatcher +{ + /** + Returns the currently available number of bytes in this sound object. This + property is usually useful only for externally loaded files. + **/ + public var bytesLoaded(default, null):Int; + + /** + Returns the total number of bytes in this sound object. + **/ + public var bytesTotal(default, null):Int; + + /** + Provides access to the metadata that is part of an MP3 file. + MP3 sound files can contain ID3 tags, which provide metadata about the + file. If an MP3 sound that you load using the `Sound.load()` method + contains ID3 tags, you can query these properties. Only ID3 tags that + use the UTF-8 character set are supported. + + Flash Player 9 and later and AIR support ID3 2.0 tags, specifically + 2.3 and 2.4. The following tables list the standard ID3 2.0 tags and + the type of content the tags represent. The `Sound.id3` property + provides access to these tags through the format `my_sound.id3.COMM`, + `my_sound.id3.TIME`, and so on. The first table describes tags that + can be accessed either through the ID3 2.0 property name or the + ActionScript property name. The second table describes ID3 tags that + are supported but do not have predefined properties in ActionScript. + + | ID3 2.0 tag | Corresponding Sound class property | + | --- | --- | + | COMM | Sound.id3.comment | + | TALB | Sound.id3.album | + | TCON | Sound.id3.genre | + | TIT2 | Sound.id3.songName | + | TPE1 | Sound.id3.artist | + | TRCK | Sound.id3.track | + | TYER | Sound.id3.year | + + The following table describes ID3 tags that are supported but do not + have predefined properties in the Sound class. You access them by + calling `mySound.id3.TFLT`, `mySound.id3.TIME`, and so on. **NOTE:** + None of these tags are supported in Flash Lite 4. + + | Property | Description | + | --- | --- | + | TFLT | File type | + | TIME | Time | + | TIT1 | Content group description | + | TIT2 | Title/song name/content description | + | TIT3 | Subtitle/description refinement | + | TKEY | Initial key | + | TLAN | Languages | + | TLEN | Length | + | TMED | Media type | + | TOAL | Original album/movie/show title | + | TOFN | Original filename | + | TOLY | Original lyricists/text writers | + | TOPE | Original artists/performers | + | TORY | Original release year | + | TOWN | File owner/licensee | + | TPE1 | Lead performers/soloists | + | TPE2 | Band/orchestra/accompaniment | + | TPE3 | Conductor/performer refinement | + | TPE4 | Interpreted, remixed, or otherwise modified by | + | TPOS | Part of a set | + | TPUB | Publisher | + | TRCK | Track number/position in set | + | TRDA | Recording dates | + | TRSN | Internet radio station name | + | TRSO | Internet radio station owner | + | TSIZ | Size | + | TSRC | ISRC (international standard recording code) | + | TSSE | Software/hardware and settings used for encoding | + | TYER | Year | + | WXXX | URL link frame | + + When using this property, consider the Flash Player security model: + + * The `id3` property of a Sound object is always permitted for SWF + files that are in the same security sandbox as the sound file. For + files in other sandboxes, there are security checks. + * When you load the sound, using the `load()` method of the Sound + class, you can specify a `context` parameter, which is a + SoundLoaderContext object. If you set the `checkPolicyFile` property + of the SoundLoaderContext object to `true`, Flash Player checks for a + URL policy file on the server from which the sound is loaded. If a + policy file exists and permits access from the domain of the loading + SWF file, then the file is allowed to access the `id3` property of the + Sound object; otherwise it is not. + + However, in Adobe AIR, content in the `application` security sandbox + (content installed with the AIR application) are not restricted by + these security limitations. + + For more information related to security, see the Flash Player + Developer Center Topic: Security. + **/ + public var id3(get, never):ID3Info; + + /** + Returns the buffering state of external MP3 files. If the value is + `true`, any playback is currently suspended while the object + waits for more data. + **/ + public var isBuffering(default, null):Bool; + + // @:noCompletion @:dox(hide) @:require(flash10_1) public var isURLInaccessible (default, null):Bool; + + /** + The length of the current sound in milliseconds. + **/ + public var length(get, never):Float; + + /** + The URL from which this sound was loaded. This property is applicable only + to Sound objects that were loaded using the `Sound.load()` + method. For Sound objects that are associated with a sound asset from a + SWF file's library, the value of the `url` property is + `null`. + + When you first call `Sound.load()`, the `url` + property initially has a value of `null`, because the final URL + is not yet known. The `url` property will have a non-null value + as soon as an `open` event is dispatched from the Sound + object. + + The `url` property contains the final, absolute URL from + which a sound was loaded. The value of `url` is usually the + same as the value passed to the `stream` parameter of + `Sound.load()`. However, if you passed a relative URL to + `Sound.load()` the value of the `url` property + represents the absolute URL. Additionally, if the original URL request is + redirected by an HTTP server, the value of the `url` property + reflects the final URL from which the sound file was actually downloaded. + This reporting of an absolute, final URL is equivalent to the behavior of + `LoaderInfo.url`. + + In some cases, the value of the `url` property is truncated; + see the `isURLInaccessible` property for details. + **/ + public var url(default, null):String; + + #if lime + @:noCompletion private var __buffer:AudioBuffer; + #end + + #if openfljs + @:noCompletion private static function __init__() + { + untyped Object.defineProperties(Sound.prototype, { + "id3": {get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_id3 (); }")}, + "length": {get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_length (); }")}, + }); + } + #end + + /** + Creates a new Sound object. If you pass a valid URLRequest object to the + Sound constructor, the constructor automatically calls the + `load()` function for the Sound object. If you do not pass a + valid URLRequest object to the Sound constructor, you must call the + `load()` function for the Sound object yourself, or the stream + will not load. + + Once `load()` is called on a Sound object, you can't later + load a different sound file into that Sound object. To load a different + sound file, create a new Sound object. + In Flash Player 10 and later and AIR 1.5 and later, instead of using + `load()`, you can use the `sampleData` event handler + to load sound dynamically into the Sound object. + + @param stream The URL that points to an external MP3 file. + @param context An optional SoundLoader context object, which can define + the buffer time(the minimum number of milliseconds of MP3 + data to hold in the Sound object's buffer) and can specify + whether the application should check for a cross-domain + policy file prior to loading the sound. + **/ + public function new(stream:URLRequest = null, context:SoundLoaderContext = null) + { + super(this); + + bytesLoaded = 0; + bytesTotal = 0; + isBuffering = false; + url = null; + + if (stream != null) + { + load(stream, context); + } + } + + /** + Closes the stream, causing any download of data to cease. No data may + be read from the stream after the `close()` method is called. + + @throws IOError The stream could not be closed, or the stream was not + open. + **/ + public function close():Void + { + #if lime + if (__buffer != null) + { + __buffer.dispose(); + __buffer = null; + } + #end + } + + #if false + /** + Extracts raw sound data from a Sound object. + This method is designed to be used when you are working with + dynamically generated audio, using a function you assign to the + `sampleData` event for a different Sound object. That is, you can use + this method to extract sound data from a Sound object. Then you can + write the data to the byte array that another Sound object is using to + stream dynamic audio. + + The audio data is placed in the target byte array starting from the + current position of the byte array. The audio data is always exposed + as 44100 Hz Stereo. The sample type is a 32-bit floating-point value, + which can be converted to a Number using `ByteArray.readFloat()`. + + @param target A ByteArray object in which the extracted sound samples + are placed. + @param length The number of sound samples to extract. A sample + contains both the left and right channels × that is, + two 32-bit floating-point values. + @return The number of samples written to the ByteArray specified in + the `target` parameter. + **/ + // @:noCompletion @:dox(hide) @:require(flash10) public function extract (target:ByteArray, length:Float, startPosition:Float = -1):Float; + #end + + #if lime + /** + Creates a new Sound from an AudioBuffer immediately. + + @param buffer An AudioBuffer instance + @returns A new Sound + **/ + public static function fromAudioBuffer(buffer:AudioBuffer):Sound + { + var sound = new Sound(); + sound.__buffer = buffer; + return sound; + } + #end + + /** + Creates a new Sound from a file path synchronously. This means that the + Sound will be returned immediately (if supported). + + HTML5 and Flash do not support creating Sound synchronously, so these targets + always return `null`. + + In order to load files from a remote web address, use the `loadFromFile` method, + which supports asynchronous loading. + + @param path A local file path containing a sound + @returns A new Sound if successful, or `null` if unsuccessful + **/ + public static function fromFile(path:String):Sound + { + #if lime + return fromAudioBuffer(AudioBuffer.fromFile(path)); + #else + return null; + #end + } + + /** + Initiates loading of an external MP3 file from the specified URL. If you + provide a valid URLRequest object to the Sound constructor, the + constructor calls `Sound.load()` for you. You only need to call + `Sound.load()` yourself if you don't pass a valid URLRequest + object to the Sound constructor or you pass a `null` value. + + Once `load()` is called on a Sound object, you can't later + load a different sound file into that Sound object. To load a different + sound file, create a new Sound object. + + When using this method, consider the following security model: + + * Calling `Sound.load()` is not allowed if the calling file + is in the local-with-file-system sandbox and the sound is in a network + sandbox. + * Access from the local-trusted or local-with-networking sandbox + requires permission from a website through a URL policy file. + * You cannot connect to commonly reserved ports. For a complete list + of blocked ports, see "Restricting Networking APIs" in the _ActionScript + 3.0 Developer's Guide_. + * You can prevent a SWF file from using this method by setting the + `allowNetworking` parameter of the `object` and + `embed` tags in the HTML page that contains the SWF + content. + + In Flash Player 10 and later, if you use a multipart Content-Type(for + example "multipart/form-data") that contains an upload(indicated by a + "filename" parameter in a "content-disposition" header within the POST + body), the POST operation is subject to the security rules applied to + uploads: + + * The POST operation must be performed in response to a user-initiated + action, such as a mouse click or key press. + * If the POST operation is cross-domain(the POST target is not on the + same server as the SWF file that is sending the POST request), the target + server must provide a URL policy file that permits cross-domain + access. + + + Also, for any multipart Content-Type, the syntax must be valid + (according to the RFC2046 standards). If the syntax appears to be invalid, + the POST operation is subject to the security rules applied to + uploads. + + In Adobe AIR, content in the `application` security sandbox + (content installed with the AIR application) are not restricted by these + security limitations. + + For more information related to security, see the Flash Player + Developer Center Topic: [Security](http://www.adobe.com/go/devnet_security_en). + + @param stream A URL that points to an external MP3 file. + @param context An optional SoundLoader context object, which can define + the buffer time(the minimum number of milliseconds of MP3 + data to hold in the Sound object's buffer) and can specify + whether the application should check for a cross-domain + policy file prior to loading the sound. + @throws IOError A network error caused the load to fail. + @throws IOError The `digest` property of the + `stream` object is not `null`. + You should only set the `digest` property + of a URLRequest object when calling the + `URLLoader.load()` method when loading a + SWZ file(an Adobe platform component). + @throws SecurityError Local untrusted files may not communicate with the + Internet. You can work around this by reclassifying + this file as local-with-networking or trusted. + @throws SecurityError You cannot connect to commonly reserved ports. For a + complete list of blocked ports, see "Restricting + Networking APIs" in the _ActionScript 3.0 + Developer's Guide_. + **/ + public function load(stream:URLRequest, context:SoundLoaderContext = null):Void + { + url = stream.url; + + #if lime + #if (js && html5) + var defaultLibrary = lime.utils.Assets.getLibrary("default"); // TODO: Improve this + + if (defaultLibrary != null && defaultLibrary.cachedAudioBuffers.exists(url)) + { + AudioBuffer_onURLLoad(defaultLibrary.cachedAudioBuffers.get(url)); + } + else + { + AudioBuffer.loadFromFile(url).onComplete(AudioBuffer_onURLLoad).onError(function(_) + { + AudioBuffer_onURLLoad(null); + }); + } + #else + AudioBuffer.loadFromFile(url).onComplete(AudioBuffer_onURLLoad).onError(function(_) + { + AudioBuffer_onURLLoad(null); + }); + #end + #end + } + + /** + Load MP3 sound data from a ByteArray object into a Sound object. The data will be read from the current + ByteArray position and will leave the ByteArray position at the end of the specified bytes length once + finished. If the MP3 sound data contains ID3 data ID3 events will be dispatched during this function call. + This function will throw an exception if the ByteArray object does not contain enough data. + + @param bytes + @param bytesLength + **/ + public function loadCompressedDataFromByteArray(bytes:ByteArray, bytesLength:Int):Void + { + if (bytes == null || bytesLength <= 0) + { + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + return; + } + + if (bytes.position > 0 || bytes.length > bytesLength) + { + var copy = new ByteArray(bytesLength); + copy.writeBytes(bytes, bytes.position, bytesLength); + bytes = copy; + } + + #if lime + __buffer = AudioBuffer.fromBytes(bytes); + + if (__buffer == null) + { + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + } + else + { + dispatchEvent(new Event(Event.COMPLETE)); + } + #else + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + #end + } + + /** + Creates a new Sound from a file path or web address asynchronously. The file + load will occur in the background. + + Progress, completion and error callbacks will be dispatched in the current + thread using callbacks attached to a returned Future object. + + @param path A local file path or web address containing a sound + @returns A Future Sound + **/ + public static function loadFromFile(path:String):Future + { + #if lime + return AudioBuffer.loadFromFile(path).then(function(audioBuffer) + { + return Future.withValue(fromAudioBuffer(audioBuffer)); + }); + #else + return cast Future.withError("Cannot load audio file"); + #end + } + + /** + Creates a new Sound from a set of file paths or web addresses asynchronously. + The audio backend will choose the first compatible file format, and will load the file + it selects in the background. + + Progress, completion and error callbacks will be dispatched in the current + thread using callbacks attached to a returned Future object. + + @param paths A set of local file paths or web addresses containing sound + @returns A Future Sound + **/ + public static function loadFromFiles(paths:Array):Future + { + #if lime + return AudioBuffer.loadFromFiles(paths).then(function(audioBuffer) + { + return Future.withValue(fromAudioBuffer(audioBuffer)); + }); + #else + return cast Future.withError("Cannot load audio files"); + #end + } + + /** + Load PCM 32-bit floating point sound data from a ByteArray object into a Sound object. The data will be read + from the current ByteArray position and will leave the ByteArray position at the end of the specified sample + length multiplied by either 1 channel or 2 channels if the stereo flag is set once finished. + + Starting with Flash Player 11.8, the amount of audio data that can be passed to this function is limited. For + SWF versions >= 21, this function throws an exception if the amount of audio data passed into this function is + more than 1800 seconds. That is, samples / sampleRate should be less than or equal to 1800. For swf versions < + 21, the runtime fails silently if the amount of audio data passed in is more than 12000 seconds. This is + provided only for backward compatibility. + + This function throws an exception if the ByteArray object does not contain enough data. + + @param bytes + @param samples + @param format + @param stereo + @param sampleRate + **/ + public function loadPCMFromByteArray(bytes:ByteArray, samples:Int, format:String = "float", stereo:Bool = true, sampleRate:Float = 44100):Void + { + if (bytes == null) + { + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + return; + } + + var bitsPerSample = (format == "float" ? 32 : 16); // "short" + var channels = (stereo ? 2 : 1); + var bytesLength = Std.int(samples * channels * (bitsPerSample / 8)); + + if (bytes.position > 0 || bytes.length > bytesLength) + { + var copy = new ByteArray(bytesLength); + copy.writeBytes(bytes, bytes.position, bytesLength); + bytes = copy; + } + + #if lime + var audioBuffer = new AudioBuffer(); + audioBuffer.bitsPerSample = bitsPerSample; + audioBuffer.channels = channels; + audioBuffer.data = new UInt8Array(bytes); + audioBuffer.sampleRate = Std.int(sampleRate); + + __buffer = audioBuffer; + + dispatchEvent(new Event(Event.COMPLETE)); + #else + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + #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 + monitor volume.(To control the volume, panning, and balance, access the + SoundTransform object assigned to the sound channel.) + + @param startTime The initial position in milliseconds at which playback + should start. + @param loops Defines the number of times a sound loops back to the + `startTime` value before the sound channel + stops playback. + @param sndTransform The initial SoundTransform object assigned to the + sound channel. + @return A SoundChannel object, which you use to control the sound. This + method returns `null` if you have no sound card or if + you run out of available sound channels. The maximum number of + sound channels available at once is 32. + **/ + public function play(startTime:Float = 0.0, loops:Int = 0, sndTransform:SoundTransform = null):SoundChannel + { + #if lime + if (__buffer == null || SoundMixer.__soundChannels.length >= SoundMixer.MAX_ACTIVE_CHANNELS) + { + return null; + } + + if (changeID < funkin.system.Main.changeID) { + changeID = funkin.system.Main.changeID; + regen(); + } + + if (sndTransform == null) + { + sndTransform = new SoundTransform(); + } + else + { + sndTransform = sndTransform.clone(); + } + + var pan = SoundMixer.__soundTransform.pan + sndTransform.pan; + + if (pan > 1) pan = 1; + if (pan < -1) pan = -1; + + var volume = SoundMixer.__soundTransform.volume * sndTransform.volume; + + var source = new AudioSource(__buffer); + source.offset = Std.int(startTime); + if (loops > 1) source.loops = loops - 1; + + source.gain = volume; + + var position = source.position; + position.x = pan; + position.z = -1 * Math.sqrt(1 - Math.pow(pan, 2)); + source.position = position; + + return new SoundChannel(source, sndTransform); + #else + return null; + #end + } + + // Get & Set Methods + @:noCompletion private function get_id3():ID3Info + { + return new ID3Info(); + } + + @:noCompletion private function get_length():Int + { + #if lime + if (__buffer != null) + { + #if (js && html5 && howlerjs) + return Std.int(__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); + } + else if (__buffer.__srcVorbisFile != null) + { + var samples = Int64.toInt(__buffer.__srcVorbisFile.pcmTotal()); + return Std.int(samples / __buffer.sampleRate * 1000); + } + else + { + return 0; + } + #end + } + #end + + return 0; + } + + // Event Handlers + #if lime + @:noCompletion private function AudioBuffer_onURLLoad(buffer:AudioBuffer):Void + { + if (buffer == null) + { + dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR)); + } + else + { + __buffer = buffer; + dispatchEvent(new Event(Event.COMPLETE)); + } + } + #end +} +#else +typedef Sound = flash.media.Sound; +#end