Drone FX

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:

1
2
3
4
5
6
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) )

Wow. Sounds awesome. I wish you the best of luck!

Naomi

Very nice! Thanks for sharing. Couple of questions/follow ups:

Panning:
That looks a little funny to me. Maybe what's going on is that the default OpenAL listener orientation is not facing the direction you think it should be. Probably setting the listener orientation would make setting the position for panning much more straight forward. (Imagine the listener is your head. The listener needs to turn so it is looking into the screen and its left ear points to the left side of the screen and the right ear points to the right side of the screen. Then you only need to pan across the x-value.)

Timing:
For the onComplete callback, what do you mean by backgrounding? Are you allowing audio to be played when the app is backgrounded? If so, I think technically you get the event when you return to the app, but yes, the Corona event system does effectively get shutdown durning backgrounding. (We hope to improve this someday.)

But if you are not allowing audio to be played when you leave the app, I thought Corona would pause the audio and resume when you come back. I would expect that when you resume, audio will resume and you will get the callback when it finishes. If this is not the case, I encourage you to file a bug.

Also, there is a similar discussion on getting more accurate timing here:
http://developer.anscamobile.com/forum/2011/08/15/drum-toy-0#comment-85323

WOW - this looks amazing!

Ewing,
It was hard-pressed to find any solid documentation on panning -- even in the openal and almixer docs. So I had to do a lot of trial and error. I've got a bruise on my forehead from the experience.

I suppose the best way to explain my understanding (how ever wrong it may be) is with a graphic:

In the graphic above (click here if you don't see the graphic):

A. Changing just the X value causes the sound to only be heard either on the left or the right. And we assume that the further away, the quieter the sound...

B. However, from trial and error, I found that no matter how "far away" I set the X value, the sound was always at the same level. (Or the volume levels were getting affected, preventing me from having a fixed volume level -- it's been a while since I worked on this :) Whatever the case, it just wasn't working as easily and seemlessly as I had hoped.

C. When setting the Z and X value, the variances seemed extreme and not what I expected. And I assumed that what you see in graphic C was what was happening, that at the extremes, the speaker was going way far away.

D. So I had a hunch that I needed to "curve" the Z value in order to maintain a consistent distance.

This seems to have worked out quite nicely for me, since I only had to contend with one value for the overall pan -- while maintaining a consistent volume level.

NOTE: Keeping the "pan" value between -1.5 and +1.5, prevents the cosine values from freaking out.

It's all still a little murky. I'm still confused on what al.POSITION refers to, the "speaker" or the "person"?

Since OpenAL is set up for games, the whole 3d concept is kind of overkill for a linear app like Drone FX.

When it comes to simple audio / basic sound manipulation (and the general idea of panning in a linear 2d world), we have to reverse-engineer the 3d ObjectAL world back down to 2d "linear" thinking.

Maybe I over-thought the whole situation. But at the very least, it works, and doesn't require any kind of manipulation of additional settings outside of al.Source(_src, al.POSITION, [pan_table])

Backgrounding

As for "backgrounding" -- yes, I was trying to figure out a way to get the app to work during multi-tasking. (This is where the other bruise on my forehead came from.)

What I was hoping to do was sneak out under Lua down to the C level where openal/almixer resides, by pushing a sound down just before going into the background and having openal/almixer at the C level ping back to lua through the "onComplete" event.

Hoping, beyond hope that my little scheme would work. But it didn't, and the Corona docs even clearly state the callbacks don't work once the app gets put into background mode.

My scheme was based on the observance that a long audio file (like a song or something) will continue to play if audio.play() was called before the app goes to the background. But once the song is finished, that's it, no more soup for you.

And that's how / why i developed the "main loop" based on an audio file with the "onComplete" callback. Eh, just a few hours down the tube :)

FYI: I ended up just keeping things simple and keeping UIApplicationExitsOnSuspend as the default value (not adding that flag to build.settings and just letting Corona deal with the default).

Thanks for the great description of your though process and how you use the openAL APIs. I'm sure that will help others.

Just a few comments:
1) Corona timers are based on the enterFrame event, which is typically 30 FPS, or 33.3 msec. This means if you try to set a timer to fire every 10 msec., it will fire every 33.3 msec. instead. If you set the FPS to 60, the time will be cut in half (every 16 msec.).

2). Setting UIApplicationExitsOnSuspend to false causes your app to suspend, which stops timers, display updates, and audio. The advantage of setting the flag to false is the app will generally resume (including any audio that was playing) when the app is started again. We currently don't support backgrounding audio when suspended.

Yes, despite OpenAL being essentially a long time industry standard, there isn't a lot of documentation out there. That's why I wrote a (comprehensive) book on it.

By the way, nice picture.

So I think a few things are off. First, I think z points the other direction.
Second, x-y-z positions should be completely linear, so it shouldn't be curving like your picture D.

So in OpenAL, a "source" is an object that emits sound. You can have multiple sources in a game, each making noise. So these can be missiles, lasers, rocket engines, explosions, etc. Every one of these objects has some x-y-z position in space. That is what the OpenAL source position refers to. (In ALmixer/Corona, a source has a 1-to-1 mapping to a channel; So with 32-channels, you have 32-sources.

In OpenAL, there is also a "listener". This is your head. There is only one listener allowed in OpenAL. The listener also has a x-y-z position in space.

OpenAL automatically calculates how to play the sound based on the relative positions of sources to the listener. So if the listener is a <0,0,0> (say center of the screen), and an explosion (source 1) is at position <-1,0,0> (to the left of the listener), the noise will come out the left speaker. If you move the explosion to <+2,0,0>, the noise will now come out of the right speaker.

But there are some additional things which are probably the reason things aren't quite right for you.

First, you need to figure out which direction the coordinate system on your device is oriented. On a desktop, I *think* x points to the right, y goes up screen, and z comes at your head like an arrow shooting through you.

But on a device, particularly where you've changed to landscape mode, it might be that everything is rotated. I might guess x goes up the screen, y points to the left, and z still comes at you.

To compensate, there is another property of the listener in OpenAL called the listener "orientation". Orientation is important because even though you might be standing in the center of the room, the direction you face determines which ear you hear sounds out of.

Once you set the orientation correctly, you should be able to pan simply by changing the x-value of the position from negative to positive and leave y and z at 0.0.

al.Source(_src, al.POSITION, {pan_value, 0, 0} )
One other detail is that OpenAL let's you control both the reference difference and how the sound decays. The reference distance lets you change the scale, e.g. you go from 0 to 10, or 0 to 128, or 0 1,000,000, etc. Then based on the distance, you can specify the decay curve which can be linear, exponential, or inverse. This just means how fast does the sound volume drop as it moves away. For music panning, you probably want to specify linear.

All this I talk about in great detail in my book and in some detail in this video (somewhere like 1hr 15min to 1hr 30min in).
http://www.youtube.com/watch?v=6QQAzhwalPI

By the way, have you tried the ALExplorer demo program?

(Also, just to throw it out there, there could be a bug in my luaal.c binding. It has not been heavily tested. It is open source, so anybody can inspect it.)

views:2714 update:2012/2/12 11:34:30
corona forums © 2003-2011