Trombone is a much more sophisticated musical instrument app than the preceding chapter’s Cowbell app. You can move the slide up and down to different positions to play any note. (Other than starting at F, the slide positions bear little resemblance to real trombone slide positions!) This app supports two different sliding modes. If you use the left side of the screen, you can freely move the slide. If you use the right side of the screen, the slide snaps to the closest note line. Besides being an easier way to play this instrument, this means you could also use this app as a pitch pipe.
This trombone can play its notes in two octaves; to raise the sound by an octave, place a second finger anywhere on the screen. The most fun part about this app is that, like with a real trombone, you must actually blow on your phone to produce sound!
These last two app features require phone features discussed in later chapters (multi-touch and using the microphone) so that portion of the code is not explained in this chapter. Instead, the focus is on manipulating a single sound effect’s pitch and duration to create all the audio needed by this app.
The User Interface
The main page, pictured in Figure 31.1 in its initial state, contains the moveable trombone slide, note guide lines, and buttons that link to the other two pages. Listing 31.1 contains the XAML.
LISTING 31.1 MainPage.xaml—The User Interface for Trombone’s Main Page
[code]
<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.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”
SupportedOrientations=”Portrait”>
<Canvas x:Name=”LayoutRoot”>
<!– The stationary inner slide –>
<Image Canvas.Left=”72” Source=”Images/innerSlide.png”/>
<!– The moveable outer slide –>
<Image x:Name=”SlideImage” Canvas.Left=”72” Canvas.ZIndex=”1”
Source=”Images/outerSlide.png”/>
<!– An instructions button –>
<Rectangle Canvas.Left=”18” Canvas.Top=”30” Canvas.ZIndex=”1”
Width=”48” Height=”48” Fill=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”InstructionsButton_Click”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”/Shared/Images/normal.instructions.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– A settings button –>
<Rectangle Canvas.Left=”18” Canvas.Top=”94” Canvas.ZIndex=”1”
Width=”48” Height=”48” Fill=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”SettingsButton_Click”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”/Shared/Images/normal.settings.png”/>
</Rectangle.OpacityMask>
</Rectangle>
</Canvas>
</phone:PhoneApplicationPage>
[/code]
- The note guide lines pictured in Figure 31.1 are added in this page’s code-behind.
- An application bar would get in the way of this user interface, so two rectangles acting as buttons are used instead. They use the familiar opacity mask trick to ensure they appear as expected for any theme.
- The trombone slide consists of two images, one on top of the other. These two images are shown in Figure 31.2.
The Code-Behind
Listing 31.2 contains the code-behind for the main page.
LISTING 31.2 MainPage.xaml.cs—The Code-Behind for Trombone’s Main Page
[code]
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Resources;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio; // For SoundEffect
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The single sound effect instance
SoundEffectInstance soundEffectInstance;
string[] notes = { “G ”, “G”, “A ”, “A”, “B ”, “B”,
“C”, “D ”, “D”, “E ”, “E”, “F” };
// The relative distance of each note’s pitch,
// where 0 is the initial F and -1 is one octave lower
double[] pitches = { -.9 /*G */, -.82 /*G*/, -.75 /*A */, -.68 /*A*/,
-.6 /*B */, -.5 /*B*/, -.4 /*C*/, -.35 /*D */,
-.25 /*D*/, -.18 /*E */, -.08 /*E*/, 0 /*F*/ };
// For microphone processing
byte[] buffer;
int currentVolume;
// For several calculations
const int TOP_NOTE_POSITION = 20;
const int BOTTOM_NOTE_POSITION = 780;
const int OCTAVE_RANGE = 844;
public MainPage()
{
InitializeComponent();
// Load the single sound file used by this app: the sound of F
StreamResourceInfo info = App.GetResourceStream(
new Uri(“Audio/F.wav”, UriKind.Relative));
SoundEffect effect = SoundEffect.FromStream(info.Stream);
// Enables manipulation of the sound effect while it plays
this.soundEffectInstance = effect.CreateInstance();
// The source .wav file has a loop region, so exploit it
this.soundEffectInstance.IsLooped = true;
// Add each of the note guide lines
for (int i = 0; i < this.pitches.Length; i++)
{
double position = BOTTOM_NOTE_POSITION + this.pitches[i] * OCTAVE_RANGE;
// Add a line at the right position
Line line = new Line { X2 = 410,
Stroke = Application.Current.Resources[“PhoneAccentBrush”] as Brush,
StrokeThickness = 5, Opacity = .8 };
Canvas.SetTop(line, position);
this.LayoutRoot.Children.Add(line);
// Add the note label next to the line
TextBlock label = new TextBlock {
Text = this.notes[i][0].ToString(), // Ignore the , use 0th char only
Foreground = Application.Current.Resources[“PhoneAccentBrush”] as Brush,
FontSize = 40 };
Canvas.SetLeft(label, line.X2 + 12);
Canvas.SetTop(label, position – 20);
this.LayoutRoot.Children.Add(label);
// Add the separately, simulating a superscript so it looks better
if (this.notes[i].EndsWith(“”))
{
TextBlock flat = new TextBlock { Text = “”, FontSize = 25,
FontWeight = FontWeights.Bold, Foreground =
Application.Current.Resources[“PhoneAccentBrush”] as Brush };
Canvas.SetLeft(flat, line.X2 + label.ActualWidth + 6);
Canvas.SetTop(flat, position – 21);
this.LayoutRoot.Children.Add(flat);
}
}
// Configure the microphone
Microphone.Default.BufferDuration = TimeSpan.FromSeconds(.1);
Microphone.Default.BufferReady += Microphone_BufferReady;
// Initialize the buffer for holding microphone data
int size = Microphone.Default.GetSampleSizeInBytes(
Microphone.Default.BufferDuration);
buffer = new byte[size];
// Start listening
Microphone.Default.Start();
CompositionTarget.Rendering += delegate(object sender, EventArgs e)
{
// Required for XNA Sound Effect API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
// Play the sound whenever the blowing into the microphone is loud enough
if (this.currentVolume > Settings.VolumeThreshold.Value)
{
if (soundEffectInstance.State != SoundState.Playing)
soundEffectInstance.Play();
}
else if (soundEffectInstance.State == SoundState.Playing)
{
// Rather than stopping immediately, the “false” makes the sound break
// out of the loop region and play the remainder
soundEffectInstance.Stop(false);
}
};
// Call also once at the beginning
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint touchPoint = e.GetPrimaryTouchPoint(this);
if (touchPoint != null)
{
// Get the Y position of the primary finger
double position = touchPoint.Position.Y;
// If the finger is on the right side of the screen, snap to the
// closest note
if (touchPoint.Position.X > this.ActualWidth / 2)
{
// Search for the current offset, expressed as a negative value from
// 0-1, in the pitches array.
double percentage = (-BOTTOM_NOTE_POSITION + position) / OCTAVE_RANGE;
int index = Array.BinarySearch<double>(this.pitches, percentage);
if (index < 0)
{
// An exact match wasn’t found (which should almost always be the
// case), so BinarySearch has returned a negative number that is the
// bitwise complement of the index of the next value that is larger
// than percentage (or the array length if there’s no larger value).
index = ~index;
if (index < this.pitches.Length)
{
// Don’t always use the index of the larger value. Also check the
// closest smallest value (if there is one) and snap to it instead
// if it’s closer to the current value.
if (index > 0 &&
Math.Abs(percentage – this.pitches[index]) >
Math.Abs(percentage – this.pitches[index – 1]))
index–;
// Snap the position to the new location, expressed in pixels
position = BOTTOM_NOTE_POSITION +
this.pitches[index] * OCTAVE_RANGE;
}
}
}
// Place the outer slide to match the finger position or snapped position
Canvas.SetTop(this.SlideImage, position – this.ActualHeight – 40);
// See how many fingers are in contact with the screen
int numPoints =
(from p in e.GetTouchPoints(this)
where
p.Action != TouchAction.Up
select p).Count();
// 1 represents one octave higher (-1 represents one octave lower)
int startingPitch = (numPoints > 1) ? 1 : 0;
// Express the position as a delta from the bottom position, and
// clamp it to the valid range. This gives a little margin on both
// ends of the screen because it can be difficult for the user to move
// the slide all the way to either end.
double offset = BOTTOM_NOTE_POSITION –
Math.Max(TOP_NOTE_POSITION, Math.Min(BOTTOM_NOTE_POSITION,
touchPoint.Position.Y));
// Whether it’s currently playing or not, change the sound’s pitch based
// on the current slide position and whether the octave has been raised
this.soundEffectInstance.Pitch =
(float)(startingPitch – (offset / OCTAVE_RANGE));
}
}
void Microphone_BufferReady(object sender, EventArgs e)
{
int size = Microphone.Default.GetData(buffer);
if (size > 0)
this.currentVolume = GetAverageVolume(size);
}
// Returns the average value among all the values in the buffer
int GetAverageVolume(int numBytes)
{
long total = 0;
// Although buffer is an array of bytes, we want to examine each
// 2-byte value.
// [SampleDuration for 1 sec (32000) / SampleRate (16000) = 2 bytes]
// Therefore, we iterate through the array 2 bytes at a time.
for (int i = 0; i < numBytes; i += 2)
{
// Cast from short to int to prevent -32768 from overflowing Math.Abs:
int value = Math.Abs((int)BitConverter.ToInt16(buffer, i));
total += value;
}
return (int)(total / (numBytes / 2));
}
// Button handlers
void SettingsButton_Click(object sender, MouseButtonEventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/SettingsPage.xaml”, UriKind.Relative));
}
void InstructionsButton_Click(object sender, MouseButtonEventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
}
}
[/code]
- The single sound file used by this app is a recording of an F being played on a trombone. The different notes are created by dynamically altering the pitch of the F as it plays.
- Rather than directly use the SoundEffect object, as in the preceding chapter, this app calls its CreateInstance method to get a SoundEffectInstance object. SoundEffectInstance provides a few more features compared to SoundEffect and, because it is tied to a single instance of the sound, it enables manipulation of the sound after it has already started to play. Trombone requires SoundEffectInstance for its looping behavior and its ability to modify the pitch of an already-playing sound.
- SoundEffectInstance exposes an IsLooped property (false by default) that enables you to loop the audio indefinitely until Stop is called. This can behave in one of two ways, depending on the source audio file:
- For a plain audio file, the looping applies to the entire duration, so the sound will seamlessly restart from the beginning each time it reaches the end.
- For an audio file with a loop region, the sound will play from the beginning the first time through, but then only the loop region will loop indefinitely. Calling the default overload of Stop stops the sound immediately, but calling an overload and passing false for its immediate parameter finishes the current iteration of the loop, and then breaks out of the loop and plays the remainder of the sound.
Figure 31.3 demonstrates these two different behaviors. The latter behavior is perfect for this app, because it enables a realisticsounding trombone note of any length, complete with a beginning and end that makes a smooth transition to and from silence. Therefore, the F.wav sound file included with this app defines a loop region. Although the sound file is less than a third of a second long, the loop region enables it to last for as long as the user can sustain his or her blowing.
Be careful about the length of your loop region!
If you don’t want to stop a sound immediately, but want it to gracefully stop fairly quickly with Stop(false) as with this app’s sound effect, your loop region (and remainder of the sound) must be very short.Otherwise, the process of finishing the current loop iteration could be too time-consuming.
Wavosaur (www.wavosaur.com) is a free and very powerful sound editor that enables you to create a loop region inside a .wav file. Simply highlight a region of the sound; then select Tools, Loop, Create loop points.The exported .wav file will still play straight through under normal circumstances, but playing it with SoundEffectInstance and IsLooped set to true will leverage your custom loop region.
SoundEffect versus SoundEffectInstance
Whereas SoundEffect only enables you to play sounds, SoundEffectInstance enables you to pause/resume/stop the particular sound instance with its Pause, Resume, and Stop methods. Whereas each call to SoundEffect’s Play method starts playing a fresh instance of the sound that can’t be stopped (and may overlap sounds from earlier calls), a call to SoundEffectInstance’s Play method does nothing if that instance of the sound is currently playing. Because SoundEffectInstance is tied to a specific sound instance, it is also able to expose a State property that reveals whether the sound is playing, paused, or stopped. In addition to the IsLooped property, SoundEffectInstance exposes three properties for controlling the resulting sound.These can be set at any time, even in the middle of playback:
- Volume (default=1)—A value from 0 (muted) to 1 (full volume).
- Pitch (default=0)—A value from –1 (one octave lower) to 1 (one octave higher). A value of 0 plays the sound at its natural pitch.
- Pan (default=0)—A value from –1 (all the way to the left speaker) to 1 (all the way to the right speaker). A value of 0 centers the sound.
SoundEffect also enables controlling these three values with a Play overload that accepts volume, pitch, and pan parameters.However, these values always apply for the duration of the sound. (SoundEffect’s parameterless Play method, used in the preceding chapter, uses a volume of 1 and a pitch and pan of 0.)
SoundEffectInstance also exposes two overloads of an Apply3D method that enables you to apply 3D positioning to the sound playback.This feature is most interesting for Xbox and PC games. For phones, 3D positioning (and even custom pan values) is likely to be overkill.
Regular Sound File
Sound File with a Looping Region
FIGURE 31.3 Options for looping with SoundEffectInstance.IsLooped set to true.
- In the CompositionTarget.Rendering event handler, the current volume from the microphone is continually compared against a threshold setting (adjustable on the settings page). If it’s loud enough and the sound isn’t already playing, Play is called. (The State check isn’t strictly necessary because, unlike SoundEffect.Play, SoundEffectInstance.Play does nothing if the sound is already playing.) If the sound is playing and the volume is no longer loud enough, then Stop(false) is called to break out of the loop and play the end of the sound.
- Inside Touch_FrameReported, which detects where the primary finger is in contact with the screen and whether a second finger is touching the screen (as discussed in Part VII, “Touch and Multi- Touch”), the sound’s pitch is adjusted. The startingPitch variable tracks which octave the base F note is in (0 for the natural octave or 1 for an octave higher); then the distance that the finger is from the bottom of the screen determines how much lower the pitch is adjusted. As you can see from the values in the pitches array at the beginning of the listing, a D is produced by lowering the pitch of the F by 25% (producing a value of -.25 or .75 depending on the octave), and a B is produced by lowering the pitch of the F by half (producing a value of -.5 or .5 depending on the octave).
Can I make my audio heard when the phone’s master volume is muted, or can I play audio louder than the master volume level?
No, the user is empowered to choose the maximum volume level for any sound that could be made by their phone.The 0–1 volume level of a sound effect is relative to the master volume.Note that SoundEffect exposes a static MasterVolume property that enables you to simultaneously adjust the volume of all your sounds (whether played from SoundEffect or SoundEffectInstance), but this does not enable you to get any louder than the user’s chosen volume level.
The Finished Product