How to use the BASS audio library in UWP development

The BASS audio library is a solid library to decode and play many audio formats, supports gapless playback (using the mixer, not used in this example) and equalizer (not used in this example).

The library is available also for Windows Store development.

To reference the BASS audio library in your UWP project remember to add conditional references based on platform in the csproj file

<ItemGroup Condition="'$(Platform)' == 'x86'">
  <Content Include="$(SolutionDir)bass\Windows 10\x86\bass.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>
<ItemGroup Condition="'$(Platform)' == 'x64'">
  <Content Include="$(SolutionDir)bass\Windows 10\x64\bass.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>
<ItemGroup Condition="'$(Platform)' == 'ARM'">
  <Content Include="$(SolutionDir)bass\Windows 10\arm\bass.dll">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

Let’s see how we can use the library in a background audio player.

First we need the background task. Here we initialize the BASS audio library, and free it when the task ends. Since we are going to use BASS only to decode the audio we can use the “no sound” device.

using System;
using Un4seen.Bass;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
using Windows.Media.Playback;

namespace BackgroundPlayer
{
  public sealed class Player : IBackgroundTask
  {
    BackgroundTaskDeferral m_Deferral;
    BassMediaSource m_MediaSourceAdapter = null;

    public void Run(IBackgroundTaskInstance taskInstance)
    {
      m_Deferral = taskInstance.GetDeferral();
      taskInstance.Canceled += TaskInstance_Canceled;
      taskInstance.Task.Completed += TaskInstance_Completed;

      BassNet.Registration("Your email", "Your registration key");
      if (!Bass.BASS_Init(0, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero)) {
        BASSError err = Bass.BASS_ErrorGetCode();
        m_Deferral.Complete();
        return;
      }

      BackgroundMediaPlayer.MessageReceivedFromForeground += OnMessageReceived;
    }

    private void TaskInstance_Canceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
    {
      Bass.BASS_Free();
      m_Deferral.Complete();
    }

    void TaskInstance_Completed(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args)
    {
      Bass.BASS_Free();
      m_Deferral.Complete();
    }
  }
}

Now we need to play the file when the foreground app asks for it. Communication between the foreground app and the background task is made through messages.

When the background task receives a “file” request message it opens the file using BASS (we’ll see later how) and sets the media source of the bacgkround media player.

private async void OnMessageReceived(object sender, MediaPlayerDataReceivedEventArgs args)
{
  ValueSet msg = args.Data;

  if (msg.ContainsKey("file")) {
    string audioFile = msg["file"] as string;
    if (!string.IsNullOrEmpty(audioFile)) {
      m_MediaSourceAdapter = await BassMediaSource.CreateAsync(audioFile);
      BackgroundMediaPlayer.Current.Source = MediaSource.CreateFromIMediaSource(m_MediaSourceAdapter.GetMediaSource());
      BackgroundMediaPlayer.Current.Play();
    }
  }
}

The BassMediaSource class implements IMediaSource and is the one responsible of opening and playing the file.

The code is quite simple: we need to create the decode stream for the BASS audio library and listen to the MediaSource events (Closed, Starting, SampleRequested).

public async Task InitializeAsync()
{
  StorageFile sFile = await StorageFile.GetFileFromPathAsync(m_FilePath);
  BasicProperties prop = await sFile.GetBasicPropertiesAsync();
  m_FileSize = prop.Size;

  m_BassHandle = Bass.BASS_StreamCreateFile(m_FilePath, 0, (long)m_FileSize, BASSFlag.BASS_STREAM_DECODE);
  if (m_BassHandle == 0) {
    BASSError err = Bass.BASS_ErrorGetCode();
    System.Diagnostics.Debug.WriteLine("InitializeAsync error {0}", err);
  }

  BASS_CHANNELINFO cInfo = Bass.BASS_ChannelGetInfo(m_BassHandle);
  if (cInfo == null) {
    BASSError err = Bass.BASS_ErrorGetCode();
    System.Diagnostics.Debug.WriteLine("InitializeAsync error {0}", err);
  }
  long len = Bass.BASS_ChannelGetLength(m_BassHandle, BASSMode.BASS_POS_BYTES);
  double secs = Bass.BASS_ChannelBytes2Seconds(m_BassHandle, len);
  uint bits = 16;
  if (cInfo.Is32bit)
    bits = 32;
  else if (cInfo.Is8bit)
    bits = 8;

  AudioEncodingProperties pcmprops = AudioEncodingProperties.CreatePcm((uint)cInfo.freq, (uint)cInfo.chans, bits);
  m_MediaStreamSource = new MediaStreamSource(new AudioStreamDescriptor(pcmprops));
  m_MediaStreamSource.CanSeek = true;
  m_MediaStreamSource.BufferTime = TimeSpan.Zero;
  m_MediaStreamSource.Duration = TimeSpan.FromSeconds(secs);
  m_MediaStreamSource.Closed += mss_Closed;
  m_MediaStreamSource.Starting += mss_Starting;
  m_MediaStreamSource.SampleRequested += mss_SampleRequested;
} // InitializeAsync

The SampleRequested event occurs when then audio player needs a decoded sample to play. Here we just need to decode the requested data and return it to the player.

void mss_SampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
{
  var deferral = args.Request.GetDeferral();
  System.Diagnostics.Debug.WriteLine("mss_SampleRequested");

  try {
    byte[] buffer = new byte[4096];
    int decoded = Bass.BASS_ChannelGetData(m_BassHandle, buffer, buffer.Length);
    if (decoded == -1) {
      BASSError err = Bass.BASS_ErrorGetCode();
      System.Diagnostics.Debug.WriteLine("mss_SampleRequested error {0}", err);
    } else {
      double secs = Bass.BASS_ChannelBytes2Seconds(m_BassHandle, decoded);

      MediaStreamSample sample = MediaStreamSample.CreateFromBuffer(buffer.AsBuffer(), TimeSpan.FromSeconds(m_SecondsPosition));
      sample.Duration = TimeSpan.FromSeconds(secs);
      m_SecondsPosition += sample.Duration.TotalSeconds;
      args.Request.Sample = sample;
    }
  } catch { }
  deferral.Complete();
} // mss_SampleRequested

The application lets you select a file from your music library (files outside of your music library won’t work because the application does not have access to external folders).

Here’s the full source code of the test program (remember to set your BASS registration info)

Download source code

Have fun! 😉

Universal Windows Platform Hamburger menu navigation

A lot of UWP apps use the hamburger menu for page navigation, so I made a sample app.

pc2

Since Windows.UI.Xaml.Controls.Page is a UserControl the idea is to just replace the content of the MainPage instead of really navigate through pages.

I encountered some problems:

I solved these problems by creating a BasePage class with properties to set transitions and exposing new navigation events. The NavigationCacheMode is managed by the code.

Sadly NavigationEventArgs cannot be inherited (it’s a sealed class) and has no constructor so I had to use my own class.

The example app has some pages to navigate through and uses different transitions. The app runs correctly on the PC and on the phone.

To change the menu appearence on mobile I used the VisualStateManager: on mobile (but also on pc if you resize the window to less than 720 pixels) the menu is totally closed and a little larger when opened.

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState x:Name="wideView">
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="720" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="m_SplitView.CompactPaneLength" Value="40"/>
        <Setter Target="m_SplitView.OpenPaneLength" Value="200"/>
      </VisualState.Setters>
    </VisualState>
    <VisualState x:Name="narrowView">
      <VisualState.Setters>
        <Setter Target="m_SplitView.CompactPaneLength" Value="0"/>
        <Setter Target="m_SplitView.OpenPaneLength" Value="250"/>
      </VisualState.Setters>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="0" />
      </VisualState.StateTriggers>
    </VisualState>
  </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Here are some screenshots:

The app looks good also using the dark theme

I made a simple control for the menu item and in the MainPage there’s the standard SplitView. To let the code open the correct page and select the menu item you need to put in the ActionName the page’s class name.

<SplitView DisplayMode="CompactOverlay"  IsPaneOpen="False" 
           Grid.Row="1"
           x:Name="m_SplitView"
           CompactPaneLength="40" OpenPaneLength="200">
  <SplitView.Pane>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>

      <ScrollViewer VerticalScrollBarVisibility="Auto">
        <StackPanel Orientation="Vertical">
          <controls:MenuButton Icon="&#xE80F;" Label="Home" ActionName="HamburgerMenuNavigationUWP.Pages.HomePage" Tapped="MenuButton_Tapped" />
          <controls:MenuButton Icon="&#xE14C;" Label="Data page" ActionName="HamburgerMenuNavigationUWP.Pages.DataPage" Tapped="MenuButton_Tapped" />
        </StackPanel>
      </ScrollViewer>

      <StackPanel Grid.Row="1" BorderBrush="{ThemeResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,1,0,0" >
        <controls:MenuButton Icon="&#xE897;" Label="About" ActionName="HamburgerMenuNavigationUWP.Pages.AboutPage" Tapped="MenuButton_Tapped" />
        <controls:MenuButton Icon="&#xE713;" Label="Settings" ActionName="HamburgerMenuNavigationUWP.Pages.SettingsPage" Tapped="MenuButton_Tapped" />
      </StackPanel>
    </Grid>
  </SplitView.Pane>
  <SplitView.Content>
    <Grid Name="m_Grid">
      <!-- Here goes the content -->
    </Grid>
  </SplitView.Content>
</SplitView>

Pages must inherit from the BasePage class but are normal pages for the rest.

All the “dirty” work is done in App.cs (back stack, page cache, show/hide the back button, manage the back button event).

private void RootFrame_Navigating(object sender, NavigatingCancelEventArgs e)
{
  if (e.SourcePageType == typeof(MainPage))
    return;

  MainPage mp = (Window.Current.Content as Frame).Content as MainPage;

  // Do not navigate to the same page:
  if (m_CurrentPage != null && e.SourcePageType == m_CurrentPage.SourcePageType){
    if (mp != null)
      mp.IsPaneOpen = false;
    e.Cancel = true;
    return;
  }

  Pages.BasePage oldPage = null;
  Pages.BasePage newPage = null;

  //Check the cache:
  if (!m_PagesCache.TryGetValue(e.SourcePageType.FullName, out newPage))
    newPage = (Pages.BasePage)Activator.CreateInstance(e.SourcePageType, new object[] { });

  if (newPage != null) {
    newPage.Transitions = new TransitionCollection();   
    if (mp != null) {
      if (mp.MainGrid.Children.Count > 0)
        oldPage = mp.MainGrid.Children[0] as Pages.BasePage;

      // Call page events:
      if (oldPage != null) {
        oldPage.OnNavigatingFrom(e);
        if (e.Cancel)
          return;
      }

      // Animations:
      if (m_GoingBack) {
        if (oldPage != null) { 
          oldPage.Transitions = new TransitionCollection();
          if (oldPage.BackExitTransition != null)
            oldPage.Transitions = new TransitionCollection() { oldPage.BackExitTransition };              
        }

        if (newPage.BackEnterTransition != null)
          newPage.Transitions = new TransitionCollection() { newPage.BackEnterTransition };
      } else {
        if (oldPage != null) {
          oldPage.Transitions = new TransitionCollection();
          if (oldPage.ForwardExitTransition != null)
            oldPage.Transitions = new TransitionCollection() { oldPage.ForwardExitTransition };
        }

        if (newPage.ForwardEnterTransition != null)
          newPage.Transitions = new TransitionCollection() { newPage.ForwardEnterTransition };
      }
      
      if (oldPage != null) {
        // Remove oldPage from the MainPage:
        mp.MainGrid.Children.Remove(oldPage);

        // Call oldPage events:
        oldPage.OnNavigatedFrom(new Pages.NavigationArgs() { NavigationMode = m_GoingBack ? NavigationMode.Back : NavigationMode.Forward });
      }
        
      // Add the new page to the MainPage:
      mp.MainGrid.Children.Add(newPage);

      // Call newPage events:
      newPage.OnNavigatedTo(new Pages.NavigationArgs() { NavigationMode = m_GoingBack ? NavigationMode.Back : NavigationMode.New, Parameter = e.Parameter, SourcePageType = e.SourcePageType });

      // Select the menu item
      if (!mp.SelectMenu(e.SourcePageType.FullName) && !string.IsNullOrEmpty(newPage.SelectMenuAction))
        mp.SelectMenu(newPage.SelectMenuAction);

      // Close the menu:
      mp.IsPaneOpen = false;

      // Add the new page to the back stack
      if (!m_GoingBack) {
        if (m_CurrentPage != null)
          m_BackStack.Add(m_CurrentPage);            
      }
      m_CurrentPage = new PageStackEntry(e.SourcePageType, e.Parameter, e.NavigationTransitionInfo);

      // Save/remove pages with NavigationCacheMode.Required
      if (oldPage != null) {
        if (!m_GoingBack && oldPage.NavigationCacheMode == NavigationCacheMode.Required) {
          m_PagesCache[oldPage.GetType().FullName] = oldPage;
        } else if (m_GoingBack && m_PagesCache.ContainsKey(oldPage.GetType().FullName)) {
          m_PagesCache.Remove(oldPage.GetType().FullName);
        }
      }

      // Show/hide the back button
      SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
        m_BackStack.Count > 0 ? AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed;

      m_GoingBack = false;
    }
  }
  e.Cancel = true;
} // RootFrame_Navigating

 

You can download the source code here.

If you have advice, ideas, better solutions feel free to leave a comment.

Have fun. 😉