Just got Drone FX approved. And thought I’d share some of the nitty-gritty discoveries I made along the way with ya’ll.
You can learn more about the app here:
http://www.drumbot.com/projects/dronefx/
Drone FX is an ambient music / generative / soundscape thing, which was spun off of my first app Key Chords.
After creating Key Chords, I realized that when I slowed everything down, some interesting melodies were created. And so I thought, why not replace the guitar notes with fatter, looping sounds and spread things out (time-wise)?
The concept was to have a whole bunch of “instruments” available, so as to allow for a wide variety of sounds. If you’re familiar with Brian Eno’s “Bloom” or any of the other ambient music apps out there, the music, although nice, is limited to just a few sounds. And things get old real quick.
I wanted to have tons of options for the user.
This meant that in order to avoid having an over-bloated 3 GB download, I would need to pitch-shift to cover a few octaves – thereby only requiring a few wav samples per instrument. So with the concept in hand, I marched onward.
Knowing full-well the limitations of Corona’s audio library, (due to my experience with Key Chords) I decided to trudge forward (reluctantly) and learn Objective-C.
With a fresh Objective-C ebook in hand, I methodically grasped the concepts of the Martian-like language. But, being the script baby that I am, I soon found myself scurrying around for an easier solution.
And then I happened upon this post:
The secret/undocumented audio APIs in Corona SDK
http://blog.anscamobile.com/2011/07/the-secretundocumented-audio-apis-in-corona-sdk/
And a glimmer of hope seeped through the “messaging.” I just MIGHT be able to use Corona!
And so, Drone FX was born.
Here are few things I learned through the process
Panning
Not as straight forward as you might think… this whole 3D sound is weird. Bottom line is that you need to use trigonometry to achieve left-to-right as in:
local \_sourceAudio = audio.loadSound("myTrack.mp3")
local \_options = {channel=3, loops=-1 }
local \_pan = 1.5 -- range is -1.5 to +1.5)
local \_chnl, \_src = audio.play(\_sourceAudio, \_options )
al.Source(\_src, al.POSITION, \_pan, 0.0, math.cos(\_pan) )
Timing
Keeping accurate timing is, well, not so accurate. I’m well aware of this concept from my Actionscript days.
There are a number of ways to approach this.
- Using system.getTimer() / enterFrame
- Using system.getTimer() / timer
- Using a wav / callback
#1 and #2 are pretty much the same, and are established as:
local syncTimer = {} -- forward reference the variable to keep the proper scope. (Only needed if using the "timer" method.)
local timerSpeed = 10 -- if using the "timer" method, calls occur every 10 milliseconds
local currentTime = 0
local prevTime = 0
local eventTrigger = 1000 -- trigger an event every second
local function syncRuntime(event)
currentTime = system.getTimer()
if (currentTime-prevTime) \>= eventTrigger then
-- do something
end
prevTime = currentTime
end
function start()
-- If using the timer method:
-- syncTimer = timer.performWithDelay(timerSpeed, syncRuntime, 0)
-- If opting for the enterFrame emthod
Runtime:addEventListener("enterFrame", syncRuntime)
end
function stop()
-- If using the timer method:
-- timer.cancel( syncTimer )
-- If opting for the enterFrame emthod
Runtime:removeEventListener("enterFrame", syncRuntime)
end
-- Start ASAP:
start()
The third option is to use the callback feature of a playing sound.
The idea is that you occupy one of your 32 channels with a “silent” wav. This silent wav is set to have a duration in accordance with how often you want your sync function to get pinged.
So the way this works is, if your “silence.wav” is 100 milliseconds in duration, then when it is finished playing we leverage the audio callback feature to ping the sync function, which re-loads the “silent” wav and established a loop that runs in increments directly related to the duration of the “silent.wav” file.
local function syncRuntime()
-- do something
end
local syncChannel = 1
local syncAudioFile = "\_sync\_control\_silence\_10ms.caf" -- The file's duration should be however many milliseconds you need to keep time. e.g. 10 millisonds, 100 millisonds, etc.
local syncLoader = audio.loadStream(syncAudioFile)
local function sync\_by\_audio()
syncRuntime()
-- We're setting the "onComplete" option, hence, this function gets called once the sound is complete, thereby setting up a loop that "clicks" at intervals determined by the duration of the control file.
audio.play(syncLoader, {channel = syncChannel, onComplete=function() sync\_by\_audio() end})
end
sync\_by\_audio()
I originally developed the wav file duration setup hoping that it would be a work-around to keep the app running while in the background. However, when an app is in the background, Corona essentially shuts down, so the “onComplete” event never fires.
I ultimately decided to go with the enterFrame method, simply because it was the easiest, and tied to the screen refresh, so it seems the most robust. Using the timer works great too, but I’m a little weary of timers in general, plus it seems like timers are not as “close” to the underlying runtime. But it’s all voodoo to me.
Destroying Audio
One other issue was the “build up” (in memory) of loaded sounds. Each time loadSound() is called, the wav data gets stored in memory until it is explicitly released via audio.dispose(). This sounds like an easy thing to do, but it’s not when you are preloading files and playing them from separate functions or “classes”.
The reason its tricky is because you can not “dispose” of a loaded sound unless it’s stopped. Which means you need to know the channel number and source handle prior to calling audio.dispose(). Because you need the channel number to call the audio.stop(channel), and the source handle to call the audio.dispose(source).
So the trick is to maintain and array that cross-references the channel to the source.
-- Array to hold the cross-reference between channels and sources
local channel2source = {}
-- Pre-populate the array with all the available channels. (Currently, there are 32 only 32 channels available in Corona). In this array, index will relate directly to the channel ID.
for i=1, 32 do
channel2source[i] = ""
end
-- Container to hold source references.
local mySlots = {}
-- A function to pre-load our sources, so we can play them quickly and efficiently.
function preloadTracks(files)
-- Before loading, we need to ensure that we dispose of any pre-existing sources, so as not to build up memory.
-- Loop through our source array.
for i=1, #mySlots do
-- The channel2source array uses string values for the loadSound (myAudio) to prevent any non-closures.
local mySrc = tostring(mySlots[i])
-- Loop through our cross-reference to discover any matches.
for k=1, 32 do
if channel2source[k] == mySrc then
audio.stop( k )
audio.dispose( mySrc )
end
end
end
for i=1, #files do
mySlots[i] = audio.loadSound(files[i])
end
end
local myFiles = {"track1.mp3", "track2.mp3", "track3.mp3"}
preloadTracks(myFiles)
-- Somewhere else we make a request to actually play a track. (Perhaps in another lua file, or another function.)
function playSlot(theSlot)
-- One of the secrets is that Corona returns two arguments when you play a track, the channel ID and the source, which we capture with te \_chnl and \_src respectively. Of course we can (and should) specify a channel in the audio.play([options]) area, and manage our channels directly... but another time.
local \_chnl, \_src = audio.play(mySlots[theSlot])
-- The channel2source array uses string values for the loadSound (myAudio) to prevent any non-closures.
-- Now we have a nice little setup to cross-reference which channel is playing which source:
channel2source[\_chnl] = tostring(\_src)
end
-- And now we can load tracks at any point in time. Since out preloadTracks() function properly disposes of any existing tracks, we no longer "build up" memory.
local myFiles = {"track11.mp3", "track12.mp3", "track13.mp3"}
preloadTracks(myFiles)
Hopefully, some of these tid-bits of code might help future music app developers!
[import]uid: 616 topic_id: 21617 reply_id: 321617[/import]