PETZOLD BOOK BLOG

Charles Petzold on writing books, reading books, and exercising the internal UTM


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

Sharing XAudio2 Code Between Windows 8 and Windows Phone 8 Apps

October 2, 2013
New York, N.Y.

Often as I'm writing about particular software technologies, I'm so immersed that I need to deliberately ignore other technologies that might be percolating. I don't fret too much because I figure I can catch up at a later time. Such is the case with Windows Phone 8.

I did a lot of Windows Phone 7 programming a couple years ago, but then I shifted my attentions to Windows 8. By the time Windows Phone 8 was ready to be played with, I was deep into exploring the many aspects of using DirectX from Windows 8 applications, and DirectX tends to dominate one's life. Yet, this made my lack of familiarity with Windows Phone 8 all the stranger, because one of the big enhancements of Windows Phone 8 over Windows Phone 7 is the ability to access DirectX!

Consequently, in the months that I've been writing my DirectX Factor column for MSDN Magazine, I've often wondered "How easy would it be to port this code from Windows 8 to Windows Phone 8?" and I recently decided to try to answer that question.

Turns out large chunks of DirectX aren't available at all to Windows Phone 8 applications. Direct2D, DirectWrite, and the Windows Imaging Component (WIC) are not available, which means that columns I've been working on recently won't be portable at all. But Direct3D is available as well as XAudio2 and the Windows Audio Session API (WASAPI). The first six installments of the DirectX Factor column focused on XAudio2 (which lets you generate sound in real time and is ideal for making musical synthesizer programs) so I was in luck!

There's another giant difference between accessing DirectX from a Windows 8 app and a Windows Phone 8 app: Because DirectX is a C++ based API, it is most straightforward to use DirectX from a C++ application. Indeed, the Visual Studio templates for creating a Windows 8 DirectX application are for C++ only. However, you can't write a Windows Phone 8 application directly in C++. To use DirectX from a Windows Phone 8 app, you need to isolate the DirectX code in a Windows Phone Runtime Component written in C++, and then access that component from the C# or Visual Basic application. Interestingly, you can use this same strategy with Windows 8: You can isolate the DirectX code in a Windows Runtime Component written in C++ and then access that from a Windows 8 application written in C# or Visual Basic.

As a test case, I decided to port the ChromaticButtonKeyboard sample from the February 2013 installment of DirectX Factor to Windows Phone 8. This program uses XAudio2 to build an extremely primitive electronic music instrument with a keyboard resembling the bayan, a Russian button accordian I first heard in the music of Russian composer Sofia Gubaidulina.

But I didn't want to just port it. I wanted instead to refactor the code and markup into two Visual Studio solutions, one for Windows 8.1 and the other for Windows Phone 8, and share a lot of code and markup in the process. And by "share" I don't mean I wanted to copy code and markup from one project to another and tweak it a bit in the process. I mean I wanted code and XAML files that are truly shared between the two Visual Studio projects through file linking, so when a file is changed in one project, it gets changed in the other as well.

That's right: Call me naïve and have a good chuckle, but I wanted to write once and run everywhere . Or at least run on the two Microsoft operating systems with the windows "Windows" and the numeral "8" in their names! Both Windows 8 and Windows Phone 8 have APIs that derive from Silverlight, so how hard could it be?

I decided to call the program Bayan, and for createst consistency between Windows 8 and Windows Phone 8, I decided to use C# and XAML for the application project itself. So I created two Visual Studio projects:

Of course, you can use regular Visual Studio 2013 to create these two solutions. I experimented with having both types of application projects in one solution, but it got messy because they have different target CPUs.

I'm going to be discussing the actual process of creating these projects and sharing the code just in case you need to use these techniques in your own work. If you want, you can play along.

The Windows 8.1 Audio Component

Let's begin with the Windows 8.1 program because the code is mostly just refactored from the ChromaticButtonKeyboard program in the Febuary 2013 DirectX Factor. Consult that article for what the code actually does!

The Bayan_Windows81 solution I just created already contains a C# application project named Bayan, but it needs another project for the DirectX C++ code. I right-clicked the solution name in the Solution Explorer, and selected Add and New Project. In the New Project dialog I specified Visual C++ and Windows Store at the left, and chose the "Windows Runtime Component" template. This template creates a Windows 8 DLL that can be accessed from Windows 8 programs written in other languages. I gave it a name of BayanAudioComponent and deleted the Class1.cpp and Class1.h files automatically created by the project template.

The BayanAudioComponent will contain all the XAudio2 code. Consequently, the project needs access to the xaudio2.lib import library. Right-click the project name and select Properties to bring up the project Property Pages dialog. Select Linker and Input on the left, select All Configurations and All Platforms at the top, and edit the Additional Dependencies field to add:

xaudio2.lib

To the pch.h file I added the line:

#include <xaudio2.h>

To the BayanAudioComponent project I added a class named SawtoothOscillator. Here's the SawtoothOscillator.h header file:

#pragma once

namespace BayanAudioComponent
{
    class SawtoothOscillator
    {
    private:
        static const int BASE_FREQ = 20;
        static const int BUFFER_LENGTH = (44100 / BASE_FREQ);

        IXAudio2SourceVoice * pSourceVoice;
        short waveformBuffer[BUFFER_LENGTH];

    public:
        SawtoothOscillator(IXAudio2* pXAudio2);
        void SetFrequency(float freq);
        void SetAmplitude(float amp);
    };
}

Except for the namespace name, it's identical to the SawtoothOscillator.h file in the earlier ChromaticButtonKeyboard project. This class is private to the component, so it actually doesn't need a namespace name, but I gave it one anyway. Here's the SawtoothOscillator.cpp code file, also the same as the earlier file except for the namespace name:

#include "pch.h"
#include "SawtoothOscillator.h"

using namespace BayanAudioComponent;
using namespace Platform;

SawtoothOscillator::SawtoothOscillator(IXAudio2* pXAudio2)
{
    // Create a source voice
    WAVEFORMATEX waveFormat;
    waveFormat.wFormatTag = WAVE_FORMAT_PCM;
    waveFormat.nChannels = 1;
    waveFormat.nSamplesPerSec = 44100;
    waveFormat.nAvgBytesPerSec = 44100 * 2;
    waveFormat.nBlockAlign = 2;
    waveFormat.wBitsPerSample = 16;
    waveFormat.cbSize = 0;

    HRESULT hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat, 
                                             0, XAUDIO2_MAX_FREQ_RATIO);
    if (FAILED(hr))
        throw ref new COMException(hr, "CreateSourceVoice failure");

    // Initialize the array
    for (int sample = 0; sample < BUFFER_LENGTH; sample++)
        waveformBuffer[sample] = 
            (short)(65535 * sample / BUFFER_LENGTH - 32768);

    // Submit the array
    XAUDIO2_BUFFER buffer = {0};
    buffer.AudioBytes = 2 * BUFFER_LENGTH;
    buffer.pAudioData = (byte *)waveformBuffer;
    buffer.Flags = XAUDIO2_END_OF_STREAM;
    buffer.PlayBegin = 0;
    buffer.PlayLength = BUFFER_LENGTH;
    buffer.LoopBegin = 0;
    buffer.LoopLength = BUFFER_LENGTH;
    buffer.LoopCount = XAUDIO2_LOOP_INFINITE;

    hr = pSourceVoice->SubmitSourceBuffer(&buffer);

    if (FAILED(hr))
        throw ref new COMException(hr, "SubmitSourceBuffer failure");

    // Start the voice playing
    pSourceVoice->Start();
}

void SawtoothOscillator::SetFrequency(float freq)
{
    pSourceVoice->SetFrequencyRatio(freq / BASE_FREQ);
}

void SawtoothOscillator::SetAmplitude(float amp)
{
    pSourceVoice->SetVolume(amp);
}

The BayanAudioComponent project should now build without errors, but I'm not done with it yet.

In a Windows Runtime Component, a public class must a reference class (use the ref keyword so it is reference counted) and have a namespace and be sealed (or non-instantiable), and all public methods and properties must use only Windows Runtime types. DirectX types are not Windows Runtime types. So to use DirectX from a Windows 8 C# program (as I'll be doing here), all the DirectX stuff must be internal to the Windows Runtime Component with a public interface consisting solely of Windows Runtime types. The SawtoothOscillator class is private to the Windows Runtime Component so the rules don't apply to that, but I also added a public class to BayanAudioComponent that I called AudioController. Here's the header file:

#pragma once
#include "SawtoothOscillator.h"

namespace BayanAudioComponent
{
    public ref class AudioController sealed
    {
    private:
        ~AudioController();
        int m_numOscillators;
        Microsoft::WRL::ComPtr<IXAudio2> m_xaudio2;
        IXAudio2MasteringVoice * m_pMasteringVoice;
        std::vector<SawtoothOscillator *> m_availableOscillators;
        std::map<int, SawtoothOscillator *> m_playingOscillators;

    public:
        AudioController(int numOscillators);
        void Start();
        void Stop();
        void NoteOn(int id, int midiNumber);
        void NoteOff(int id);
    };
}

The constructor argument indicates the number of SawtoothOscillator instances to create and store in the availableOscillators collection. Most of the code in this class was originally in the MainPage.xaml.cpp file in the original ChromaticButtonKeyboard project:

// AudioController.cpp
#include "pch.h"
#include "AudioController.h"

using namespace BayanAudioComponent;
using namespace Platform;

AudioController::AudioController(int numOscillators) : 
                        m_numOscillators(numOscillators)
{
    // Create an IXAudio2 object
    HRESULT hr = XAudio2Create(&m_xaudio2);

    if (FAILED(hr))
        ref new COMException(hr, "XAudio2Create failure");

    XAUDIO2_DEBUG_CONFIGURATION debugConfig = { 0 };
    debugConfig.TraceMask = XAUDIO2_LOG_DETAIL | XAUDIO2_LOG_WARNINGS;
    m_xaudio2->SetDebugConfiguration(&debugConfig);

    // Create a mastering voice
    hr = m_xaudio2->CreateMasteringVoice(&m_pMasteringVoice);

    if (FAILED(hr))
        ref new COMException(hr, "CreateMasteringVoice failure");

    // Create half a dozen oscillators
    for (int i = 0; i < numOscillators; i++)
    {
        SawtoothOscillator* oscillator = new SawtoothOscillator(m_xaudio2.Get());
        oscillator->SetAmplitude(0);
        m_availableOscillators.push_back(oscillator);
    }
}

AudioController::~AudioController()
{
    for (std::pair<int, SawtoothOscillator *> pair : m_playingOscillators)
    {
        NoteOff(pair.first);
    }

    for (SawtoothOscillator * oscillator : m_availableOscillators)
    {
        delete oscillator;
    }
}

void AudioController::Start()
{
    m_xaudio2->StartEngine();
}

void AudioController::Stop()
{
    m_xaudio2->StopEngine();
}

void AudioController::NoteOn(int id, int midiNumber)
{
    if (m_availableOscillators.size() > 0)
    {
        SawtoothOscillator* pOscillator = m_availableOscillators.back();
        m_availableOscillators.pop_back();

        double freq = 440 * pow(2, (midiNumber - 69) / 12.0);
        pOscillator->SetFrequency((float)freq);
        pOscillator->SetAmplitude(1.0f / m_numOscillators);
        m_playingOscillators[id] = pOscillator;
    }
}

void AudioController::NoteOff(int id)
{
    SawtoothOscillator * pOscillator = m_playingOscillators[id];

    if (pOscillator != nullptr)
    {
        pOscillator->SetAmplitude(0);
        m_availableOscillators.push_back(pOscillator);
        m_playingOscillators.erase(id);
    }
}

The very brief Windows Phone 8 specific documentation for XAudio2 (in a link above) indicates that StopEngine and StartEngine should be called when the app is suspended and resumed, so I've anticipated that requirement.

Thus completes the BayanAudioComponent.

The Windows 8.1 Controls Component

I also decided to isolate the code for the controls required by the program. In the Bayan_Windows81 solution I created a second Windows Runtime Component project, but for this one I specified C# and named it BayanControlsComponent. This component will contain two controls: Key, which derives from ContentControl, and Keyboard, which derives from UserControl.

For the Key control, I added a new item to the BayanControlsComponent of type Templated Control and named it Key. This process creates two files: a Key.cs file for the control's code, and a Generic.xaml file in the Themes folder containing the default template for the control.

In the original ChromaticButtonKeyboard project, the template for the Key control was defined in the MainPage.xaml file. I moved that markup to the Generic.xaml file in this new project:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BayanControlsComponent">

    <Style TargetType="local:Key">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Key">
                    <Grid>
                        <Ellipse Name="ellipse"
                                 Fill="{TemplateBinding Background}"
                                 Stroke="{Binding RelativeSource={RelativeSource TemplatedParent}, 
                                                  Path=Foreground}"
                                 StrokeThickness="3" />
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />

                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="Pressed">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
                                                                       Storyboard.TargetProperty="Fill">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Lime" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

The Key.cs file is basically the same as the Key.cpp file from the original ChromaticButtonKeyboard project except converted from C++ to C#:

using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace BayanControlsComponent
{
    public sealed class Key : ContentControl
    {
        static readonly DependencyProperty isPressedProperty =
                    DependencyProperty.Register("IsPressed",
                        typeof(bool), typeof(Key),
                        new PropertyMetadata(false, OnIsPressedChanged));

        public event EventHandler<bool> IsPressedChanged;

        List<uint> pointerList = new List<uint>();

        public Key()
        {
            this.DefaultStyleKey = typeof(Key);
        }

        public static DependencyProperty IsPressedProperty
        {
            get { return isPressedProperty; }
        }

        public bool IsPressed
        {
            set { SetValue(IsPressedProperty, value); }
            get { return (bool)GetValue(IsPressedProperty); }
        }

        protected override void OnPointerEntered(PointerRoutedEventArgs args)
        {
            if (args.Pointer.IsInContact)
                AddToList(args.Pointer.PointerId);

            base.OnPointerEntered(args);
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs args)
        {
            AddToList(args.Pointer.PointerId);
            base.OnPointerPressed(args);
        }

        protected override void OnPointerReleased(PointerRoutedEventArgs args)
        {
            RemoveFromList(args.Pointer.PointerId);
            base.OnPointerReleased(args);
        }

        protected override void OnPointerExited(PointerRoutedEventArgs args)
        {
            RemoveFromList(args.Pointer.PointerId);
            base.OnPointerExited(args);
        }

        void AddToList(uint id)
        {
            if (!pointerList.Contains(id))
                pointerList.Add(id);

            CheckList();
        }

        void RemoveFromList(uint id)
        {
            if (pointerList.Contains(id))
                pointerList.Remove(id);

            CheckList();
        }

        void CheckList()
        {
            this.IsPressed = pointerList.Count > 0;
        }

        static void OnIsPressedChanged(DependencyObject obj,
                                       DependencyPropertyChangedEventArgs args)
        {
            Key key = (Key)obj;

            if (key.IsPressedChanged != null)
                key.IsPressedChanged(key, key.IsPressed);

            VisualStateManager.GoToState(key, key.IsPressed ? "Pressed" : "Normal", false);
        }
    }
}

Notice that I changed the base class in the code that Visual Studio generated from Control to ContentControl. I needed a custom class here rather than just a template on a Button because these keys do not work like buttons. These keys need to trigger press events when fingers are slid across them to make glissandi. The IsPressed property indicates whether the key is currently pressed or not; the IsPressedChanged event indicates a change in that status.

In the original project, the MainPage.xaml.cpp file instantiated a bunch of Key controls and assembled them into a keyboard resembling a bayan. I decided to move that logic to a separate Keyboard class, so I added a new item to the BayanControlsComponent of type User Control and named it Keyboard. The Keyboard.xaml file simply contains a Canvas for the keys but it's in a Viewbox so it will adapt to the size of the screen:

<UserControl
    x:Class="BayanControlsComponent.Keyboard"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid>
        <Viewbox>
            <Canvas Name="layoutCanvas" />
        </Viewbox>
    </Grid>
</UserControl>

In the earlier program, the number of columns of keys and an optional octave transpose were set by C++ #define statements. The Keyboard class defines these as dependency properties so they can be set in XAML or even by a Style. I've also added a KeyPressedChanged event that indicates when the IsPressedChanged event is fired on any of the child Key controls:

using System;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace BayanControlsComponent
{
    public sealed partial class Keyboard : UserControl
    {
        // 14, 18, 22 are good values
        static readonly DependencyProperty columnCountProperty =
                    DependencyProperty.Register("ColumnCount",
                        typeof(int), typeof(Keyboard),
                        new PropertyMetadata(18, OnPropertyChanged));

        // Set to 1 if using fewer buttons
        static readonly DependencyProperty octaveTransposeProperty =
                    DependencyProperty.Register("OctaveTranspose",
                        typeof(int), typeof(Keyboard),
                        new PropertyMetadata(0, OnPropertyChanged));

        public event EventHandler<KeyArgs> KeyPressedChanged;

        public Keyboard()
        {
            InitializeComponent();
            CreateKeys();
        }

        public static DependencyProperty ColumnCountProperty
        {
            get { return columnCountProperty; }
        }

        public static DependencyProperty OctaveTransposeProperty
        {
            get { return octaveTransposeProperty; }
        }

        public int ColumnCount
        {
            set { SetValue(ColumnCountProperty, value); }
            get { return (int)GetValue(ColumnCountProperty); }
        }

        public int OctaveTranspose
        {
            set { SetValue(OctaveTransposeProperty, value); }
            get { return (int)GetValue(OctaveTransposeProperty); }
        }

        static void OnPropertyChanged(DependencyObject obj,
                                      DependencyPropertyChangedEventArgs args)
        {
            ((Keyboard)obj).CreateKeys();
        }

        void CreateKeys()
        {
            layoutCanvas.Children.Clear();

            int[,] keyNums = new int[5, 4] { { 37, 40, 43, 46 }, 
                                             { 35, 38, 41, 44 },
                                             { 36, 39, 42, 45 },
                                             { 34, 37, 40, 43 },
                                             { 35, 38, 41, 44 } };

            string[] notes = { "C", "C#", "D", "D#", "E", "F", 
                               "F#", "G", "G#", "A", "A#", "B" };

            Brush whiteBrush = new SolidColorBrush(Colors.White);
            Brush blackBrush = new SolidColorBrush(Colors.Black);
            Brush grayBrush = new SolidColorBrush(Color.FromArgb(255, 192, 192, 192));

            layoutCanvas.Width = 100 * this.ColumnCount;
            layoutCanvas.Height = 500;

            for (int row = 0; row < 5; row++)
            {
                bool isEvenRow = (row & 1) == 0;
                int cols = this.ColumnCount - (isEvenRow ? 1 : 0);

                for (int col = 0; col < cols; col++)
                {
                    int keyNum = keyNums[row, col % 4] + 12 * (col / 4 + OctaveTranspose);
                    string note = notes[keyNum % 12];

                    Key key = new Key();
                    key.Tag = row * 1000 + keyNum;
                    key.Content = note;
                    key.Foreground = blackBrush;
                    key.Background = note.Length == 2 ? grayBrush : whiteBrush;
                    key.Width = 90;
                    key.Height = 90;
                    key.FontSize = 48;
                    key.IsPressedChanged += OnKeyIsPressedChanged;

                    Canvas.SetTop(key, row * 100 + 5);
                    Canvas.SetLeft(key, col * 100 + (isEvenRow ? 55 : 5));
                    layoutCanvas.Children.Add(key);
                }
            }
        }

        void OnKeyIsPressedChanged(object sender, bool isPressed)
        {
            KeyArgs keyArgs = new KeyArgs
            {
                KeyNumber = (int)((Key)sender).Tag,
                IsPressed = isPressed
            };

            if (KeyPressedChanged != null)
                KeyPressedChanged(this, keyArgs);
        }
    }
}

The KeyPressedChanged event makes use of a tiny class named KeyArgs:

namespace BayanControlsComponent
{
    public sealed class KeyArgs
    {
        public int KeyNumber { set; get; }

        public bool IsPressed { set; get; }
    }
}

If you're playing along, the BayanControlsComponent project should now build without errors, and it is complete (for now).

The Windows 8.1 Application

Let's now move to the Bayan application project in the Bayan_Windows81 solution. Under that project, right-click References, selection Solution and Projects at the left, and add the two components we've just created. In the MainPage.xaml file, we need a XML namespace declaration for BayanControlsComponent for instantiating the Keyboard control:

<Page
    x:Class="Bayan.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Bayan"
    xmlns:ctrls="using:BayanControlsComponent"
    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}">
        
        <ctrls:Keyboard KeyPressedChanged="OnKeyboardKeyPressedChanged" />

    </Grid>
</Page>

The code-behind file includes two using directives for the two components. It initializes the AudioController to create six oscillators, and calls NoteOn and NoteOff in response to keys being pressed and released:

using Windows.UI.Xaml.Controls;
using BayanAudioComponent;
using BayanControlsComponent;

namespace Bayan
{
    public sealed partial class MainPage : Page
    {
        AudioController audioController;

        public MainPage()
        {
            this.InitializeComponent();

            audioController = new AudioController(6);
        }

        void OnKeyboardKeyPressedChanged(object sender, KeyArgs keyArgs)
        {
            if (keyArgs.IsPressed)
            {
                audioController.NoteOn(keyArgs.KeyNumber, keyArgs.KeyNumber % 1000);
            }
            else
            {
                audioController.NoteOff(keyArgs.KeyNumber);
            }
        }
    }
}

That's the entire program, and here's how it looks on the Windows 8.1 screen (reduced to 1/3 size):

You can play up to six notes at once with fingers, mouse, or pen. In summary, the Bayan_Windows81 solution contains three projects:

And really all I've done so far is refactor code from the original ChromaticButtonKeyboard project and converted some C++ to C#. No biggie.

The Windows Phone 8 Audio Component

Now let's see what's involved in re-using some of this code and markup in a Windows Phone 8 app. Earlier I said I used Visual Studio Express 2012 for Windows Phone to create a solution named Bayan_WindowsPhone8 containing an application project named Bayan. Let's add a new project to this solution. In the Add New Project dialog, on the left select Visual C++ and Windows Phone, and then select the Windows Phone Runtime Component, and give it a name of BayanAudioComponent, the same name I used in the Windows 8 solution. (It's best if the project name matches the namespace name used in the code.)

This BayanAudioComponent project also needs to be linked with the xaudio2.lib import library, so I brought up the project Property Pages dialog, selected Linker and Input on the left, All Configurations and All Platforms on the top, and edited the Additional Dependencies field to add:

xaudio2.lib

In the pch.h file I added:

#include <collection.h>
#include <xaudio2.h>

The collection.h file is required for std::vector and std::map.

Now here's the crucial part. Here's how to share code files between two projects: In the Solution Explorer, right click the BayanAudioComponent project name, and select Add and Existing Item. Navigate to the BayanAudioComponent project in the Bayan_Windows81 solution and select AudioController.cpp, AudioController.h, SawtoothOscillator.cpp, and SawtoothOscillator.h. Do not select anything else! Click Add.

Visual Studio adds these files to the project, but the files themselves are not copied. Instead, links are created to the original files, as you can easily verify by checking the BayanAudioComponent folder in the Bayan_WindowsPhone8 solution. This means that you can make changes to these files in either project, and they will be changed in both projects. If you have both solutions active in two separate instances of Visual Studio, Visual Studio will detect when a file has been changed and saved to disk in the other instance, and ask if you want to reload the new version.

But no changes to these particular files are required: The Window Phone 8 version of BayanAudioComponent can use the same files as the Windows 8.1 version, and the project should build without errors. Hurrah!

The Windows Phone 8 Controls Component

Now let's add another project for the Key and Keyboard controls. This needs to be a C# project, and in the Add New Project dialog, if you select Visual C# and Windows Phone at the left, you won't find a Windows Phone Runtime Component in the template list. But that's OK. Pick the Windows Phone Class Library template instead and give it the same name used earlier of BayanControlsComponent.

If you now try to add a Templated Control item to this project, you'll discover that it's not in the template list, so we need to manually create a Themes folder in the BayanControlsComponent project for the Generic.xaml file that will contain the default control template for the Key class.

It would be very desirable to have a link to the Generic.xaml file in the Windows 8 version of BayanControlsComponent , but that's not going to be possible. The Generic.xaml file needs to reference the Key class in the BayanControlsComponent namespace, and the XAML syntax for associating an XML namespace with a code namespace is different in Windows 8 and Windows Phone 8. In Windows 8 it looks like this:

xmlns:local="using:BayanControlsComponent"

In Windows Phone 8, it must look like this:

xmlns:local="clr-namespace:BayanControlsComponent"

Don't blame me! I'm just the messenger here. And don't fret, either. There is a work-around that will allow us at least to share the markup for the control template, which is the bulk of this XAML file.

Back in the Windows 8.1 Controls Component

Let's go back into the Bayan_Windows81 solution, and in the BayanControlsComponent project create another file in the Themes namespace named Common.xaml. Move the template into that file:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ControlTemplate x:Key="KeyControlTemplate">
        <Grid>
            <Ellipse Name="ellipse"
                     Fill="{TemplateBinding Background}"
                     Stroke="{Binding RelativeSource={RelativeSource TemplatedParent}, 
                                      Path=Foreground}"
                     StrokeThickness="3" />
            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />

            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal" />
                    <VisualState x:Name="Pressed">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
                                                           Storyboard.TargetProperty="Fill">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="Lime" />
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        </Grid>
    </ControlTemplate>
</ResourceDictionary>

This Common.xaml file contains only the template. It doesn't need to reference the Key class in the ControlTemplate tag, but it does need a dictionary x:Key. (Sorry for the similarity in my control name and a XAML keyword!) Now the Generic.xaml file can reference the Common.xaml file and the template:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BayanControlsComponent">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/BayanControlsComponent/Themes/Common.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="local:Key">
        <Setter Property="Template" Value="{StaticResource KeyControlTemplate}" />
    </Style>
</ResourceDictionary>

Return to the Windows Phone 8 Controls Component

Now let's return to the Bayan_WindowsPhone8 solution. We want to share the Common.xaml file but not the Generic.xaml file. Right-click the Themes folder in the Windows Phone 8 version of BayanControlsComponent, and select Add Existing Component. Navigate to the Themes folder in the Windows 8 solution, and select Common.xaml only, and be sure to select "Add As Link" from the Add button dropdown. With C# projects, the file linking is optional.

Now we need to add a unique Generic.xaml file to the Themes folder in the Windows Phone 8 project, and perhaps the easist way is to make a copy of the earlier file and then edit it. Right-click the Themes folder again, select Add Existing Component, and this time select Generic.xaml, but use "Add" from the dropdown to make a copy. Two changes need to be made: "using" replaced with "clr-namespace" and a ";component" added to the Source URL on ResourceDictionary:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BayanControlsComponent">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/BayanControlsComponent;component/Themes/Common.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="local:Key">
        <Setter Property="Template" Value="{StaticResource KeyControlTemplate}" />
    </Style>
</ResourceDictionary>

What we've done is share the markup for the control template, but provide unique Style definitions for the two environments. This is likely to be necessary anyway, because often the Style refers to predefined resources that are be different in Windows 8 and Windows Phone 8.

Still in the BayanControlsComponent project of Bayan_WindowsPhone8, we want to add links to the four files for the Key, KeyArgs and Keyboard classes. Right-click the project name, select Add and Existing Item, navigate to the BayanControlsComponent directory in the Bayan_Windows81 solution, select the four files, and "Add As Link."

The KeyArgs.cs file is fine; the Keyboard.xaml file is fine, but the two other C# files have problems. The most obvious is that namespace names are different between Windows 8 and Windows Phone 8, but that's easy to fix. If you right-click the BayanControlsComponent project in the two solutions, and select Properties and then Build, you'll discover in the Conditional Compilation Symbols field that the symbol NETFX_CORE is defined for Windows 8 code and SILVERLIGHT and WINDOWS_PHONE are defined for Windows Phone 8 code, so Keyboard.xaml.cs is fixed with preprocessor #if directives and using directives like this:

using System;

#if NETFX_CORE

using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

#endif

#if WINDOWS_PHONE

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

#endif

The Key class has a more serious problem. It uses Windows Runtime Pointer events that don't exist in Windows Phone. In the Windows Phone app we need the Touch.FrameReported event to get something roughly equivalent, so the Key.cs file becomes this ugly mess:

using System;
using System.Collections.Generic;

#if NETFX_CORE

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

#endif

#if WINDOWS_PHONE

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

#endif

namespace BayanControlsComponent
{
    public sealed class Key : ContentControl
    {
        static readonly DependencyProperty isPressedProperty =
                    DependencyProperty.Register("IsPressed",
                        typeof(bool), typeof(Key),
                        new PropertyMetadata(false, OnIsPressedChanged));

        public event EventHandler<bool> IsPressedChanged;

        List<uint> pointerList = new List<uint>();

        public Key()
        {
            this.DefaultStyleKey = typeof(Key);

#if WINDOWS_PHONE

            this.Loaded += (sender, args) =>
                {
                    Touch.FrameReported += OnTouchFrameReported;
                };

            this.Unloaded += (sender, args) =>
                {
                    Touch.FrameReported -= OnTouchFrameReported;
                };

#endif
        }

        public static DependencyProperty IsPressedProperty
        {
            get { return isPressedProperty; }
        }

        public bool IsPressed
        {
            set { SetValue(IsPressedProperty, value); }
            get { return (bool)GetValue(IsPressedProperty); }
        }

#if WINDOWS_PHONE

        void OnTouchFrameReported(object sender, TouchFrameEventArgs args)
        {
            TouchPointCollection touchPoints = args.GetTouchPoints(this);

            foreach (TouchPoint touchPoint in touchPoints)
            {
                uint id = (uint)touchPoint.TouchDevice.Id;
                bool overThis = IsOverThis(touchPoint.TouchDevice.DirectlyOver);

                switch (touchPoint.Action)
                {
                    case TouchAction.Down:
                        if (overThis)
                            AddToList(id);

                        break;

                    case TouchAction.Move:
                        if (overThis)
                            AddToList(id);
                        else
                            RemoveFromList(id);

                        break;

                    case TouchAction.Up:
                        if (overThis)
                            RemoveFromList(id);

                        break;
                }
            }
        }

        bool IsOverThis(UIElement element)
        {
            if (element == this)
                return true;

            element = VisualTreeHelper.GetParent(element) as UIElement;

            if (element == null)
                return false;

            return IsOverThis(element);
        }

#endif
        
#if NETFX_CORE

        protected override void OnPointerEntered(PointerRoutedEventArgs args)
        {
            if (args.Pointer.IsInContact)
                AddToList(args.Pointer.PointerId);

            base.OnPointerEntered(args);
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs args)
        {
            AddToList(args.Pointer.PointerId);
            base.OnPointerPressed(args);
        }

        protected override void OnPointerReleased(PointerRoutedEventArgs args)
        {
            RemoveFromList(args.Pointer.PointerId);
            base.OnPointerReleased(args);
        }

        protected override void OnPointerExited(PointerRoutedEventArgs args)
        {
            RemoveFromList(args.Pointer.PointerId);
            base.OnPointerExited(args);
        }

#endif

        void AddToList(uint id)
        {
            if (!pointerList.Contains(id))
                pointerList.Add(id);

            CheckList();
        }

        void RemoveFromList(uint id)
        {
            if (pointerList.Contains(id))
                pointerList.Remove(id);

            CheckList();
        }

        void CheckList()
        {
            this.IsPressed = pointerList.Count > 0;
        }

        static void OnIsPressedChanged(DependencyObject obj,
                                       DependencyPropertyChangedEventArgs args)
        {
            Key key = (Key)obj;

            if (key.IsPressedChanged != null)
                key.IsPressedChanged(key, key.IsPressed);

            VisualStateManager.GoToState(key, key.IsPressed ? "Pressed" : "Normal", false);
        }
    }
}

An ugly mess, yes. But what are you going to do? Demand that Microsoft make the APIs for Windows 8 and Windows Phone 8 identical? Listen to me: After coding for Microsoft operating systems for 30 years, the only way I've retained my sanity is by serenely accepting the things I cannot change.

Notice that the Windows Phone 8 version of Key attaches a handler for the Touch.FrameReported event when the control is loaded, and detaches the handler when the control is unloaded. This code is necessary because the Keyboard class recreates Key controls when the ColumnCount property changes, and you don't want a Key control processing Touch.FrameReported events when it's not actually on the visual tree.

The Windows Phone 8 Application

The good news is that the Windows Phone version of BayanControlsComponent now builds, which means we can now focus on the Bayan application project. Right-click References in that project and add references to the two other projects.

The MainPage.xaml file instantiates the Keyboard class but notice I've made use of the two properties to format it a little better for the tiny screen:

<phone:PhoneApplicationPage
    x:Class="Bayan.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:ctrls="clr-namespace:BayanControlsComponent;assembly=BayanControlsComponent"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Landscape" Orientation="Landscape"
    shell:SystemTray.IsVisible="True">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="BAYAN" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
        </StackPanel>

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

            <ctrls:Keyboard KeyPressedChanged="OnKeyboardKeyChanged"
                            ColumnCount="10"
                            OctaveTranspose="1" />
        </Grid>
    </Grid>
</phone:PhoneApplicationPage>

The code-behind file handles the KeyPressedChanged event the same as the Windows 8 version, and calls the Start and Stop methods I added to AudioController:

using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using BayanAudioComponent;
using BayanControlsComponent;

namespace Bayan
{
    public partial class MainPage : PhoneApplicationPage
    {
        AudioController audioController;

        public MainPage()
        {
            InitializeComponent();
            audioController = new AudioController(6);
        }

        protected override void OnNavigatedFrom(NavigationEventArgs args)
        {
            audioController.Stop();
            base.OnNavigatedFrom(args);
        }

        protected override void OnNavigatedTo(NavigationEventArgs args)
        {
            audioController.Start();
            base.OnNavigatedTo(args);
        }

        void OnKeyboardKeyChanged(object sender, KeyArgs keyArgs)
        {
            if (keyArgs.IsPressed)
            {
                audioController.NoteOn(keyArgs.KeyNumber, keyArgs.KeyNumber % 1000);
            }
            else
            {
                audioController.NoteOff(keyArgs.KeyNumber);
            }
        }
    }
}

Here's the Bayan program running on a Windows Phone 8 device (reduced to 1/3 size):

I'm not sure, but the Bayan program running on a Windows Phone may very well be the smallest bayan ever! Here are the two Visual Studio solutions constructed exactly as I've described.


Comments:

A couple corrections:

First, I took the Windows Phone 8 documentation at its word when it indicated that Direct2D, DirectWrite, and WIC aren't supported. My MSDN Magazine colleague Kenny Kerr has demonstrated that this is not the case in a blog entry with source code that nails it.

Secondly, Kenny's source code is written entirely in C++, which contradicts my assertion that you'll need to write your Windows Phone 8 apps in C# or VB. Obviously I assumed that if you could write a Win Phone entirely in C++, Visual Studio would allow that option! Silly me.

— Charles

The Key Control Revisited

As I was playing around with the Bayan program on Windows Phone, it was very evident to me that response was sluggish. Of course! Each of the Key instances handles the Touch.FrameReported event and searches for a match with itself. If many controls are involved, the Touch.FrameReported event should really be handled only once. A much better approach would be to attach a handler to the event in a static constructor of Key.

I also realized the Key is only used within the BayanControlsComponent library so it need not be public. This meant that I could use a somewhat different approach to sharing code between a Windows 8 and Windows Phone 8 application by defining a shared KeyBase class and separate Key classes for each environment. This approach avoids the big blocks of code separated by #if directives.

The KeyBase and new Key classes are implemented in the two solutions in the BetterBayan.zip file.

— Charles

a Google search sent me to this beautiful blog entry ... your ability to simplify difficult subjects is amazing. you do not know how much you helped.

thank you.

— Eric, Mon, 7 Oct 2013 03:56:33 -0400


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

(c) Copyright Charles Petzold
www.charlespetzold.com