0

Basically I'm using AS3 to generate a tone. I need to be able to pass my function an array which would look something like {0,50,100,0,20,500,200,100} each representing milliseconds. It would just be like "off, on, off, on,off" ect and I need the tone to play exactly down to the millisecond with no delays or hiccups.

I tried making a function with timers to accomplish this...but it's really not as precise as I need it to be. There are slight delays, and it's noticeably not playing the really short ones as short as they need to be.

I was thinking I'd just play my tone, then use a SoundTransform to toggle the volume on and off, and that could help make it faster since i'm not starting and stopping a sound, i'm just manipulating the volume in real time.

But maybe it's not the volume that's slowing it down, maybe it's just the timers aren't all that reliable. Here's my code, the function just loops until I make it stop with another function I have. Any suggestions on how to get this to be more precise?

My function that handles the array with all the timers

private function soundPattern(patternArr:Array):void
        {
            //setup vars
            var pTotal:Number = patternArr.length;
            var pCount:Number = 0;

            if(pTotal >=1)
            {
                //setup listenrs
                patTimer = new Timer(patternArr[pCount],1);
                patTimer.addEventListener(TimerEvent.TIMER_COMPLETE, comp);

                function comp(e:TimerEvent=null):void
                {
                    pCount++;
                    if(pCount != pTotal)
                    {
                        patTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, comp);

                        toneGen.soundTrans.volume=1;
                        toneGen.toneChannel.soundTransform = toneGen.soundTrans;

                        patTimer = new Timer(patternArr[pCount],1);
                        patTimer.addEventListener(TimerEvent.TIMER_COMPLETE, compTwo);

                        if(patternArr[pCount]>0)
                        {
                            patTimer.reset();
                            patTimer.start();
                        }
                        else
                        {
                            compTwo();
                        }
                    }
                    else if(repeat)
                    {
                        trace("1resetting...");
                        patTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, comp);
                        pCount = 0;
                        patTimer = new Timer(patternArr[pCount],1);
                        patTimer.addEventListener(TimerEvent.TIMER_COMPLETE, compTwo);

                        toneGen.soundTrans.volume=0;
                        toneGen.toneChannel.soundTransform = toneGen.soundTrans;

                        if(patternArr[pCount]>0)
                        {
                            patTimer.reset();
                            patTimer.start();
                        }
                        else
                        {
                            compTwo();
                        }
                    }
                }

                //in-between
                function compTwo(e:TimerEvent=null):void
                {
                    pCount++;
                    if(pCount != pTotal)
                    {
                        patTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, compTwo);
                        patTimer = new Timer(patternArr[pCount],1);
                        patTimer.addEventListener(TimerEvent.TIMER_COMPLETE, comp);

                        toneGen.soundTrans.volume=0;
                        toneGen.toneChannel.soundTransform = toneGen.soundTrans;

                        if(patternArr[pCount]>0)
                        {
                            patTimer.reset();
                            patTimer.start();
                        }
                        else
                        {
                            comp();
                        }
                    }
                    else if(repeat)
                    {
                        trace("2resetting...");
                        patTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, compTwo);
                        pCount = 0;
                        patTimer = new Timer(patternArr[pCount],1);
                        patTimer.addEventListener(TimerEvent.TIMER_COMPLETE, comp);

                        toneGen.soundTrans.volume=0;
                        toneGen.toneChannel.soundTransform = toneGen.soundTrans;

                        if(patternArr[pCount]>0)
                        {
                            patTimer.reset();
                            patTimer.start();
                        }
                        else
                        {
                            comp();
                        }
                    }
                }


                //get the tone started, but remember the first is a pause
                toneGen.startTone();

                //start things
                if(patternArr[pCount]>0)
                {
                    patTimer.reset();
                    patTimer.start();
                }
                else
                {
                    comp();
                }
            }
        }

and here's the toneGen class i'm using

package
{
    import flash.events.SampleDataEvent;
    import flash.media.Sound;
    import flash.media.SoundChannel;
    import flash.media.SoundTransform;

    public class ToneGenerator {

        [Bindable]
        public var amp_multiplier_right:Number = 0.5;
        [Bindable]
        public var amp_multiplier_left:Number = 0.5;
        [Bindable]
        public var freq_right:Number = 580;
        [Bindable]
        public var freq_left:Number = 580;

        public static const SAMPLING_RATE:int = 44100;
        public static const TWO_PI:Number = 2*Math.PI;
        public static const TWO_PI_OVER_SR:Number = TWO_PI/SAMPLING_RATE;

        public var tone:Sound;
        public var toneChannel:SoundChannel;
        public var soundTrans:SoundTransform;

        public function ToneGenerator() {
        }

        public function stopTone():void {

            if(tone)
            {
                toneChannel.stop();
                tone.removeEventListener(SampleDataEvent.SAMPLE_DATA, generateSineTone);
            }

        }

        public function startTone():void {

            tone = new Sound();

            tone.addEventListener(SampleDataEvent.SAMPLE_DATA, generateSineTone);

            soundTrans = new SoundTransform(0);
            toneChannel = tone.play();
            toneChannel.soundTransform = soundTrans;

        }

        public function generateSineTone(e:SampleDataEvent):void {

            var sample:Number;

            for(var i:int=0;i<8192;i++) {
                sample = Math.sin((i+e.position) * TWO_PI_OVER_SR * freq_left);
                e.data.writeFloat(sample * amp_multiplier_left);
                sample = Math.sin((i+e.position) * TWO_PI_OVER_SR * freq_right);
                e.data.writeFloat(sample * amp_multiplier_right);
            }  

        }


    }
}
brybam
  • 5,009
  • 12
  • 51
  • 93

3 Answers3

2

Timers are notoriously not accurate (this is due to the architecture of the Flash Player : read this).

Whenever you need accurate time-based calculation, use the getTimer() method to calculate the time elapsed between two moments in time. You will also need a way to have a tick() method as frequently as possible (this will be your accuracy), and in that case you can use a Timer or even Event.ENTER_FRAME.

var ticker:Sprite = new Sprite();
sprite.addEventListener(Event.ENTER_FRAME, tick);

const delay:uint = 300;

var timeReference:uint;
var lastTimeReference:uint = getTimer();

function tick(evt:Event):void {
   timeReference = getTimer();
   if(timeReference - lastTimeReference >= delay)
   {
      trace("delay reached");
      lastTimeReference = timeReference;
   }

}
Antoine Lassauzay
  • 1,557
  • 11
  • 12
  • Alright, I basically re-created what I have up top but rather than using the Timer, i'm using an enter frame event on an empty sprite like in your example. It seems to be more consistent, which is definitely a step in the right direction. But, it still doesn't seem to be as fast as it needs to be. I'm thinking it's because it's an enterframe event...and when i have a sound pattern that's like `{30,20,30,20}` that might be too fast for 24 fps. You mentioned using the Timer for my tick...but that's exactly what the problem was in the first place, you mentioned it's unreliable. any ideas? – brybam Oct 28 '12 at 18:38
  • Yes, 30 or 20 ms is too small. You can calculate the theorical refresh rate by doing (1000ms/FPS), for example 1000/30 = 33ms. If you look at the link I've attached in my answer, there is an explanation on how everything is related to the rendering model. See also http://www.craftymind.com/2008/04/18/updated-elastic-racetrack-for-flash-9-and-avm2/. The problem basically is that you can't have precise timer because the rendering and code execution impacts the refresh rate. Timers don't run independently of everything else. – Antoine Lassauzay Oct 28 '12 at 19:33
0

Since you are generating the tones by creating raw sample data yourself, the most accurate thing to do would be to adjust the volume in the code that creates the tones, rather than relying on something else which is running in a separate thread, with a separate clock and associated arbitrary delays to turn it on and off.

Since you just have simple tones, you can turn them on and off by waiting for the tone to "cross-zero" before starting and stopping. This will usually work fine. Alternatively, you can multiply by a constant which is nominally one or zero and ramp the value between one and zero when you need to change turn it on and off. An easy way to ramp is to use linear interpolation:

http://blog.bjornroche.com/2010/10/linear-interpolation-for-audio-in-c-c.html

Bjorn Roche
  • 11,279
  • 6
  • 36
  • 58
0

You have to adjust the generator. You have there a sequence of milliseconds, that represent on/off switch, right? Each time you sample you have to know which state the sound is right now, on or off. Given you sample for 44100 Hz, you can precisely adjust when should your sound stop and when should it start, by sending 0.0 instead of a sine wave into channels while the sound is stopped by your instructions. In fact the logical Sound structure will continuously play, but the tone will be intermittent.

The following is a sketch of how it could be done:

public class CustomTone {
    private var tone:Sound;
    private var delays:Vector.<Number>;
    private var frequency:Number;
    private var onThreshold:Number;
    private var offThreshold:Number;
    private var finishThreshold:Number;
    private var isOn:Boolean=true;
    private var sequenced:Number; // how many samples were sequenced total. 
    // any check is done within this.
    public function CustomTone(freq:Number,toneArray:Array) {
        tone=new Sound();
        delays=Vector.<Number>(toneArray);
        frequency=freq;
        tone.addEventListener(SampleDataEvent.SAMPLE_DATA,generateTone); 
        tone.addEventListener(Event.COMPLETE,finishedGenerating);
        sequenced=0;
        // fill other values according to array [TODO]
    }
    // other function to taste. Should have there a play and stop functions, as well as to play presampled sound
    }
    private function generateTone(e:SampleDataEvent):void {
        var ep:Number=e.position;
        var howMany:int=Math.min(finishThreshold-sequenced,8192);
        for (var i:int=0;i<howMany;i++) {
            if (isOn) { 
                sample = Math.sin((i+ep) * TWO_PI_OVER_SR * frequency);
                e.data.writeFloat(sample * amp_multiplier_left);
                sample = Math.sin((i+ep) * TWO_PI_OVER_SR * frequency);
                e.data.writeFloat(sample * amp_multiplier_right);
            } else {
                e.data.writeFloat(0.0);
                e.data.writeFloat(0.0);
            }
            if ((i+ep)>offThreshold) {
                isOn=false;
                offThreshold=getNextOffThreshold();
            }
            if (i+ep>onThreshold) {
                isOn=true;
                onThreshold=getNextOnThreshold();
            }
        }
        sequenced+=howMany;
    }
    ...
}

Note, you might want such a sound not be generated again from the same array, so you could use a method to replay the once-generated sound. Methods of next threshold should be replaced by inline calculations of next value, note these are measured in samples, not in milliseconds, thus you should convert them into samples as well.

Vesper
  • 18,599
  • 6
  • 39
  • 61