Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Silverlight 5 and NSpeex: Capture audio from capture source, encode, save to file, and decode to play

5.00/5 (3 votes)
10 Jan 2013CPOL3 min read 31.5K   870  
This article uses NSpeex as a codec to encode/decode audio from a capture source (e.g., computer microphone) within Silverlight.

Sample Image

Introduction

I had an application in mind whose requirements were to capture Audio, save it to disk and playback later, all using Silverlight. I also needed the captured audio file that is saved on disk to be as small as possible.

The possibility of accessing raw audio samples in Silverlight has been available for a while now but the preparation and writing the raw bytes to disk is the task you will have to take care of yourself. This is made possible by using the AudioCaptureDevice to connect to available Audio source (e.g., microphone, etc.) and using the AudioSink class in the System.Windows.Media namespace to access the raw samples.

Now, given that my intentions were not just to capture and gain access to the raw data, I also needed to write to file and of course compressed. I had no clue what to use, considering that I wanted the feature in Silverlight.

NSpeex (http://nspeex.codeplex.com/) came to the rescue – this was after a quick search on Code Project (see this article’s discussion section: http://www.codeproject.com/Articles/20093/Speex-in-C?msg=3660549#xx3660549xx).

Unfortunately there was not much of documentation on the website, so I had a tough time figuring out the whole encoding/decoding process.

This is how I did it and I hope that it will help others who need to work on similar application.

Preparations

  1. Access to CaptureDevices require elevated trust …
  2. Writing to disk requires you to enable running application out of browser
  3. Encoding raw samples requires you to write Audio Capture class that derives from AudioSink
  4. Decoding to raw samples requires you to write a custom Stream Source class that derives from MediaStreamSource

Our Audio Capture class must override some methods derived from AudioSink abstract class. These are OnCaptureStarted(), OnCaptureStopped(), OnFormatChange(), and OnSamples().

As for MediaStreamSource you may wish to refer to my previous article describing in summary the methods to override - http://www.codeproject.com/Articles/251874/Play-AVI-files-in-Silverlight-5-using-MediaElement

Using the code

  • Create a new Silverlight 5 Project (C#) and give it any name you wish
  • Set the Project properties as explained in my previous article - http://www.codeproject.com/Articles/251874/Play-AVI-files-in-Silverlight-5-using-MediaElement
  • Open the default created UserControl named MainPage.xaml
  • Change the XAML code to resemble the one below, which includes the following controls:
    • MediaElement: Requesting Audio samples
    • Four buttons: Start/Stop recording, Start/Stop playing audio
    • Slider: Adjusting the Audio Quality during encoding
    • TextBoxes: Displaying Record/Play Audio duration, recording limit in minutes
    • ComboBox: Listing of capture devices
    • Two DataGrids: Listing audio formats and displaying saved audio file details
XML
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="56"/>
<RowDefinition Height="344*" />
</Grid.RowDefinitions>
<MediaElement AutoPlay="True" 
   Margin="539,8,0,0" x:Name="MediaPlayer" 
   Stretch="Uniform" Height="20" Width="47"
   VerticalAlignment="Top" HorizontalAlignment="Left" 
   MediaOpened="MediaPlayer_MediaOpened" MediaEnded="MediaPlayer_MediaEnded" />
<Button Content="Start Rec" Height="23" HorizontalAlignment="Left" Margin="12,23,0,0" 
  Name="StartRecording" VerticalAlignment="Top" Width="64" Click="StartRecording_Click" />
<Button Content="Stop Rec" Height="23" HorizontalAlignment="Left" Margin="82,23,0,0" 
  Name="StopRecording" VerticalAlignment="Top" Width="59" Click="StopRecording_Click" IsEnabled="False" />
<Button Content="Start Play" Height="23" HorizontalAlignment="Right" Margin="0,23,77,0" 
  Name="StartPlaying" VerticalAlignment="Top" Width="64" Click="StartPlaying_Click" />
<Button Content="Stop" Height="23" HorizontalAlignment="Right" Margin="0,23,12,0" 
  Name="StopPlaying" VerticalAlignment="Top" Width="59" Click="StopPlaying_Click" IsEnabled="False" />
<!--<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="592,8,0,0" 
  Name="button1" VerticalAlignment="Top" Width="60" 
  Click="button1_Click" Visibility="Visible" IsEnabled="False" />-->
<Slider Name="Quality" Value="{Binding QualityValue, Mode=TwoWay}" Height="23" Width="129" 
  HorizontalAlignment="Left" VerticalAlignment="Top"
  Margin="342,23,0,0"
  Minimum="2" Maximum="7" SmallChange="1" DataContext="{Binding}" />
<TextBox Text="{Binding timeSpan, Mode=OneWay}" Margin="147,23,0,0" TextAlignment="Center" 
  HorizontalAlignment="Left" Width="85" IsEnabled="False" Height="23" VerticalAlignment="Top" />
<TextBlock Text="/" Margin="0,3,104,0" HorizontalAlignment="Right" Width="25" Height="38" 
  VerticalAlignment="Top" FontSize="24" TextAlignment="Center" 
  FontWeight="Normal" Foreground="#FF8D8787" Grid.Row="1" />
<TextBox Text="{Binding RecLimit, Mode=TwoWay}" Margin="250,23,0,0" 
  TextAlignment="Center" HorizontalAlignment="Left" Width="43" Height="23" VerticalAlignment="Top" />
<TextBox Text="{Binding QualityValue, Mode=TwoWay}" Margin="477,23,0,0" 
  TextAlignment="Center" HorizontalAlignment="Left" Width="47" 
  IsEnabled="False" Height="23" VerticalAlignment="Top" />
<TextBox Text="{Binding timeSpan, Mode=OneWay}" Margin="0,5,125,0" TextAlignment="Center" 
  IsEnabled="False" FontSize="24" FontWeight="Bold" FontFamily="Arial" BorderBrush="{x:Null}" 
  Height="33" VerticalAlignment="Top" HorizontalAlignment="Right" Width="107" 
  Grid.Row="1" Background="Black" Foreground="#FFADABAB" />
<TextBlock Text="/" Margin="229,25,0,0" HorizontalAlignment="Left" Width="25" Height="21" 
  VerticalAlignment="Top" FontSize="13" TextAlignment="Center" FontWeight="Normal" Foreground="#FF8D8787" />
<TextBox Text="{Binding PlayTime, Mode=OneWay}" Margin="0,5,4,0" TextAlignment="Center" 
  IsEnabled="False" FontSize="24" FontWeight="Bold" FontFamily="Arial" BorderBrush="{x:Null}" 
  Height="33" VerticalAlignment="Top" Grid.Row="1" HorizontalAlignment="Right" 
  Width="107" Foreground="#FFADABAB" Background="#FF101010" />
<TextBlock Text="Minutes" Margin="295,29,0,0" 
  HorizontalAlignment="Left" Width="47" Height="15" VerticalAlignment="Top" />
<TextBlock Text="Quality" Margin="340,8,0,0" TextAlignment="Center" FontWeight="Bold" 
  HorizontalAlignment="Left" Width="50" Height="15" VerticalAlignment="Top" />
<TextBlock Text="Limit" Margin="253,8,0,0" TextAlignment="Center" 
  FontWeight="Bold" HorizontalAlignment="Left" Width="36" Height="15" VerticalAlignment="Top" />
<TextBlock Text="Recorded Files" Margin="0,19,238,0" TextAlignment="Left" 
  FontWeight="Bold" HorizontalAlignment="Right" Width="126" 
  Height="15" VerticalAlignment="Top" Grid.Row="1" />
  
<ComboBox 
  x:Name="comboDeviceType" ItemsSource="{Binding AudioDevices, Mode=OneWay}" 
  DisplayMemberPath="FriendlyName" SelectedIndex="0" Height="21" VerticalAlignment="Top" 
  HorizontalAlignment="Left" Width="429" Margin="0,13,0,0" Grid.Row="1" />

<dg:DataGrid x:Name="RecordedFiles" SelectedIndex="0" 
   ItemsSource="{Binding FileItems, Mode=OneWay}" Grid.Row="1" 
   AutoGenerateColumns="False" Margin="430,40,0,0" 
   SelectionChanged="RecordedFiles_SelectionChanged">
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Width="209" Header="File Path" Binding="{Binding FilePath}" />
<dg:DataGridTextColumn Header="File Size" Binding="{Binding FileSize}" />
<dg:DataGridTextColumn Header="Duration" Binding="{Binding FileDuration}" />
</dg:DataGrid.Columns>
</dg:DataGrid>
<dg:DataGrid x:Name="SourceDetailsFormats" SelectedIndex="0" Grid.Row="1" 
   ItemsSource="{Binding ElementName=comboDeviceType,Path=SelectedValue.SupportedFormats}" 
   AutoGenerateColumns="False" HorizontalAlignment="Left" Width="429" Margin="0,40,0,0">
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Header="Wave Format" Binding="{Binding WaveFormat}" />
<dg:DataGridTextColumn Header="Channels" Binding="{Binding Channels}" />
<dg:DataGridTextColumn Header="Bits Per Sample" Binding="{Binding BitsPerSample}" />
<dg:DataGridTextColumn Header="Samples Per Second" Binding="{Binding SamplesPerSecond}" />
</dg:DataGrid.Columns>
</dg:DataGrid>
</Grid>

We now have the UI created, we definitely need to write the code behind for the UI controls to respond to.

But first, let us write the classes we need for capturing audio from a capture device and the other one for responding to MediaElement’s request for Audio Samples during playback:

  • Make sure to download the NSpeex library from http://nspeex.codeplex.com and add reference to the NSpeex.Silverlight DLL
  • Create a custom class derived from AudioSink (I named it MemoryAudioSink.cs)
  • Create a custom class derived from MediaStreamSource (I named it CustomSourcePlayer.cs)

Referencees

C#
// 
// MemoryAudioSink.cs
//

using System;
using System.Windows.Media;
using System.IO;

using NSpeex; // <--- you need this

namespace myAudio_23._12._212.Audio
{
    public class MemoryAudioSink : AudioSink //, INotifyPropertyChanged
    {
        // declaration of variables ...
        // ... see attached code ...

        protected override void OnCaptureStarted()
        {
            // set the encoder quality to the value selected 
            encoder.Quality = _quality;

            // reserve 2 bytes to hold the duration of the recorded audio
            // this is updated at the end of the recording
            writer.Write(BitConverter.GetBytes(timeSpan));

            // initialize the start time
            recTime = DateTime.Now;

            // initialize the recording duration
            _timeSpan = 0;// "00:00:00";
        }

        protected override void OnCaptureStopped()
        {
            // We now need to update the encoded file with how long the recording took
            // move the pointer to the beginning of the byte array
            writer.Seek(0, SeekOrigin.Begin);

            // now update to the duration of audio captured and close the BinaryWriter
            writer.Write(BitConverter.GetBytes(timeSpan));
            writer.Close();
        }

        protected override void OnFormatChange(AudioFormat audioFormat)
        {
            if (audioFormat.WaveFormat != WaveFormatType.Pcm)
                throw new InvalidOperationException("MemoryAudioSink supports only PCM audio format.");
        }

        protected override void OnSamples(long sampleTimeInHundredNanoseconds, 
                  long sampleDurationInHundredNanoseconds, byte[] sampleData)
        {
            // increment the number of samples captured by 1
            numSamples++;

            // New audio data arrived, so encode them.
            var encodedAudio = EncodeAudio(sampleData);

            // keep track of the duration of recording
            // the final one will be used in the OnCaptureStopped
            // NOTE: we could have calculated at the time
            //       of OnCaptureStopped but this is used by the UI code behind
            // for displaying the recording time
            _timeSpan = (Int32)(DateTime.Now.Subtract(recTime).TotalSeconds);

            try
            {
                // get the length of the encoded Audio
                encodedByteSize = (Int16)encodedAudio.Length;

                // get bytes of the size of encoded Sample and write it to file
                // before writing the bytes of the encoded Sample, we first write its size
                writer.Write(BitConverter.GetBytes(encodedByteSize));

                // now write the encoded Bytes to file
                writer.Write(encodedAudio);

            }
            catch (Exception ex)
            {
                // Do something about the exception
            }
        }
    
        public byte[] EncodeAudio(byte[] rawData)
        {
            // NSpeex encoder expects as input data contained in variable of short data type array
            // hence we devide the length by 2 because short holds 2 bytes
            var inDataSize = rawData.Length / 2;

            // create array to hold the raw data
            var inData = new short[inDataSize];

            // transfer the raw data 2 bytes at a time
            for (int index = 0; index < rawData.Length; index +=2 )
            {
                inData[index / 2] = BitConverter.ToInt16(rawData, index);
            }

            // raw data size should be multiple of the encoder frameSize i.e. 320
            inDataSize = inDataSize - inDataSize % encoder.FrameSize;

            // run the encoder
            var encodedData = new byte[rawData.Length];
            var encodedBytes = encoder.Encode(inData, 0, inDataSize, encodedData, 0, encodedData.Length);

            // declare and initialize encoded Audio data array
            byte[] encodedAudioData = null;
            if (encodedBytes != 0)
            {
                // reinitialize the array to the size of the encodedData
                encodedAudioData = new byte[encodedBytes];

                // now copy the encoded bytes to our new array
                Array.Copy(encodedData, 0, encodedAudioData, 0, encodedBytes);
            }
    
            return encodedAudioData;
        }
    }
}
CustomSourcePlayer.cs:
C#
using System;
using System.Windows.Media;
using System.Collections.Generic;
using System.IO;
using System.Globalization;

using NSpeex; // <<--- you need this

namespace myAudio_23._12._212
{
    public class CustomSourcePlayer : MediaStreamSource
    {
        // declaration of variables ...
        // ... see attached code ...

        protected override void OpenMediaAsync()
        {
            playTime = DateTime.Now;
            _timeSpan = "00:00:00";

            Dictionary<MediaSourceAttributesKeys, string> sourceAttributes = 
                        new Dictionary<MediaSourceAttributesKeys, string>();

            List<MediaStreamDescription> availableStreams = new List<MediaStreamDescription>();

            _audioDesc = PrepareAudio();

            availableStreams.Add(_audioDesc);
            sourceAttributes[MediaSourceAttributesKeys.Duration] = 
                   TimeSpan.FromSeconds(0).Ticks.ToString(CultureInfo.InvariantCulture);
            sourceAttributes[MediaSourceAttributesKeys.CanSeek] = false.ToString();
            ReportOpenMediaCompleted(sourceAttributes, availableStreams);
        }

        private MediaStreamDescription PrepareAudio()
        {
            int ByteRate = _audioSampleRate * _audioChannels * (_audioBitsPerSample / 8);

            // I think I got this WaveFormatEx Class from <a href="http://10rem.net/">http://10rem.net/</a>
            // reference: <a href="http://msdn.microsoft.com/en-us/library/aa908934.aspx">http://msdn.microsoft.com/en-us/library/aa908934.aspx</a>
            _waveFormat = new WaveFormatEx(); 

            _waveFormat.BitsPerSample = _audioBitsPerSample;
            _waveFormat.AvgBytesPerSec = (int)ByteRate;
            _waveFormat.Channels = _audioChannels;
            _waveFormat.BlockAlign = (short)(_audioChannels * (_audioBitsPerSample / 8));
            _waveFormat.ext = null;
            _waveFormat.FormatTag = WaveFormatEx.FormatPCM;
            _waveFormat.SamplesPerSec = _audioSampleRate;
            _waveFormat.Size = 0;

            // reference: <a href="http://msdn.microsoft.com/en-us/library/system.windows.media.mediastreamattributekeys(v=vs.95).aspx">http://msdn.microsoft.com/en-us/library/system.windows.media.mediastreamattributekeys(v=vs.95).aspx</a>
            Dictionary<MediaStreamAttributeKeys, string> streamAttributes = new Dictionary<MediaStreamAttributeKeys, string>();
            streamAttributes[MediaStreamAttributeKeys.CodecPrivateData] = _waveFormat.ToHexString();

            // reference: <a href="http://msdn.microsoft.com/en-us/library/system.windows.media.mediastreamdescription(v=vs.95).aspx">http://msdn.microsoft.com/en-us/library/system.windows.media.mediastreamdescription(v=vs.95).aspx</a>
            return new MediaStreamDescription(MediaStreamType.Audio, streamAttributes);

        }

        protected override void CloseMedia()
        {
            cleanup();
        }

        public void cleanup()
        {
            reader.Close();
        }

        // this method is called by MediaElement everytime a request for sample is made
        protected override void GetSampleAsync(
                MediaStreamType mediaStreamType)
        {
            //if (mediaStreamType == MediaStreamType.Audio)
            GetAudioSample();
        }

        private void GetAudioSample()
        {

            MediaStreamSample msSamp;

            if (reader.BaseStream.Position < reader.BaseStream.Length)
            {
                encodedSamples = BitConverter.ToInt16(reader.ReadBytes(2), 0);
                byte[] decodedAudio = Decode(reader.ReadBytes(encodedSamples));
                //encodedSamples = 8426;

                memStream = new MemoryStream(decodedAudio);

                //MediaStreamSample 
                msSamp = new MediaStreamSample(_audioDesc, memStream, _audioStreamOffset, memStream.Length,
                    _currentAudioTimeStamp, _emptySampleDict);

                _currentAudioTimeStamp += _waveFormat.AudioDurationFromBufferSize((uint)memStream.Length);

            }
            else
            {
                // Report end of stream
                msSamp = new MediaStreamSample(_audioDesc, null, 0, 0, 0, _emptySampleDict);
            }

            ReportGetSampleCompleted(msSamp);

            // calculate how long the audio has played so far
            _timeSpan = TimeSpan.FromSeconds((Int32)(DateTime.Now.Subtract(playTime).TotalSeconds)).ToString();
        }

        public byte[] Decode(byte[] encodedData)
        {
            try
            {
                // should be the same number of samples as on the capturing side
                short[] decodedFrame = new short[44100];
                int decodedBytes = decoder.Decode(encodedData, 0, encodedData.Length, decodedFrame, 0, false);

                byte[] decodedAudioData = null;

                decodedAudioData = new byte[decodedBytes * 2];

                try
                {
                    for (int shortIndex = 0, byteIndex = 0; shortIndex < decodedBytes; shortIndex++, byteIndex += 2)
                    {
                        byte[] temp = BitConverter.GetBytes(decodedFrame[shortIndex]);
                        decodedAudioData[byteIndex] = temp[0];        
                        decodedAudioData[byteIndex + 1] = temp[1];
                    }    
                }
                catch (Exception ex)
                {    
                    return decodedAudioData;
                }

                // to do: with decoded data
                return decodedAudioData;
            }
            catch (Exception ex)
            {
                //System.Diagnostics.Debug.WriteLine(ex.Message.ToString());
                return null;
            }
        }

    }
}

Once our custom classes have been created, we then need to hook them up in our UI using the code behind …

C#
//
// MainPage.xaml.cs
//

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.IO;
using System.Collections; 
using System.ComponentModel;
using System.Windows.Threading;
using System.Collections.ObjectModel;

using myAudio_23._12._212.Audio;

using System.Runtime.Serialization;

namespace myAudio_23._12._212
{
    public partial class MainPage : UserControl, INotifyPropertyChanged 
    {

        // declaration of variables ...
        // ... see attached code ...

        public MainPage()
        {
            InitializeComponent();

            this.Loaded += (s, e) =>
            {
                this.DataContext = this;

                // initialize the timer 
                this.timer = new DispatcherTimer()
                {
                    Interval = TimeSpan.FromMilliseconds(200)
                };
                this.timer.Tick += new EventHandler(timer_Tick);

                // I have named the serialized object file to recordedAudio.s2x
                path = System.IO.Path.Combine(Environment.GetFolderPath(
                        Environment.SpecialFolder.MyDocuments), "recordedAudio.s2x");

                if (reader != null)
                    reader.Close();

                // First check if the file exists
                if (File.Exists(path))
                {
                    // open file for reading
                    reader = new BinaryReader(File.Open(path, FileMode.Open));

                    // I had decided to reserve first 4 bytes of the file to contain the total bytes 
                    // that represents the serialized FileList object.
                    // *** I cannot remember why I decided to do that *****
                    // so here we extract the value from the 4 bytes ...
                    Int32 fSize = BitConverter.ToInt32(reader.ReadBytes(4), 0);

                    // ... and then extract the bytes representing the object to be deserialized
                    byte[] b = reader.ReadBytes(fSize);
                
                    // Now, declare and instantiate the object ...
                    FileList fL = new FileList(FileItems);

                    // ... deserialize the bytes back to its original form
                    fL = (FileList)WriteFromByte(b);

                    // This assignment will cause the NotifyPropertyChanged("FileItems") to be triggered
                    // and hence update the UI
                    FileItems = fL._files;

                    reader.Close();
                }

                // Make the first item in the RecordedFiles DataGrid selected 
                RecordedFiles.SelectedIndex = 0;
            };
        }


        /// <summary>
        /// This timer is started/stopped during recording/playing
        /// If recording, it also keeps track of recording time limit in minutes        
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>    
        void timer_Tick(object sender, EventArgs e)
        {
            if (this._sink != null)
            {
                if (this._sink.timeSpan <= _recLimit * 60)
                {
                    timeSpan = TimeSpan.FromSeconds(this._sink.timeSpan).ToString();
                    PlayTime = timeSpan;
                }
                else
                    StopRec();
            }
            else
            {
                timeSpan = this._mediaSource.timeSpan;
            }
        }

        /// <summary>
        /// checks to see if the user is allowed to access the capture devices
        /// </summary>
        /// <returns>True/False</returns>
        private bool EnsureAudioAccess()
        {
            return CaptureDeviceConfiguration.AllowedDeviceAccess || 
                         CaptureDeviceConfiguration.RequestDeviceAccess();
        } 

        private void StartRecording_Click(object sender, RoutedEventArgs e)
        {
            // check to see if user has permission to access capture devices
            // if not do nothing - you may wish to display a message box
            if (!EnsureAudioAccess())
                return;
    
            // initialize the new recording file name
            // at least ensures unique name
            long fileName = DateTime.Now.Ticks;

            // get the path to where the file will be saved
            // you may decide to display a file dialogue box for user to select since it is running
            // out of browser
            path = System.IO.Path.Combine(Environment.GetFolderPath(
                    Environment.SpecialFolder.MyDocuments), fileName.ToString()+".spx");

            // this is obvious checking if the file already exists
            // you may wish to be more careful not to just delete the file
            if (File.Exists(path))
            {
                File.Delete(path);
            }

            // set the writer to the file for writing bytes        
            writer = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate));

            // retrieve the audio capture devices
            var audioDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();

            // assign the audioDevice to our new Custom Source object
            // we picked the Default,
            // you may wish to let the user select one from the list. there may be more than one
            _captureSource = new CaptureSource() { AudioCaptureDevice = audioDevice };

            // instantiate our audio sink        
            _sink = new MemoryAudioSink();
    
            // set the selected capture source to the audio sink
            _sink.CaptureSource = _captureSource;

            // set the writer to the audio sink
            // used to write encoded bytes to file
            _sink.SetWriter = writer;

            // set the quality for encoding
            _sink.Quality = (Int16)Quality.Value;

            // initiate the start of capture 
            _captureSource.Start();

            // reinitialize the time span to zeros
            _timeSpan = "00:00:00";

            // start the timer 
            this.timer.Start();

            // call this method to enable/disable record/play buttons on UI
            StartRec();
        }

        private void StartRec()
        {
            StartRecording.IsEnabled = false;
            StopRecording.IsEnabled = true;
            StartPlaying.IsEnabled = false;
        }

        private void StopRecording_Click(object sender, RoutedEventArgs e)
        {
            StopRec();
        }

        private void StopRec()
        {
            // prepare the FileItem object that holds only three items below ...
            FileItem fI = new FileItem();

            fI.FilePath = path;            // path to the encoded NSpeex audio file
            fI.FileSize = writer.BaseStream.Length;    // file size
            fI.FileDuration = timeSpan;        // audio duration

            // add FileItem object to collection of FileItem 
            FileItems.Add(fI);

            // instantiate a File List object and initialize it to the content of the above File Item
            FileList fL = new FileList(FileItems);

            // create a byte array of serialized object
            byte[] b = WriteFromObject(fL);

            // set the path to save the serialized bytes to
            path = System.IO.Path.Combine(Environment.GetFolderPath(
            Environment.SpecialFolder.MyDocuments), "recordedAudio.s2x");

            // check its existence, delete it if it does
            if (File.Exists(path))
            {
                File.Delete(path);
            }

            // create a new Binary writer to write the bytes to file             
            BinaryWriter fileListwriter = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate));

            Int32 fSize = b.Length;         // get the size of the file to be written
            fileListwriter.Write(fSize);    // first write the size of the byte array (of the object)
            fileListwriter.Write(b);        // then write the actual byte array (of the object);

            _captureSource.Stop();        // stop capture from the capture device        
            this.timer.Stop();            // stop the timer         
            writer.Close();               // close the writer and the underlying stream
            fileListwriter.Close();       // close the other writer and the underlying stream
            _sink = null;                 // set the audio sink to null


            // reset the buttons enabled status
            StartRecording.IsEnabled = true;
            StopRecording.IsEnabled = false;        
            StartPlaying.IsEnabled = true;
        }


        private void StartPlaying_Click(object sender, RoutedEventArgs e)
        {
            // make sure the previous reader is closed
            if (reader != null)
            reader.Close();

            // confirm a file has been selected
            if (fileToPlay != "")
            {
                // reinitialize the reader
                reader = new BinaryReader(File.Open(fileToPlay, FileMode.Open));

                // reinstantiate the media stream source        
                _mediaSource = new CustomSourcePlayer();
    
                //Get the media total play time
                PlayTime = TimeSpan.FromSeconds(
                  BitConverter.ToInt32(reader.ReadBytes(4), 0) - 1).ToString();

                // set the media stream source reader to the created reader
                _mediaSource.SetReader = reader;

                // set the mediaElement's source to the media stream source 
                // mediaElement will call the derived method to enquire for samples at intervals
                // since it is AutoPlay is set to "True", it will start playing immediately
                MediaPlayer.SetSource(_mediaSource);

                // reset the buttons enabled status
                StartPlaying.IsEnabled = false;    
                StopPlaying.IsEnabled = true;        
                StartRecording.IsEnabled = false;
            }
        }

        private void StopPlaying_Click(object sender, RoutedEventArgs e)
        {    
            MediaPlayer.Stop();

            StopPlay();
        }

        private void MediaPlayer_MediaOpened(object sender, RoutedEventArgs e)
        {
            timer.Start();
        }

        private void MediaPlayer_MediaEnded(object sender, RoutedEventArgs e)
        {
            StopPlay();
        }

        private void StopPlay()
        {
            timer.Stop();
            _mediaSource.cleanup();
            _mediaSource = null;

            StartPlaying.IsEnabled = true;
            StopPlaying.IsEnabled = false;
            StartRecording.IsEnabled = true;
        }

        // Create a object, serializ it and return its byte array
        public static byte[] WriteFromObject(FileList files)
        {
            //Create FileList object.
            FileList fileItems = files;

            //Create a stream to serialize the object to.
            MemoryStream ms = new MemoryStream();

            // Serializer the object to the stream.
            DataContractSerializer ser = new DataContractSerializer(typeof(FileList));
            ser.WriteObject(ms, fileItems);
            byte[] array = ms.ToArray();

            ms.Close();
            ms.Dispose();

            return array;
        }


        // deserialize the object from byte array.
        public static object WriteFromByte(byte[] array)
        {
            MemoryStream memoryStream = new MemoryStream(array);
            DataContractSerializer ser = new DataContractSerializer(typeof(FileList));
            object obj = ser.ReadObject(memoryStream);
            return obj;
        }

        // get the selected file to play 
        private void RecordedFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {    
            IList list = e.AddedItems;
            fileToPlay = ((FileItem)list[0]).filePath;
        }
    }
}

Points of Interest

You may wish to enable logging in the attached code so that you can actually have a feel of size of the encoded bytes returned by the NSpeex encoder for each Sample. in my case the very first Sample was different in size from the rest of the encoded samples and sometimes the very last sample also turns out to be different.

For me to be sure to decode the correct number of bytes for every sample I decide to insert 2 bytes before each sample containing the size of that particular encoded sample  ...

C#
// get the length of the encoded Audio
encodedByteSize = (Int16)encodedAudio.Length;

// get bytes of the 'size' of the encoded Sample and write it to file
// before writing the actual bytes of the encoded Sample
writer.Write(BitConverter.GetBytes(encodedByteSize)); // <<--- the 2 bytes

// now write the encoded Bytes to file
writer.Write(encodedAudio);

// Log to file
logWriter.WriteLine(numSamples.ToString() + "\t" + encodedAudio.Length);

bytes representation

If anyone can figure out how to save the file such that it can be playable in any player with Speex codec, that would be great to share.

I hope you find this application useful.

History

No changes yet.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)