How to synchronize the container application with the API |
|
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 ... and many others |
||
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. |
||
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. |
||
The waveform analyzer has been resized horizontally, usually after a call to the WaveformAnalyzer.Move method. |
||
A custom vertical line on the waveform analyzer has been moved through the mouse or through the WaveformAnalyzer.GraphicItemHorzPositionSet method. |
||
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.. |
||
The playback position of the sound under editing reaches a custom vertical line inside the waveform analyzer. |
||
The playback position of the sound under editing reaches the beginning of a custom horizontal line inside the waveform analyzer. |
||
The playback position of the sound under editing leaves the end of a custom horizontal line inside the waveform analyzer. |
||
The playback position of the sound under editing reaches the beginning of a custom wave range inside the waveform analyzer. |
||
The playback position of the sound under editing leaves the end of a custom wave range inside the waveform analyzer. |
||
A graphic item is clicked with the mouse on the waveform analyzer |
||
A graphic item is double-clicked with the mouse on the waveform analyzer |
||
A mouse event happens on the waveform analyzer. |
||
The waveform analyzer has completed its graphic rendering and it's now possible adding custom graphics directly from the container application's code. |
||
A manual scroll happens on the waveform scroller. |
||
A mouse event happens on the waveform scroller. |
||
During playback there is a change on the VU-Meter peak values. |
||
During playback there is a change on the waveform values. |
||
During playback the status of the player changes |
||
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 |
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 ... and many others |
||
Notifications from CoreAudio devices |
||
A new spectral analysis is requested |
||
A playback session is completed |
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; } }
|