views:

4312

answers:

2

I have the code

`if(_snd !=null)

{

_channel.stop();

}

_snd = new SoundBeep();

_channel = new SoundChannel();

_channel = _snd.play();`

but if the user clicks a few times fast, flash waits a bit, then beeps them fast. I'm not sure what's going wrong. Any ideas?

A: 

try this:

if(_channel != null)
{
 _channel.stop();
}

_snd = new SoundBeep();
_channel = _snd.play();
Tyler Egeto
+1  A: 

I had this problem - it was maddening! In other languages (java, C#) I would have used synchronization around a critical section. In Flash I had to fake it. It is not perfect - still a small chance that the second sound will play over the first, but it is very small.

Here is the key: implement a combination of the factory and singleton patterns.

One important discovery I made was that I need to copy my member variable into a temporary variable before I act on it, in case some other call to the object tries to swap in a new Sound before I am finished cleaning up the previous one. Here is a snippet that does that:

 /** Seek into the audio to the given position in milliseconds. */
 public function seek(position:Number, resumePlay:Boolean) {
  trace("--> Seek(" + position + ") " + name);

  var tempAudioChannel:SoundChannel = audioChannel;
  audioChannel = null;
  if (tempAudioChannel != null) {
   tempAudioChannel.stop();
   tempAudioChannel.removeEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
  }
  audioLastPosition = position;

  if (resumePlay) {
   tempAudioChannel = audio.play(audioLastPosition);
   tempAudioChannel.addEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
   audioChannel = tempAudioChannel;
  }     
 }

Note how I not only copy from audioChannel to tempAudioChannel, but I have to null out audioChannel until the method is nearly complete, then set its value again. I am safe from synchronization errors during all but the short period between when I test the value of audioChannel and when I have finished the copying and nulling, which should be exceedingly brief.

Back to my comment about the factory and singleton patterns. While one sound is being cleaned up - stopped and discarded - another may be getting started, so I need more than one SoundManager. Thus I create one SoundManager per audio filename. Every time I need to act on an an audio file, the SoundManager class tells me which manager instance to call, so I won't hear duplicates of the same sound started as a result.

The following code is a complete SoundManager from my current commercial project. It references one other class not shown - Configuration, but that class only handles getting paths to audio files from a config file, and getting amplification values from another XML file. (The client supplied files which were not normalized to the same volume level, so we had to do it for them.) So you would have to edit all references to the config object out of this code.

NOTE: I make use of the temporary variable trick ALL OVER THE PLACE, on at least two member variables, audio and audioChannel.

package com.vpg.rns.audio {
    import flash.system.System;
    import flash.events.*;
    import flash.media.Sound;
    import flash.media.SoundChannel;
    import flash.media.SoundTransform;
    import flash.net.NetStream;
    import flash.net.NetConnection;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.errors.IOError;



    /** Load a Sound from an MP3 file, then hold onto it and mediate playing and disposing of the object, and keep the several Sounds from interfering with one another. 
     *  This class stores each SoundManager instance in a collection keyed by the step name. 
     *
     *  Do not call the Constructor directly to acquire a SoundManager. Instead, use getInstance.
     */
    public class SoundManager {
     private static var allSounds:Object = new Object();

     public static function getInstance(name:String, config:Configuration) {
      var soundMgr:SoundManager = allSounds[name];
      if (soundMgr == null) {
       soundMgr = new SoundManager(name, config);
       addSoundManager(soundMgr);
      }
      return soundMgr;
     }

     private static function addSoundManager(soundMgr:SoundManager) {
      allSounds[soundMgr.name] = soundMgr;
     }

     private static function removeSoundManager(name) {
      var soundMgr:SoundManager = allSounds[name] as SoundManager;
      allSounds[name] = null;
      if (soundMgr != null) {
       soundMgr.dispose();
      }
     }

     public static function removeAllManagers() {
      var allSoundsTemp:Object = allSounds;
      allSounds = new Object();
      for (var prop:String in allSoundsTemp){
       var soundMgr:SoundManager = allSoundsTemp[prop] as SoundManager;
       if (soundMgr != null) {
        soundMgr.dispose();
       }
      }
     }

     public static function stopManagers(exceptMgrName:String) {
      for (var prop:String in allSounds){
       var soundMgr:SoundManager = allSounds[prop] as SoundManager;
       if (soundMgr != null && soundMgr.name != exceptMgrName) {
        soundMgr.stop();
       }
      }
     }

     private var mgrName:String;
     public function get name():String { return mgrName; }

     public var config:Configuration;
     public var audio:Sound;
     public var audioChannel:SoundChannel;
     public var audioLoadStatus:String; // States: no audio, loading, loaded, ready. "loaded" means loaded enough to start playing, but possibly still loading more.

     private var rootPath:String;
     private var dataPath:String;
     private var mediaPath:String;
     public var audioFilename:String;
     private var onLoadHandler:Function; // Called When loading file is complete
     private var onAudioCompleteHandler:Function; // Called When playing audio is complete

     public var duration:Number;
     public var audioLastPosition:Number;

     private var volumeAdjustment:Number;

     /** Construct a SoundManager. Do not call this directy. Use the factory method getInstance instead. */
     function SoundManager(name:String, config:Configuration) {
      mgrName = name;
      this.config = config;
      audioLoadStatus = "no audio";
      duration = 0;
      audioLastPosition = 0;
      volumeAdjustment = 1;
     }

     /*
      * Load the audio, then tigger the loading of the optional cue point xml file, and initialization of the controls.
      *
      * @param rootDirectory ...... Directory containing the config file. 
      * @param dataDirectory ...... Directory where cuepoint data is located. Expect the cuepoints file to be in the xml/cuepoints subdirectory. 
      * @param mediaDirectory ..... Directory where audio files are located. 
      * @param audioFile .......... Name of audio file with extension. Does not include path.
      * @param onLoadHandler ...... Called once the audio is loaded, so the caller can start playing it.
      */
     public function loadAudio(rootDirectory:String, dataDirectory:String, mediaDirectory:String, audioFile:String, onLoadHandler:Function, onAudioCompleteHandler:Function) {
      audioLoadStatus = "loading";
      //Load the audio file.
      this.rootPath = rootDirectory;
      this.dataPath = dataDirectory;
      this.mediaPath = mediaDirectory;
      this.audioFilename = audioFile;
      this.onLoadHandler = onLoadHandler;
      this.onAudioCompleteHandler = onAudioCompleteHandler;
      this.volumeAdjustment = config.getAmplification(this.audioFilename);

      var mySoundReq:URLRequest = new URLRequest(config.osSpecificPath(mediaPath + "/" + audioFilename));

      audio = new Sound();
      audio.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent):void{ trace("SoundLoader.loadAudio ERROR!!!"); trace(e); });
      if (config.platform == "Flash_IDE") {
       audio.addEventListener(Event.COMPLETE, audioReady);
      }
      else {
       // We can't afford to wait for whole audio to load, so wait until some of it is loaded.
       audio.addEventListener(ProgressEvent.PROGRESS, audioProgress1);
       audio.addEventListener(Event.COMPLETE, audioCompletelyLoaded);
      }
      audio.load(mySoundReq);

     }  

     // A sufficient portion of the audio has loaded, so start playing.
     private function audioProgress1(evt:ProgressEvent) {
      var loadPercent:Number = Math.round(100 * evt.bytesLoaded / evt.bytesTotal);
      if (loadPercent > 10 && audioLoadStatus == "loading") { //TODO: Deduce a better threshold.
       var audioTemp:Sound = audio;
       audioTemp.removeEventListener(ProgressEvent.PROGRESS, audioProgress1);
       audioTemp.addEventListener(ProgressEvent.PROGRESS, audioProgress2);
       audioLoaded();
      }
     }  

     // As the audio continues to load, the duration lengthens, affecting the scrubber thumb position.
     private function audioProgress2(evt:ProgressEvent) {
      var loadPercent:Number = Math.round(100 * evt.bytesLoaded / evt.bytesTotal);
      if (audioLoadStatus == "loading" || audioLoadStatus == "loaded") {
       var audioTemp:Sound = audio;
       if (audioTemp != null) {
        duration = audioTemp.length / 1000; // Convert from milliseconds to seconds.
       }
      }
     }  

     private function audioCompletelyLoaded(evt:Event) {
      var audioTemp:Sound = audio;
      if (audioTemp != null) {
       audioTemp.removeEventListener(Event.COMPLETE, audioCompletelyLoaded);
       audioTemp.removeEventListener(ProgressEvent.PROGRESS, audioProgress1);
       audioTemp.removeEventListener(ProgressEvent.PROGRESS, audioProgress2);
       duration = audioTemp.length / 1000;
      }
     }

     private function audioReady(evt:Event) {
      var audioTemp:Sound = audio;
      if (audioTemp != null) {
       audioTemp.removeEventListener(Event.COMPLETE, audioReady);
       audioLoaded();
      }
     }

     private function audioLoaded() {
      audioLoadStatus = "loaded";

      var audioTemp:Sound = audio;
      if (audioTemp != null) {
       duration = audioTemp.length / 1000; // Convert from milliseconds to seconds.
       var audioChannelTemp:SoundChannel;
       audioChannelTemp = audioTemp.play();
       audioChannelTemp.stop();
       audioChannel = null;
       audioLastPosition = 0;
       audioLoadStatus = "ready";
       onLoadHandler(this);
      }
     }

     public function play() {
      pause();
      trace("--> Play " + name);
      audioChannel = audio.play(audioLastPosition);
      audioChannel.addEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
     }

     /** Seek into the audio to the given position in milliseconds. */
     public function seek(position:Number, resumePlay:Boolean) {
      trace("--> Seek(" + position + ") " + name);

      var tempAudioChannel:SoundChannel = audioChannel;
      audioChannel = null;
      if (tempAudioChannel != null) {
       tempAudioChannel.stop();
       tempAudioChannel.removeEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
      }
      audioLastPosition = position;

      if (resumePlay) {
       tempAudioChannel = audio.play(audioLastPosition);
       tempAudioChannel.addEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
       audioChannel = tempAudioChannel;
      }     
     }

     public function pause() {
      trace("--> Pause " + name);
      if (audioChannel != null) {
       audioLastPosition = audioChannel.position;
       audioChannel.stop();
       audioChannel.removeEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
       audioChannel = null;
      }
     }

     public function stop() {
      trace("--> Stop " + name);
      audioLastPosition = 0;
      if (audioChannel != null) {
       audioChannel.stop();
       audioChannel.removeEventListener(Event.SOUND_COMPLETE, onAudioCompleteHandler);
       audioChannel = null;
      }
     }  

     /** Elapsed time of audio in seconds. */
     public function get audioElapsed():Number {
      if (audioLoadStatus == "ready") {
       if (audioChannel != null) {
        return audioChannel.position / 1000.0;
       }
       else {
        return audioLastPosition / 1000.0;
       }
      }
      else {
       return 0;
      }
     }    

     /** Set the audio volume to a number between zero (mute) and one (loud). */
     public function setVolume(volume:Number, soundTransform:SoundTransform = null) {
      if (audioChannel != null) {
       if (soundTransform == null) {
        soundTransform = new SoundTransform();
       }
       if (volumeAdjustment != 1.0) {
        trace("setVolume using volume adjustment of " + volumeAdjustment);
       }
       soundTransform.volume = volume * volumeAdjustment;
       audioChannel.soundTransform = soundTransform;
      }
     }

     public function unloadAudio() {
      dispose();
     }    

     private function dispose() {
      audioLoadStatus = "no audio";
      var audioTemp:Sound = audio;
      audio = null;
      stop();
      if (audioTemp != null) {
                try {
                    audioTemp.close();
                }
                catch (error:IOError) {
                    trace("Error: Couldn't close audio stream: " + error.message);    
                }

      }
     }

    }

}
Paul Chernoch