Copyright © 2006-2019 MultiMedia Soft

How to synchronize the container application with the API

Previous pageReturn to chapter overviewNext page

In order to allow managing feedback coming from the API code to the container application code, Audio Sound Recorder API for .NET gives the possibility to setup a certain number of callback delegates that inform about percentage of advancement of a lengthy operation or about manual operations performed by the user on elements of the user interface like the waveform analyzer.

 

Some of these callback delegates are invoked from the application's main thread while others are invoked from secondary threads. Let's see the difference between these two categories of callback delegates:

 

Callback delegates invoked from the application's main thread

Callback delegates invoked from secondary threads

 

 

Important note about COM interoperability with Microsoft Visual Basic 6

 

For applications developed using Visual Basic 6, which results totally unreliable when dealing with delegates and callbacks in general, a set of COM compatible events is provided: see the Events for Visual Basic 6 COM interoperability section for further details.

 

 

 

 

Callback delegates invoked from the application's main thread

 

A list of situations that notify the container application through delegates can be found on the table below:

 

Situations

Corresponding callback delegates

Method for initializing the delegates

 

 

 

A method involving the usage of a recorder has been invoked, for example

StartFromAsioDevice

StartFromDirectSoundDevice

StartFromWasapiCaptureDevice

StartFromWasapiLoopbackDevice

Pause

Resume

Stop

... and many others

CallbackForRecorderEvents

CallbackForRecorderEventsSet

The range of sound displayed on the waveform analyzer is modified after a zooming operation or after a mouse scrolling operation or after a call to the WaveformAnalyzer.SetDisplayRange method.

CallbackWaveformAnalyzerRange

CallbackWaveformAnalyzerRangeSet

A portion of the waveform on the waveform analyzer has been selected/deselected through a mouse operation or through a call to the WaveformAnalyzer.SetSelection method.

CallbackWaveformAnalyzerSelection

CallbackWaveformAnalyzerSelectionSet

The waveform analyzer has been resized horizontally, usually after a call to the WaveformAnalyzer.Move method.

CallbackWaveformAnalyzerWidth

CallbackWaveformAnalyzerWidthSet

A custom vertical line on the waveform analyzer has been moved through the mouse or through the WaveformAnalyzer.GraphicItemHorzPositionSet method.

CallbackWaveformAnalyzerLineMoved

CallbackWaveformAnalyzerLineMovedSet

A custom horizontal line on the waveform analyzer has been moved through the mouse or through the WaveformAnalyzer.GraphicItemHorzPositionSet method or through the WaveformAnalyzer.GraphicItemVertPositionSet method..

CallbackWaveformAnalyzerHorzLineMoved

CallbackWaveformAnalyzerHorzLineMovedSet

The playback position of the sound under editing reaches a custom vertical line inside the waveform analyzer.

CallbackWaveformAnalyzerLineReached

CallbackWaveformAnalyzerLineReachedSet

The playback position of the sound under editing reaches the beginning of a custom horizontal line inside the waveform analyzer.

CallbackWaveformAnalyzerHorzLineReached

CallbackWaveformAnalyzerHorzLineReachedSet

The playback position of the sound under editing leaves the end of a custom horizontal line inside the waveform analyzer.

CallbackWaveformAnalyzerHorzLineLeaved

CallbackWaveformAnalyzerHorzLineLeavedSet

The playback position of the sound under editing reaches the beginning of a custom wave range inside the waveform analyzer.

WaveAnalyzerWaveRangeReached

WaveAnalyzerWaveRangeReachedSet

The playback position of the sound under editing leaves the end of a custom wave range inside the waveform analyzer.

WaveAnalyzerWaveRangeLeaved

WaveAnalyzerWaveRangeLeavedSet

A graphic item is clicked with the mouse on the waveform analyzer

CallbackWaveformAnalyzerGraphItemClick

CallbackWaveformAnalyzerGraphItemClickSet

A graphic item is double-clicked with the mouse on the waveform analyzer

CallbackWaveformAnalyzerGraphItemDblClick

CallbackWaveformAnalyzerGraphItemDblClickSet

A mouse event happens on the waveform analyzer.

CallbackWaveformAnalyzerMouseNotif

CallbackWaveformAnalyzerMouseNotifSet

The waveform analyzer has completed its graphic rendering and it's now possible adding custom graphics directly from the container application's code.

CallbackWaveformAnalyzerPaintDone

CallbackWaveformAnalyzerPaintDoneSet

A manual scroll happens on the waveform scroller.

CallbackWaveformScrollerManualScroll

CallbackWaveformScrollerManualScrollSet

A mouse event happens on the waveform scroller.

CallbackWaveformScrollerMouseNotif

CallbackWaveformScrollerMouseNotifSet

During playback there is a change on the VU-Meter peak values.

CallbackVuMeterValueChange

CallbackVuMeterValueChangeSet

During playback there is a change on the waveform values.

CallbackWaveformValueChange

CallbackWaveformValueChangeSet

During playback the status of the player changes

CallbackSoundPlaybackStatusChanged

CallbackSoundPlaybackStatusChangedSet

On WIndows XP, the system default multimedia input/output device (sound card) is changed through the Windows Control Panel's multimedia settings or USB device is plugged or unplugged

CallBackDeviceChange

CallBackDeviceChangeSet

 

Let's see a couple of code snippets that clarify how a callback delegate can be instantiated and managed. The snippets below show how to start a recording session from the system default recording device and display the sound duration and size on corresponding labels.

 

Visual Basic .NET

 

Imports AudioSoundRecorderApi

 

Namespace MyApplication

   Public Partial Class Form1

       Inherits Form

 

       Public Sub New()

           InitializeComponent()

       End Sub

 

      ' instance of the API

       Private m_audioRecorderAPI As AudioSoundRecorderApi.AudioSoundRecorderApiObj = New AudioSoundRecorderApi.AudioSoundRecorderApiObj()

 

      ' callback delegate

       Private addrRecorderCallback As CallbackForRecorderEvents = New CallbackForRecorderEvents(AddressOf RecorderCallback)

 

      ' callback that manages recorde's events

       Private Sub RecorderCallback(ByVal nEvent As enumRecorderEvents, ByVal nData1 As Int32, ByVal nData2 As Int32, _

                            ByVal nDataHigh3 As Int32, ByVal nDataLow3 As Int32, _

                            ByVal nDataHigh4 As Int32, ByVal nDataLow4 As Int32)

           Select Case nEvent

               Case enumRecorderEvents.EV_REC_START

                   Console.WriteLine ("Recording started")

               Case enumRecorderEvents.EV_REC_STOP

                   Console.WriteLine ("Recording stopped")

               Case enumRecorderEvents.EV_REC_SIZE

                   Dim nDataSize As Int64 = (Convert.ToInt64 (nDataHigh4) << 32) Or Convert.ToInt64 (nDataLow4)

                   labelSize.Text = "Recording size in bytes: " & nDataSize.ToString()

               Case enumRecorderEvents.EV_REC_DURATION

                   labelDuration.Text = "Recording duration: " & m_audioRecorderAPI.FromMsToFormattedTime (nData1, False, True, ":", ".", 3)

               Case Else

                   Return

           End Select          

 

          ... do something else

       End Sub

 

       Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load

          ' initialize the API

           m_audioRecorderAPI.InitSoundSystem(1, 0, 0, 0, 0)

 

          ' predispose the callback that will notify about recorder's events

           m_audioRecorderAPI.CallbackForRecorderEventsSet(addrRecorderCallback)

 

          ' use custom resampling format 44100 Hz Stereo and record in MP3 format

           m_audioRecorderAPI.EncodeFormats.ResampleMode = enumResampleModes.RESAMPLE_MODE_CUSTOM_FORMAT

           m_audioRecorderAPI.EncodeFormats.ResampleCustomFrequency = 44100

           m_audioRecorderAPI.EncodeFormats.ResampleCustomChannels = 2

           m_audioRecorderAPI.EncodeFormats.ForRecording == enumEncodingFormats.ENCODING_FORMAT_MP3

 

          ' start a recording session from the system default recording device

           m_audioRecorderAPI.StartFromDirectSoundDevice (0, 0, "c:\myrecording.mp3")

 

          ... do other stuffs

 

       End Sub

   End Class

End Namespace

 

 

Visual C#

 

using AudioSoundRecorderApi;

 

namespace MyApplication

{

   public partial class Form1 : Form

   {

       public Form1()

       {

           InitializeComponent();

       }

 

      // instance of the API

       static AudioSoundRecorderApi.AudioSoundRecorderApiObj m_audioRecorderAPI = new AudioSoundRecorderApi.AudioSoundRecorderApiObj();

 

      // callback delegate

       CallbackForRecorderEvents addrRecorderCallback;

 

      // callback that manages recorder's events

       void RecorderCallback(enumRecorderEvents nEvent, Int32 nData1, Int32 nData2, Int32 nDataHigh3, Int32 nDataLow3, Int32 nDataHigh4, Int32 nDataLow4)

       {

           switch (nEvent)

           {

           case enumRecorderEvents.EV_REC_START:

               Console.WriteLine ("Recording started");

               break;

           case enumRecorderEvents.EV_REC_STOP:

               Console.WriteLine ("Recording stopped");

               break;

           case enumRecorderEvents.EV_REC_SIZE:

               Int64 nDataSize = (Convert.ToInt64 (nDataHigh4) << 32) | Convert.ToInt64 (nDataLow4);

               labelSize.Text = "Recording size in bytes: " + nDataSize.ToString();

               break;

           case enumRecorderEvents.EV_REC_DURATION:

               labelDuration.Text = "Recording duration: " + m_audioRecorderAPI.FromMsToFormattedTime (nData1, false, true, ":", ".", 3);

               break;

           }

       }

 

       private void Form1_Load(object sender, EventArgs e)

       {

          // initialize the API

           m_audioRecorderAPI.InitRecordingSystem();

 

          // predispose the callback that will notify about recorder's events

           addrRecorderCallback = new CallbackForRecorderEvents(RecorderCallback);

           m_audioRecorderAPI.CallbackForRecorderEventsSet(addrRecorderCallback);

         

          // use custom resampling format 44100 Hz Stereo and record in MP3 format

           m_audioRecorderAPI.EncodeFormats.ResampleMode = enumResampleModes.RESAMPLE_MODE_CUSTOM_FORMAT;

           m_audioRecorderAPI.EncodeFormats.ResampleCustomFrequency = 44100;

           m_audioRecorderAPI.EncodeFormats.ResampleCustomChannels = 2;

           m_audioRecorderAPI.EncodeFormats.ForRecording == enumEncodingFormats.ENCODING_FORMAT_MP3;

 

          // start a recording session from the system default recording device

           m_audioRecorderAPI.StartFromDirectSoundDevice (0, 0, @"c:\myrecording.mp3");

 

          ... do other stuffs

       }

      ...

   }

  ...

}

 

 

 

 

Callback delegates invoked from secondary threads

 

Some method available inside Audio Sound Editor API for .NET performs lengthy operations which, on a single-threaded environment, could block the user interface of the container application also for several seconds: just think about the time requested to load a sound file whose duration is more than 30 minutes or to perform the waveform analysis for a song longer than 5 minutes and you will perfectly understand that this kind of tasks couldn't be completed in less than one second.

 

In order to avoid this kind of blocks, the API performs lengthy operations inside secondary threads executed in synchronous mode: this means that the call to a method starting a new thread will not return control to the container application till the moment in which the secondary thread has not completed its task and has been closed.

 

During execution of the secondary thread, percentage of advancement of the thread's task is reported to the container application through a set of callback delegates. A list of methods that will start a secondary thread, with the corresponding callback delegate, can be found on the table below:

 

Situations

Corresponding callback delegates

Method for initializing the delegates

 

 

 

A method involving the usage of a recorder has been invoked, for example

StartFromFile

StartFromMemory

... and many others

CallbackForRecorderEvents

CallbackForRecorderEventsSet

Notifications from CoreAudio devices

CallbackForCoreAudioEvents

CallbackForCoreAudioEventsSet

A new spectral analysis is requested

CallbackWaveAnalyzerSpectralViewStart

CallbackWaveAnalyzerSpectralViewDone

CallbackWaveAnalyzerSpectralViewStartSet

CallbackWaveAnalyzerSpectralViewDoneSet

A playback session is completed

CallbackSoundPlaybackDone

CallbackSoundPlaybackDoneSet

 

Let's see a couple of code snippets that clarify how a callback delegate invoked from a secondary thread can be instantiated and managed.

The snippets below show how to start a recording session from a sound file stored locally and immediately proceed with the sound's waveform analysis: during the loading and the waveform analysis the recording percentage is displayed on a label and on progress bar.

 

Visual Basic .NET

 

Imports AudioSoundRecorderApi

 

Namespace MyApplication

   Public Partial Class Form1

       Inherits Form

 

       Public Sub New()

           InitializeComponent()

       End Sub

 

      ' instance of the API

       Private m_audioRecorderAPI As AudioSoundRecorderApi.AudioSoundRecorderApiObj = New AudioSoundRecorderApi.AudioSoundRecorderApiObj()

 

      ' delegate for managing callbacks invoked from secondary threads

       Private Delegate Sub RecorderCallbackDelegate(ByVal nEvent As enumRecorderEvents, _

                               ByVal nData1 As Int32, ByVal nData2 As Int32, _

                               ByVal nDataHigh3 As Int32, ByVal nDataLow3 As Int32, _

                               ByVal nDataHigh4 As Int32, ByVal nDataLow4 As Int32)

 

      ' callback delegate

       Private addrRecorderCallback As CallbackForRecorderEvents = New CallbackForRecorderEvents(AddressOf RecorderCallback)

 

      ' callback that manages recorde's events

       Private Sub RecorderCallback(ByVal nEvent As enumRecorderEvents, ByVal nData1 As Int32, ByVal nData2 As Int32, _

                            ByVal nDataHigh3 As Int32, ByVal nDataLow3 As Int32, _

                            ByVal nDataHigh4 As Int32, ByVal nDataLow4 As Int32)

 

          ' check if we are being invoked from a secondary thread

           If Me.InvokeRequired Then

              ' this callback is being called from a secondary thread so, in order to access controls on the form, we must

               ' call Invoke using this same function as a delegate

               Me.Invoke(New RecorderCallbackDelegate(AddressOf RecorderCallback), nEvent, nData1, nData2, _

                                                      nDataHigh3, nDataLow3, nDataHigh4, nDataLow4)

               Return

           End If

 

           Select Case nEvent

               Case enumRecorderEvents.EV_REC_PERC:

                   strStatus = "Status: Loading sound file... " & nData1.ToString() & "%"

               Case enumRecorderEvents.EV_REC_WAVE_ANALYSIS_PERC:

                   strStatus = "Status: Analyzing waveform... " & nData1.ToString() & "%"

               Case Else

                   Return

           End Select

 

          ' safely update elements of the user interface

           LabelStatus.Text = strStatus

           progressBar1.Value = nData1

       End Sub

 

       Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load

          ' initialize the API

           m_audioRecorderAPI.InitSoundSystem(1, 0, 0, 0, 0)

 

          ' predispose the callback that will notify about recorder's events

           m_audioRecorderAPI.CallbackForRecorderEventsSet(addrRecorderCallback)

 

           progressBar1.Visible = False

 

          ' load the new song

           m_audioRecorderAPI.StartFromFile("", "c:\mysound.mp3")

 

           LabelStatus.Text = "Status: Idle"

           progressBar1.Visible = False

 

          ' request full waveform analysis using default resolution

           progressBar1.Visible = True

           m_audioRecorderAPI.DisplayWaveformAnalyzer.AnalyzeFullSound()

 

           LabelStatus.Text = "Status: Idle"

           progressBar1.Visible = False

 

          ' display the full waveform

           m_audioRecorderAPI.DisplayWaveformAnalyzer.SetDisplayRange(0, -1)

 

       End Sub

   End Class

End Namespace

 

 

Visual C#

 

using AudioSoundRecorderApi;

 

namespace MyApplication

{

   public partial class Form1 : Form

   {

       public Form1()

       {

           InitializeComponent();

       }

 

      // instance of the API

       static AudioSoundRecorderApi.AudioSoundRecorderApiObj m_audioRecorderAPI = new AudioSoundRecorderApi.AudioSoundRecorderApiObj();

 

 

      // delegate for managing callbacks invoked from secondary threads

       delegate void RecorderCallbackDelegate(enumRecorderEvents nEvent, Int32 nData1, Int32 nData2,

                                               Int32 nDataHigh3, Int32 nDataLow3,

                                               Int32 nDataHigh4, Int32 nDataLow4);      

 

      // callback delegate

       CallbackForRecorderEvents addrRecorderCallback;

 

      // callback that manages recorder's events

       void RecorderCallback(enumRecorderEvents nEvent, Int32 nData1, Int32 nData2,

                             Int32 nDataHigh3, Int32 nDataLow3, Int32 nDataHigh4, Int32 nDataLow4)

       {

          // check if we are being invoked from a secondary thread

           if (this.InvokeRequired)

           {

               // this callback is being called from a secondary thread so, in order to access controls on the form, we must

               // call Invoke using this same function as a delegate

               this.Invoke(new RecorderCallbackDelegate(RecorderCallback), nEvent, nData1, nData2, nDataHigh3, nDataLow3, nDataHigh4, nDataLow4);

               return;

           }

           switch (nEvent)

           {

           case enumRecorderEvents.EV_REC_PERC:

               strStatus = "Status: Loading sound file... " + nData1.ToString() + "%";

               break;

           case enumRecorderEvents.EV_REC_WAVE_ANALYSIS_PERC:

               strStatus = "Status: Analyzing waveform... " + nData1.ToString() + "%";

               break;

           }

 

          // safely update elements of the user interface

           LabelStatus.Text = strStatus;

           progressBar1.Value = nData1;

       }

 

       private void Form1_Load(object sender, EventArgs e)

       {

          // initialize the API

           m_audioRecorderAPI.InitRecordingSystem();

 

          // predispose the callback that will notify about recorder's events

           addrRecorderCallback = new CallbackForRecorderEvents(RecorderCallback);

           m_audioRecorderAPI.CallbackForRecorderEventsSet(addrRecorderCallback);

         

           progressBar1.Visible = true;

 

           // load the new song

           m_audioRecorderAPI.StartFromFile("", @"c:\mysound.mp3");

 

           LabelStatus.Text = "Status: Idle";

           progressBar1.Visible = false;

 

           // request full waveform analysis using default resolution

           progressBar1.Visible = true;

           m_audioRecorderAPI.DisplayWaveformAnalyzer.AnalyzeFullSound();

 

           LabelStatus.Text = "Status: Idle";

           progressBar1.Visible = false;

 

          // display the full waveform

           m_audioRecorderAPI.DisplayWaveformAnalyzer.SetDisplayRange(0, -1);

       }

      ...

   }

  ...

}

 

 

 

 

 

IMPORTANT NOTE ABOUT DELEGATES AND THEIR SCOPE: When an instance of a callback delegate is passed to one of the API functions, the delegate object is not reference counted. This means that the .NET framework would not know that it might still being used by the API so the Garbage Collector might remove the delegate instance if the variable holding the delegate is not declared as global. As a general rule, make sure to always keep your delegate instance in a variable which lives as long as the API needs it by using a global variable or member.

 

 

IMPORTANT NOTES ABOUT BEST PRACTICES

Always keep management functions of events generated by the component as fast/short as possible and never use them in order to display messages or dialog boxes which require user interaction.

 

It's very important to remember that a call to a method of a specific component should be never performed from within a management function of an event generated by the same component: this is usually cause of errors and dead-lock situations and it's a practice that should be always avoided; in order to avoid issues the best approach would be using the callback to trigger a one-shot timer and to call methods for requesting new operations from within the management function of the timer's event.

 

 

 

How to create and use one-shot timers in C#

 

The procedure below allows using a single one-shot timer that could manage some of the situations described above:

 

At design-time, from the Visual Studio toolbox, insert a Timer object inside the form containing the Audio Sound Recorder component: inside this sample it will be named TimerSync
At design-time, set the Enabled property of the timer object to False
At design-time, set the Interval property of the timer object to 50
Inside your code add a global variable of type "String" that you could use for knowing the next operation that needs to be performed immediately after receiving the timer's tick: inside this sample it will be named g_strNextOperation

 

Supposing that we have have stopped a recording session and that we need to start the waveform analysis, the code below could not work correctly:

 

Visual C#

 

using AudioSoundRecorderApi;

 

namespace MyApplication

{

   public partial class Form1 : Form

   {

      ...

 

      // callback that manages recorder's events

       void RecorderCallback(enumRecorderEvents nEvent, Int32 nData1, Int32 nData2, Int32 nDataHigh3, Int32 nDataLow3, Int32 nDataHigh4, Int32 nDataLow4)

       {

           switch (nEvent)

           {

           case enumRecorderEvents.EV_REC_START:

               Console.WriteLine ("Recording started");

               break;

           case enumRecorderEvents.EV_REC_STOP:

               Console.WriteLine ("Recording stopped");

               m_audioRecorderAPI.DisplayWaveformAnalyzer.AnalyzeFullSound (); // <-- this call could fail and the application could hang

               break;

       }

      ...

 

   }

  ...

}

 

 

and you should prefer using something like this

 

Visual C#

 

using AudioSoundRecorderApi;

 

namespace MyApplication

{

   public partial class Form1 : Form

   {

      ...

 

      // callback that manages recorder's events

       void RecorderCallback(enumRecorderEvents nEvent, Int32 nData1, Int32 nData2, Int32 nDataHigh3, Int32 nDataLow3, Int32 nDataHigh4, Int32 nDataLow4)

       {

           switch (nEvent)

           {

           case enumRecorderEvents.EV_REC_START:

               Console.WriteLine ("Recording started");

               break;

           case enumRecorderEvents.EV_REC_STOP:

               Console.WriteLine ("Recording stopped");

               g_strNextOperation = "AnalyzeSound";

               TimerSync.Enabled = true;

               break;

       }

      ...

 

   }

  ...

}

 

 

Now let's catch the timer's tick and perform requested operation

 

Visual C#

 

private void TimerSync_Tick(object sender, System.EventArgs e)

{

   // reset the timer to avoid unwanted recursions

  TimerSync.Enabled = false;

 

  // start requested operation

  if (g_strNextOperation == "AnalyzeSound")

      m_audioRecorderAPI.DisplayWaveformAnalyzer.AnalyzeFullSound ();

}

 

 

As you may understand, you could expand the timer's handler above for all operations executed inside secondary threads by adding new values for the g_strNextOperation variable.

 

Visual C#

 

private void TimerSync_Tick(object sender, System.EventArgs e)

{

   // reset the timer to avoid unwanted recursions

  TimerSync.Enabled = false;

   

  // start requested operation

switch (g_strNextOperation)

  {

  case "AnalyzeSound":

       audioSoundRecorder1.DisplayWaveformAnalyzer.AnalyzeFullSound ();

      break;

  case "CreateWaveformBitmap":

       audioSoundRecorder1.DisplayWaveformAnalyzer.SnapshotViewSaveToFile (0, -1, etc.);

      break;

  }

}