C# Corner

Recording Media in a Windows Store App, Part 1: Audio

Eric Vogel demonstrates how to use the Windows Runtime MediaCapture API to record audio.

More On Windows Store App Media from Visual Studio Magazine:

Today I'll cover how to use the Windows Runtime MediaCapture API to record audio in multiple formats and encoding qualities.

To get started, create a new C# Blank Windows Store App Project. Then update the app's capabilities to allow recording from a microphone. In addition, we'll need read and write access to the user's music library. To update the app's capabilities, open up the Package.appxmanifest file and select the Capabilities tab. Next, check the Microphone and Music Library check boxes, as seen in Figure 1.

[Click on image for larger view.] Figure 1. Setting Package.appxmanifest audio capture capabilities.

The next step is to add the AudioEncodingFormat enum used by the application to allow the selection of an audio encoding format. Create a new C# class file named AudioEncodingFormat, then create a new public enum named AudioEncodingFormat with Mp3, Mp4, and Wma values:

public enum AudioEncodingFormat
{ Mp3, Mp4, Wma };

I've also added an extension method class to the AudioEncodingFormat enum that includes a ToFileExtension extension method. TheToFileExtension extension method returns a matching file extension for each enumerator value:

public static class AudioEncodingFormatExtensions
{
    public static string ToFileExtension(this AudioEncodingFormat encodingFormat)
    {
        switch (encodingFormat)
        {
            case AudioEncodingFormat.Mp3:
                return ".mp3";
            case AudioEncodingFormat.Mp4:
                return ".mp4";
            case AudioEncodingFormat.Wma:
                return ".wma";
            default:
                throw new ArgumentOutOfRangeException("encodingFormat");
        }
    }
}

The next step is to create the application UI. Open up the MainPage.xaml file and copy the root StackPanel XAML from Listing 1 into your root Grid element.

<Page
    x:Class="VSMMediaCaptureDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:VSMMediaCaptureDemo"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <StackPanel Orientation="Horizontal" Margin="0,10,10,10">
                <TextBlock>Audio Format</TextBlock>
                <ComboBox Name="AudioFormat" ItemsSource="{Binding}" MinWidth="230" Margin="10,0,0,0"
                          SelectionChanged="AudioFormat_SelectionChanged"></ComboBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" Margin="0,10,10,10">
                <TextBlock>Audio Quality</TextBlock>
                <ComboBox Name="AudioQuality" ItemsSource="{Binding}" MinWidth="230" Margin="10,0,0,0"
                          SelectionChanged="AudioQuality_SelectionChanged"></ComboBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Button Name="RecordButton" Click="RecordButton_Click">Record</Button>
                <Button Name="StopButton" Click="StopButton_Click">Stop</Button>
                <Button Name="SaveButton" Click="SaveButton_Click">Save</Button>
            </StackPanel>
            <StackPanel  Orientation="Horizontal" Margin="0,10,0,0">
                <TextBlock>Duration</TextBlock>
                <TextBlock Name="Duration" Text="{Binding}" Margin="10,0,0,0"></TextBlock>
            </StackPanel>
        </StackPanel>
    </Grid>
</Page>

Listing 1: The MainPage.xaml markup.

You should now see the application UI preview (Figure 2) within Visual Studio.

[Click on image for larger view.] Figure 2. The Application UI.

Now it's time to make the application come alive. Open up the MainPage.xaml.cs code-behind file, and add the following namespace using statements to the MainPage class:

using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Popups;
using Windows.Media.Capture;

Next, add the Recording Mode enum type that has Initializing, Recording and Stopped values:

public enum RecordingMode
{
    Initializing,
    Recording,
    Stopped,
};

Now, add the private member variables for capturing an audio file and an audio stream:

private MediaCapture _mediaCapture;
private IRandomAccessStream _audioStream;

Then add a private member variable for a FileSavePicker that allows the user to save an audio file:

private FileSavePicker _fileSavePicker;

Next comes member variables for a timer and a TimeSpan, to keep track of the elapsed time during a recording:

private DispatcherTimer _timer;
private TimeSpan _elapsedTime;

Then I add member variables to store the user's selected audio encoding format and encoding quality:

private AudioEncodingFormat _selectedFormat;
private AudioEncodingQuality _encodingQuality; 

Next, in the OnNavigatedTo event, I initialize the media capture, then populate the audio format and audio quality check boxes. I also initialize the recording controls enabled states and set up the timer:

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    await InitMediaCapture();
    LoadAudioEncodings();
    LoadAudioQualities();
    UpdateRecordingControls(RecordingMode.Initializing);
    InitTimer();
}

In the InitMediaCapture method, I initialize the MediaCapture object for an audio capture and subscribe to its Failed and RecordLimitationExceeded events:

private async Task InitMediaCapture()
{
    _mediaCapture = new MediaCapture();
    var captureInitSettings = new MediaCaptureInitializationSettings();
    captureInitSettings.StreamingCaptureMode = StreamingCaptureMode.Audio;
    await _mediaCapture.InitializeAsync(captureInitSettings);
    _mediaCapture.Failed += MediaCaptureOnFailed;
    _mediaCapture.RecordLimitationExceeded += MediaCaptureOnRecordLimitationExceeded;
}

In The LoadAudioEncodings method, I populate the AudioFormat ComboBox from the list of available values for the AudioEncodingFormat enum, and set the default audio format to MP3:

private void LoadAudioEncodings()
 {
     var audioEncodingFormats = Enum.GetValues(typeof(AudioEncodingFormat)).Cast();
     AudioFormat.ItemsSource = audioEncodingFormats;
     AudioFormat.SelectedItem = AudioEncodingFormat.Mp3;
 }

In the LoadAudioQualities method, I populate the AudioQuality ComboBox from the list of available values for the AudioEncodingQuality enum and set the default quality to Auto:

private void LoadAudioQualities()
 {
     var audioQualities = Enum.GetValues(typeof (AudioEncodingQuality)).Cast();
     AudioQuality.ItemsSource = audioQualities;
     AudioQuality.SelectedItem = AudioEncodingQuality.Auto;
 }

The UpdateRecordingControls method sets up the RecordButton, StopButton, and SaveButton enabled states based on the given RecoringMode enum value. When the RecordingMode is Initializing, the RecordButton is enabled and the StopButton and SaveButton controls are disabled. When the RecordingMode is Recording, the RecordButton and SaveButton are disabled and the StopButton is enabled. When the RecordingMode is Stopped, the RecordButton and SaveButton are enabled and the SopButton is disabled. Listing 2 has the complete UpdateRecordingControls method.

private void UpdateRecordingControls(RecordingMode recordingMode)
 {
     switch (recordingMode)
     {
         case RecordingMode.Initializing:
             RecordButton.IsEnabled = true;
             StopButton.IsEnabled = false;
             SaveButton.IsEnabled = false;
             break;
         case RecordingMode.Recording:
             RecordButton.IsEnabled = false;
             StopButton.IsEnabled = true;
             SaveButton.IsEnabled = false;
             break;
         case RecordingMode.Stopped:
             RecordButton.IsEnabled = true;
             StopButton.IsEnabled = false;
             SaveButton.IsEnabled = true;
             break;
         default:
             throw new ArgumentOutOfRangeException("recordingMode");
     }
 }

Listing 2: The UpdateRecordingControls method.

The InitTimer method initializes a DispatchTimer with an interval of 100 milliseconds. In addition, the application subscribes to the timer's Tick event:

private void InitTimer()
{
    _timer = new DispatcherTimer();
    _timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
    _timer.Tick += TimerOnTick;
}

The TimerOnTick method updates the _elapsedTime TimeSpan, and binds it to the Duration TextBlock for display:

private void TimerOnTick(object sender, object o)
{
  _elapsedTime = _elapsedTime.Add(_timer.Interval);
    Duration.DataContext = _elapsedTime;
}

In MediaCaptureOnRecordLimitationExceeded, I stop the recording and notify the user that he or she exceeded the maximum recording length from the UI Thread:

private async void MediaCaptureOnRecordLimitationExceeded(MediaCapture sender)
{
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            await sender.StopRecordAsync();
            var warningMessage = new MessageDialog("The recording has stopped because you exceeded the maximum recording length.", "Recording Stoppped");
            await warningMessage.ShowAsync();
        });
}

In the MediaCaptureOnFailed method, I display the captured error message from the MediaCapture API and display it to the user from the UI Thread:

private async void MediaCaptureOnFailed(MediaCapture sender, MediaCaptureFailedEventArgs errorEventArgs)
{
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
        var warningMessage = new MessageDialog(String.Format("The audio capture failed: {0}", errorEventArgs.Message), "Capture Failed");
        await warningMessage.ShowAsync();
    });
}

The RecordButton_Click method handles the Click event of the RecordButton control. In the method, the user's selected AudioEncodingFormat value is used to create a MediaEncodingProfile:

MediaEncodingProfile encodingProfile = null;

switch (_selectedFormat)
{
    case AudioEncodingFormat.Mp3:
        encodingProfile = MediaEncodingProfile.CreateMp3(_encodingQuality);
        break;
    case AudioEncodingFormat.Mp4:
        encodingProfile = MediaEncodingProfile.CreateM4a(_encodingQuality);
        break;
    case AudioEncodingFormat.Wma:
        encodingProfile = MediaEncodingProfile.CreateWma(_encodingQuality);
        break;
    default:
        throw new ArgumentOutOfRangeException();
}

Next, an InMemoryRandomAccessStream is created and passed to the MediaCapture API to start recording audio to the stream:

_audioStream = new InMemoryRandomAccessStream();
await _mediaCapture.StartRecordToStreamAsync(encodingProfile, _audioStream);

Next the UpdateRecordingControls method is called to setup the recording buttons for recording:

UpdateRecordingControls(RecordingMode.Recording);

Last, the DispatchTimer is started:

_timer.Start();

See Listing 3 for the completed RecordButton_Click method implementation.

private async void RecordButton_Click(object sender, RoutedEventArgs e)
{
    MediaEncodingProfile encodingProfile = null;

    switch (_selectedFormat)
    {
        case AudioEncodingFormat.Mp3:
            encodingProfile = MediaEncodingProfile.CreateMp3(_encodingQuality);
            break;
        case AudioEncodingFormat.Mp4:
            encodingProfile = MediaEncodingProfile.CreateM4a(_encodingQuality);
            break;
        case AudioEncodingFormat.Wma:
            encodingProfile = MediaEncodingProfile.CreateWma(_encodingQuality);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }

    _audioStream = new InMemoryRandomAccessStream();
    await _mediaCapture.StartRecordToStreamAsync(encodingProfile, _audioStream);
    UpdateRecordingControls(RecordingMode.Recording);
    _timer.Start();
}

Listing 3: The completed RecordButton_Click method.

Next I implement the StopButton Click event via the StopButton_Click method. The method stops the in-progress recording, updates the recording controls, and stops the duration timer:

private async void StopButton_Click(object sender, RoutedEventArgs e)
{
    await _mediaCapture.StopRecordAsync();
    UpdateRecordingControls(RecordingMode.Stopped);
    _timer.Stop();
}

Now, for the SaveButton Click event via the SaveButton_Click method. First, the user's prompted to pick a save file:

var mediaFile = await _fileSavePicker.PickSaveFileAsync();

If the user selected a media file, the captured _audioStream is written to the selected file and the recording controls are updated for the Stopped state:

if (mediaFile != null)
{
    using (var dataReader = new DataReader(_audioStream.GetInputStreamAt(0)))
    {
        await dataReader.LoadAsync((uint) _audioStream.Size);
        byte[] buffer = new byte[(int) _audioStream.Size];
        dataReader.ReadBytes(buffer);
        await FileIO.WriteBytesAsync(mediaFile, buffer);
        UpdateRecordingControls(RecordingMode.Initializing);
    }
}

Here's the completed SaveButton_Click method:

private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
    var mediaFile = await _fileSavePicker.PickSaveFileAsync();

    if (mediaFile != null)
    {
        using (var dataReader = new DataReader(_audioStream.GetInputStreamAt(0)))
        {
            await dataReader.LoadAsync((uint) _audioStream.Size);
            byte[] buffer = new byte[(int) _audioStream.Size];
            dataReader.ReadBytes(buffer);
            await FileIO.WriteBytesAsync(mediaFile, buffer);
            UpdateRecordingControls(RecordingMode.Initializing);
        }
    }
}

Next up is the SelectionChanged event of the AudioFormat ComboBox via the AudioFormat_SelectionChanged method. The method sets the _selectedFormat member variable from the user's selection and updates the FileSavePicker:

private void AudioFormat_SelectionChanged(object sender, SelectionChangedEventArgs e)
 {
     _selectedFormat = (AudioEncodingFormat)AudioFormat.SelectedItem;
     InitFileSavePicker();
 }

The InitFileSavePicker initializes _fileSavePicker FileSavePicker member variable:

_fileSavePicker = new FileSavePicker();

Next, the _fileSavePicker is updated to only allow the user to select the matching file extension for the user's selected audio format:

_fileSavePicker.FileTypeChoices.Add("Encoding", new List<string>() { _selectedFormat.ToFileExtension() });

Then the SuggestedStartLocation on the _fileSavePicker is set to the user's music library:

_fileSavePicker.SuggestedStartLocation = PickerLocationId.MusicLibrary;

Next, the AudioQuality_SelectionChanged event is implemented, which handles the SelectionChanged event of the AudioQuality ComboBox control. The method simply sets the _encodingQuality member variable from the selected AudioQuality ComboBox value:

private void AudioQuality_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    _encodingQuality = (AudioEncodingQuality) AudioQuality.SelectedItem;
}

Listing 4 contains the completed MainPage class implementation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Popups;
using Windows.Media.Capture;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace VSMMediaCaptureDemo
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public enum RecordingMode
        {
            Initializing,
            Recording,
            Stopped,
        };

        private MediaCapture _mediaCapture;
        private IRandomAccessStream _audioStream;
        private FileSavePicker _fileSavePicker;
        private DispatcherTimer _timer;
        private TimeSpan _elapsedTime;
        private AudioEncodingFormat _selectedFormat;
        private AudioEncodingQuality _encodingQuality;

        public MainPage()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            await InitMediaCapture();
            LoadAudioEncodings();
            LoadAudioQualities();
            UpdateRecordingControls(RecordingMode.Initializing);
            InitTimer();
        }

        private async Task InitMediaCapture()
        {
            _mediaCapture = new MediaCapture();
            var captureInitSettings = new MediaCaptureInitializationSettings();
            captureInitSettings.StreamingCaptureMode = StreamingCaptureMode.Audio;
            await _mediaCapture.InitializeAsync(captureInitSettings);
            _mediaCapture.Failed += MediaCaptureOnFailed;
            _mediaCapture.RecordLimitationExceeded += MediaCaptureOnRecordLimitationExceeded;
        }

        private void LoadAudioEncodings()
        {
            var audioEncodingFormats = Enum.GetValues(typeof(AudioEncodingFormat)).Cast<AudioEncodingFormat>();
            AudioFormat.ItemsSource = audioEncodingFormats;
            AudioFormat.SelectedItem = AudioEncodingFormat.Mp3;
        }

        private void LoadAudioQualities()
        {
            var audioQualities = Enum.GetValues(typeof (AudioEncodingQuality)).Cast<AudioEncodingQuality>();
            AudioQuality.ItemsSource = audioQualities;
            AudioQuality.SelectedItem = AudioEncodingQuality.Auto;
        }

        private void UpdateRecordingControls(RecordingMode recordingMode)
        {
            switch (recordingMode)
            {
                case RecordingMode.Initializing:
                    RecordButton.IsEnabled = true;
                    StopButton.IsEnabled = false;
                    SaveButton.IsEnabled = false;
                    break;
                case RecordingMode.Recording:
                    RecordButton.IsEnabled = false;
                    StopButton.IsEnabled = true;
                    SaveButton.IsEnabled = false;
                    break;
                case RecordingMode.Stopped:
                    RecordButton.IsEnabled = true;
                    StopButton.IsEnabled = false;
                    SaveButton.IsEnabled = true;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("recordingMode");
            }
        }

        private void InitTimer()
        {
            _timer = new DispatcherTimer();
            _timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
            _timer.Tick += TimerOnTick;
        }

        private void TimerOnTick(object sender, object o)
        {
          _elapsedTime = _elapsedTime.Add(_timer.Interval);
            Duration.DataContext = _elapsedTime;
        }

        private async void MediaCaptureOnRecordLimitationExceeded(MediaCapture sender)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
                {
                    await sender.StopRecordAsync();
                    var warningMessage = new MessageDialog("The recording has stopped because you exceeded the maximum recording length.", "Recording Stoppped");
                    await warningMessage.ShowAsync();
                });
        }

        private async void MediaCaptureOnFailed(MediaCapture sender, MediaCaptureFailedEventArgs errorEventArgs)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                var warningMessage = new MessageDialog(String.Format("The audio capture failed: {0}", errorEventArgs.Message), "Capture Failed");
                await warningMessage.ShowAsync();
            });
        }

        private async void RecordButton_Click(object sender, RoutedEventArgs e)
        {
            MediaEncodingProfile encodingProfile = null;

            switch (_selectedFormat)
            {
                case AudioEncodingFormat.Mp3:
                    encodingProfile = MediaEncodingProfile.CreateMp3(_encodingQuality);
                    break;
                case AudioEncodingFormat.Mp4:
                    encodingProfile = MediaEncodingProfile.CreateM4a(_encodingQuality);
                    break;
                case AudioEncodingFormat.Wma:
                    encodingProfile = MediaEncodingProfile.CreateWma(_encodingQuality);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            _audioStream = new InMemoryRandomAccessStream();
            await _mediaCapture.StartRecordToStreamAsync(encodingProfile, _audioStream);
            UpdateRecordingControls(RecordingMode.Recording);
            _timer.Start();
        }

        private async void StopButton_Click(object sender, RoutedEventArgs e)
        {
            await _mediaCapture.StopRecordAsync();
            UpdateRecordingControls(RecordingMode.Stopped);
            _timer.Stop();
        }

        private async void SaveButton_Click(object sender, RoutedEventArgs e)
        {
            var mediaFile = await _fileSavePicker.PickSaveFileAsync();

            if (mediaFile != null)
            {
                using (var dataReader = new DataReader(_audioStream.GetInputStreamAt(0)))
                {
                    await dataReader.LoadAsync((uint) _audioStream.Size);
                    byte[] buffer = new byte[(int) _audioStream.Size];
                    dataReader.ReadBytes(buffer);
                    await FileIO.WriteBytesAsync(mediaFile, buffer);
                    UpdateRecordingControls(RecordingMode.Initializing);
                }
            }
        }

        private void AudioFormat_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            _selectedFormat = (AudioEncodingFormat)AudioFormat.SelectedItem;
            InitFileSavePicker();
        }

        private void InitFileSavePicker()
        {
            _fileSavePicker = new FileSavePicker();
            _fileSavePicker.FileTypeChoices.Add("Encoding", new List<string>() { _selectedFormat.ToFileExtension() });
            _fileSavePicker.SuggestedStartLocation = PickerLocationId.MusicLibrary;
        }

        private void AudioQuality_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            _encodingQuality = (AudioEncodingQuality) AudioQuality.SelectedItem;
        }
    }
}

Listing 4: The complete MainPage.xaml.cs MainPage class implementation.

You should now be able to run the completed application (Figure 3). You'll first be prompted to allow access to your microphone. After that, you should be able to record audio in either MP3, MP4, or WMA file formats. Assignable audio qualities are also available.

[Click on image for larger view.] Figure 3. The completed application, recording an MP3 with auto quality.

You should be able to save the recording from memory to file via the Save button, as seen in Figure 4.

[Click on image for larger view.] Figure 4. Saving an audio recording.

You've seen how easy it is to implement audio capture into a Windows Store App. The Windows Runtime makes audio and video capture a breeze. Stay tuned for the next installment, where I'll cover how to capture video and pictures using the Windows Runtime MediaCapture API.

About the Author

Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube