/* * PhoneGap is available under *either* the terms of the modified BSD license *or* the * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. * * Copyright (c) 2005-2011, Nitobi Software Inc. * Copyright (c) 2011, Microsoft Corporation * Copyright (c) 2011, Sergey Grebnov. */ using System; using System.IO; using System.IO.IsolatedStorage; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Media; namespace WP7GapClassLib.PhoneGap.Commands { /// /// Implements audio record and play back functionality. /// internal class AudioPlayer: IDisposable { #region Constants // AudioPlayer states private const int MediaNone = 0; private const int MediaStarting = 1; private const int MediaRunning = 2; private const int MediaPaused = 3; private const int MediaStopped = 4; // AudioPlayer messages private const int MediaState = 1; private const int MediaDuration = 2; private const int MediaPosition = 3; private const int MediaError = 9; // AudioPlayer errors private const int MediaErrorPlayModeSet = 1; private const int MediaErrorAlreadyRecording = 2; private const int MediaErrorStartingRecording = 3; private const int MediaErrorRecordModeSet = 4; private const int MediaErrorStartingPlayback = 5; private const int MediaErrorResumeState = 6; private const int MediaErrorPauseState = 7; private const int MediaErrorStopState = 8; private const string CallbackFunction = "PhoneGapMediaonStatus"; #endregion /// /// The AudioHandler object /// private Media handler; /// /// Temporary buffer to store audio chunk /// private byte[] buffer; /// /// Xna game loop dispatcher /// DispatcherTimer dtXna; /// /// Output buffer /// private MemoryStream memoryStream; /// /// The id of this player (used to identify Media object in JavaScript) /// private String id; /// /// State of recording or playback /// private int state = MediaNone; /// /// File name to play or record to /// private String audioFile = null; /// /// Duration of audio /// private double duration = -1; /// /// Audio player object /// private MediaElement player = null; /// /// Audio source /// private Microphone recorder; /// /// Internal flag specified that we should only open audio w/o playing it /// private bool prepareOnly = false; /// /// Creates AudioPlayer instance /// /// Media object /// player id public AudioPlayer(Media handler, String id) { this.handler = handler; this.id = id; } /// /// Destroys player and stop audio playing or recording /// public void Dispose() { if (this.player != null) { this.stopPlaying(); this.player = null; } if (this.recorder != null) { this.stopRecording(); this.recorder = null; } this.FinalizeXnaGameLoop(); } /// /// Starts recording, data is stored in memory /// /// public void startRecording(string filePath) { if (this.player != null) { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorPlayModeSet)); } else if (this.recorder == null) { try { this.audioFile = filePath; this.InitializeXnaGameLoop(); this.recorder = Microphone.Default; this.recorder.BufferDuration = TimeSpan.FromMilliseconds(500); this.buffer = new byte[recorder.GetSampleSizeInBytes(this.recorder.BufferDuration)]; this.recorder.BufferReady += new EventHandler(recorderBufferReady); this.memoryStream = new MemoryStream(); this.WriteWavHeader(this.memoryStream, this.recorder.SampleRate); this.recorder.Start(); FrameworkDispatcher.Update(); this.SetState(MediaRunning); } catch (Exception e) { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingRecording)); } } else { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorAlreadyRecording)); } } /// /// Stops recording /// public void stopRecording() { if (this.recorder != null) { if (this.state == MediaRunning) { try { this.recorder.Stop(); this.recorder.BufferReady -= recorderBufferReady; this.recorder = null; SaveAudioClipToLocalStorage(); this.FinalizeXnaGameLoop(); this.SetState(MediaStopped); } catch (Exception e) { //TODO } } } } /// /// Starts or resume playing audio file /// /// The name of the audio file /// /// Starts or resume playing audio file /// /// The name of the audio file public void startPlaying(string filePath) { if (this.recorder != null) { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorRecordModeSet)); return; } if ((this.player == null) || (this.state == MediaStopped)) { try { if (this.player == null) { if (!Application.Current.Resources.Contains("PhoneGapMediaPlayer")) { throw new Exception("PhoneGapMediaPlayer wasn't found in application resources"); } this.player = Application.Current.Resources["PhoneGapMediaPlayer"] as MediaElement; this.player.MediaOpened += MediaOpened; this.player.MediaEnded += MediaEnded; this.player.MediaFailed += MediaFailed; } this.audioFile = filePath; this.player.AutoPlay = false; Uri uri = new Uri(filePath, UriKind.RelativeOrAbsolute); if (uri.IsAbsoluteUri) { this.player.Source = uri; } else { using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) { if (isoFile.FileExists(filePath)) { using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, isoFile)) { this.player.SetSource(stream); } } else { throw new ArgumentException("Source doesn't exist"); } } } this.SetState(MediaStarting); } catch (Exception e) { string s = e.Message; this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingPlayback)); } } else { if ((this.state == MediaPaused) || (this.state == MediaStarting)) { this.player.Play(); this.SetState(MediaRunning); } else { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorResumeState)); } } } /// /// Callback to be invoked when the media source is ready for playback /// private void MediaOpened(object sender, RoutedEventArgs arg) { if (!this.prepareOnly) { this.player.Play(); this.SetState(MediaRunning); } this.duration = this.player.NaturalDuration.TimeSpan.TotalSeconds; this.prepareOnly = false; this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaDuration, this.duration)); } /// /// Callback to be invoked when playback of a media source has completed /// private void MediaEnded(object sender, RoutedEventArgs arg) { this.SetState(MediaStopped); } /// /// Callback to be invoked when playback of a media source has failed /// private void MediaFailed(object sender, RoutedEventArgs arg) { player.Stop(); this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError.ToString(), "Media failed")); } /// /// Seek or jump to a new time in the track /// /// The new track position public void seekToPlaying(int milliseconds) { if (this.player != null) { TimeSpan timeSpen = new TimeSpan(0, 0, 0, 0, milliseconds); this.player.Position = timeSpen; this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, milliseconds / 1000.0f)); } } /// /// Pauses playing /// public void pausePlaying() { if (this.state == MediaRunning) { this.player.Pause(); this.SetState(MediaPaused); } else { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorPauseState)); } } /// /// Stops playing the audio file /// public void stopPlaying() { if ((this.state == MediaRunning) || (this.state == MediaPaused)) { this.player.Stop(); this.SetState(MediaStopped); } else { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStopState)); } } /// /// Gets current position of playback /// /// current position public double getCurrentPosition() { if ((this.state == MediaRunning) || (this.state == MediaPaused)) { double currentPosition = this.player.Position.TotalSeconds; this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, currentPosition)); return currentPosition; } else { return -1; } } /// /// Gets the duration of the audio file /// /// The name of the audio file /// track duration public double getDuration(string filePath) { if (this.recorder != null) { return (-2); } if (this.player != null) { return this.duration; } else { this.prepareOnly = true; this.startPlaying(filePath); return this.duration; } } /// /// Sets the state and send it to JavaScript /// /// state private void SetState(int state) { if (this.state != state) { this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaState, state)); } this.state = state; } #region record methods /// /// Copies data from recorder to memory storages and updates recording state /// /// /// private void recorderBufferReady(object sender, EventArgs e) { this.recorder.GetData(this.buffer); this.memoryStream.Write(this.buffer, 0, this.buffer.Length); } /// /// Writes audio data from memory to isolated storage /// /// private void SaveAudioClipToLocalStorage() { if (this.memoryStream == null || this.memoryStream.Length <= 0) { return; } this.UpdateWavHeader(this.memoryStream); try { using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) { string directory = Path.GetDirectoryName(audioFile); if (!isoFile.DirectoryExists(directory)) { isoFile.CreateDirectory(directory); } this.memoryStream.Seek(0, SeekOrigin.Begin); using (IsolatedStorageFileStream fileStream = isoFile.CreateFile(audioFile)) { this.memoryStream.CopyTo(fileStream); } } } catch (Exception e) { //TODO: log or do something else throw; } } #region Wav format // Original source http://damianblog.com/2011/02/07/storing-wp7-recorded-audio-as-wav-format-streams/ /// /// Adds wav file format header to the stream /// https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ /// /// Wav stream /// Sample Rate private void WriteWavHeader(Stream stream, int sampleRate) { const int bitsPerSample = 16; const int bytesPerSample = bitsPerSample / 8; var encoding = System.Text.Encoding.UTF8; // ChunkID Contains the letters "RIFF" in ASCII form (0x52494646 big-endian form). stream.Write(encoding.GetBytes("RIFF"), 0, 4); // NOTE this will be filled in later stream.Write(BitConverter.GetBytes(0), 0, 4); // Format Contains the letters "WAVE"(0x57415645 big-endian form). stream.Write(encoding.GetBytes("WAVE"), 0, 4); // Subchunk1ID Contains the letters "fmt " (0x666d7420 big-endian form). stream.Write(encoding.GetBytes("fmt "), 0, 4); // Subchunk1Size 16 for PCM. This is the size of therest of the Subchunk which follows this number. stream.Write(BitConverter.GetBytes(16), 0, 4); // AudioFormat PCM = 1 (i.e. Linear quantization) Values other than 1 indicate some form of compression. stream.Write(BitConverter.GetBytes((short)1), 0, 2); // NumChannels Mono = 1, Stereo = 2, etc. stream.Write(BitConverter.GetBytes((short)1), 0, 2); // SampleRate 8000, 44100, etc. stream.Write(BitConverter.GetBytes(sampleRate), 0, 4); // ByteRate = SampleRate * NumChannels * BitsPerSample/8 stream.Write(BitConverter.GetBytes(sampleRate * bytesPerSample), 0, 4); // BlockAlign NumChannels * BitsPerSample/8 The number of bytes for one sample including all channels. stream.Write(BitConverter.GetBytes((short)(bytesPerSample)), 0, 2); // BitsPerSample 8 bits = 8, 16 bits = 16, etc. stream.Write(BitConverter.GetBytes((short)(bitsPerSample)), 0, 2); // Subchunk2ID Contains the letters "data" (0x64617461 big-endian form). stream.Write(encoding.GetBytes("data"), 0, 4); // NOTE to be filled in later stream.Write(BitConverter.GetBytes(0), 0, 4); } /// /// Updates wav file format header /// https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ /// /// Wav stream private void UpdateWavHeader(Stream stream) { if (!stream.CanSeek) throw new Exception("Can't seek stream to update wav header"); var oldPos = stream.Position; // ChunkSize 36 + SubChunk2Size stream.Seek(4, SeekOrigin.Begin); stream.Write(BitConverter.GetBytes((int)stream.Length - 8), 0, 4); // Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8 This is the number of bytes in the data. stream.Seek(40, SeekOrigin.Begin); stream.Write(BitConverter.GetBytes((int)stream.Length - 44), 0, 4); stream.Seek(oldPos, SeekOrigin.Begin); } #endregion #region Xna loop /// /// Special initialization required for the microphone: XNA game loop /// private void InitializeXnaGameLoop() { // Timer to simulate the XNA game loop (Microphone is from XNA) this.dtXna = new DispatcherTimer(); this.dtXna.Interval = TimeSpan.FromMilliseconds(33); this.dtXna.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } }; this.dtXna.Start(); } /// /// Finalizes XNA game loop for microphone /// private void FinalizeXnaGameLoop() { // Timer to simulate the XNA game loop (Microphone is from XNA) this.dtXna.Stop(); this.dtXna = null; } #endregion #endregion } }