/* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 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; using Microsoft.Phone.Controls; using System.Diagnostics; using System.Windows.Resources; namespace WPCordovaClassLib.Cordova.Commands { /// /// Implements audio record and play back functionality. /// internal class AudioPlayer : IDisposable { #region Constants // AudioPlayer states private const int PlayerState_None = 0; private const int PlayerState_Starting = 1; private const int PlayerState_Running = 2; private const int PlayerState_Paused = 3; private const int PlayerState_Stopped = 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; //TODO: get rid of this callback, it should be universal //private const string CallbackFunction = "CordovaMediaonStatus"; #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 = PlayerState_None; /// /// 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(); } private void InvokeCallback(int message, string value, bool removeHandler) { string args = string.Format("('{0}',{1},{2});", this.id, message, value); string callback = @"(function(id,msg,value){ try { if (msg == Media.MEDIA_ERROR) { value = {'code':value}; } Media.onStatus(id,msg,value); } catch(e) { console.log('Error calling Media.onStatus :: ' + e); } })" + args; this.handler.InvokeCustomScript(new ScriptCallback("eval", new string[] { callback }), false); } private void InvokeCallback(int message, int value, bool removeHandler) { InvokeCallback(message, value.ToString(), removeHandler); } private void InvokeCallback(int message, double value, bool removeHandler) { InvokeCallback(message, value.ToString(), removeHandler); } /// /// Starts recording, data is stored in memory /// /// public void startRecording(string filePath) { if (this.player != null) { InvokeCallback(MediaError, MediaErrorPlayModeSet, false); } 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); MemoryStream stream = new MemoryStream(); this.memoryStream = stream; int numBits = 16; int numBytes = numBits / 8; // inline version from AudioFormatsHelper stream.Write(System.Text.Encoding.UTF8.GetBytes("RIFF"), 0, 4); stream.Write(BitConverter.GetBytes(0), 0, 4); stream.Write(System.Text.Encoding.UTF8.GetBytes("WAVE"), 0, 4); stream.Write(System.Text.Encoding.UTF8.GetBytes("fmt "), 0, 4); stream.Write(BitConverter.GetBytes(16), 0, 4); stream.Write(BitConverter.GetBytes((short)1), 0, 2); stream.Write(BitConverter.GetBytes((short)1), 0, 2); stream.Write(BitConverter.GetBytes(this.recorder.SampleRate), 0, 4); stream.Write(BitConverter.GetBytes(this.recorder.SampleRate * numBytes), 0, 4); stream.Write(BitConverter.GetBytes((short)(numBytes)), 0, 2); stream.Write(BitConverter.GetBytes((short)(numBits)), 0, 2); stream.Write(System.Text.Encoding.UTF8.GetBytes("data"), 0, 4); stream.Write(BitConverter.GetBytes(0), 0, 4); this.recorder.Start(); FrameworkDispatcher.Update(); this.SetState(PlayerState_Running); } catch (Exception) { InvokeCallback(MediaError, MediaErrorStartingRecording, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingRecording),false); } } else { InvokeCallback(MediaError, MediaErrorAlreadyRecording, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorAlreadyRecording),false); } } /// /// Stops recording /// public void stopRecording() { if (this.recorder != null) { if (this.state == PlayerState_Running) { try { this.recorder.Stop(); this.recorder.BufferReady -= recorderBufferReady; this.recorder = null; SaveAudioClipToLocalStorage(); this.FinalizeXnaGameLoop(); this.SetState(PlayerState_Stopped); } catch (Exception) { //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) { InvokeCallback(MediaError, MediaErrorRecordModeSet, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorRecordModeSet),false); return; } if (this.player == null || this.player.Source.AbsolutePath.LastIndexOf(filePath) < 0) { try { // this.player is a MediaElement, it must be added to the visual tree in order to play PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; if (frame != null) { PhoneApplicationPage page = frame.Content as PhoneApplicationPage; if (page != null) { Grid grid = page.FindName("LayoutRoot") as Grid; if (grid != null) { this.player = grid.FindName("playerMediaElement") as MediaElement; if (this.player == null) // still null ? { this.player = new MediaElement(); this.player.Name = "playerMediaElement"; grid.Children.Add(this.player); this.player.Visibility = Visibility.Visible; } if (this.player.CurrentState == System.Windows.Media.MediaElementState.Playing) { this.player.Stop(); // stop it! } this.player.Source = null; // Garbage collect it. this.player.MediaOpened += MediaOpened; this.player.MediaEnded += MediaEnded; this.player.MediaFailed += MediaFailed; } } } this.audioFile = filePath; Uri uri = new Uri(filePath, UriKind.RelativeOrAbsolute); if (uri.IsAbsoluteUri) { this.player.Source = uri; } else { using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) { if (!isoFile.FileExists(filePath)) { // try to unpack it from the dll into isolated storage StreamResourceInfo fileResourceStreamInfo = Application.GetResourceStream(new Uri(filePath, UriKind.Relative)); if (fileResourceStreamInfo != null) { using (BinaryReader br = new BinaryReader(fileResourceStreamInfo.Stream)) { byte[] data = br.ReadBytes((int)fileResourceStreamInfo.Stream.Length); string[] dirParts = filePath.Split('/'); string dirName = ""; for (int n = 0; n < dirParts.Length - 1; n++) { dirName += dirParts[n] + "/"; } if (!isoFile.DirectoryExists(dirName)) { isoFile.CreateDirectory(dirName); } using (IsolatedStorageFileStream outFile = isoFile.OpenFile(filePath, FileMode.Create)) { using (BinaryWriter writer = new BinaryWriter(outFile)) { writer.Write(data); } } } } } if (isoFile.FileExists(filePath)) { using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, isoFile)) { this.player.SetSource(stream); } } else { InvokeCallback(MediaError, MediaErrorPlayModeSet, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, 1), false); return; } } } this.SetState(PlayerState_Starting); } catch (Exception e) { Debug.WriteLine("Error in AudioPlayer::startPlaying : " + e.Message); InvokeCallback(MediaError, MediaErrorStartingPlayback, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStartingPlayback),false); } } else { if (this.state != PlayerState_Running) { this.player.Play(); this.SetState(PlayerState_Running); } else { InvokeCallback(MediaError, MediaErrorResumeState, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorResumeState),false); } } } /// /// Callback to be invoked when the media source is ready for playback /// private void MediaOpened(object sender, RoutedEventArgs arg) { if (this.player != null) { this.duration = this.player.NaturalDuration.TimeSpan.TotalSeconds; InvokeCallback(MediaDuration, this.duration, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaDuration, this.duration),false); if (!this.prepareOnly) { this.player.Play(); this.SetState(PlayerState_Running); } this.prepareOnly = false; } else { // TODO: occasionally MediaOpened is signalled, but player is null } } /// /// Callback to be invoked when playback of a media source has completed /// private void MediaEnded(object sender, RoutedEventArgs arg) { this.SetState(PlayerState_Stopped); } /// /// Callback to be invoked when playback of a media source has failed /// private void MediaFailed(object sender, RoutedEventArgs arg) { player.Stop(); InvokeCallback(MediaError, MediaErrorStartingPlayback, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError.ToString(), "Media failed"),false); } /// /// Seek or jump to a new time in the track /// /// The new track position public void seekToPlaying(int milliseconds) { if (this.player != null) { TimeSpan tsPos = new TimeSpan(0, 0, 0, 0, milliseconds); this.player.Position = tsPos; InvokeCallback(MediaPosition, milliseconds / 1000.0f, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, milliseconds / 1000.0f),false); } } /// /// Set the volume of the player /// /// volume 0.0-1.0, default value is 0.5 public void setVolume(double vol) { if (this.player != null) { this.player.Volume = vol; } } /// /// Pauses playing /// public void pausePlaying() { if (this.state == PlayerState_Running) { this.player.Pause(); this.SetState(PlayerState_Paused); } else { InvokeCallback(MediaError, MediaErrorPauseState, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorPauseState),false); } } /// /// Stops playing the audio file /// public void stopPlaying() { if ((this.state == PlayerState_Running) || (this.state == PlayerState_Paused)) { this.player.Stop(); this.player.Position = new TimeSpan(0L); this.SetState(PlayerState_Stopped); } //else // Why is it an error to call stop on a stopped media? //{ // this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaError, MediaErrorStopState), false); //} } /// /// Gets current position of playback /// /// current position public double getCurrentPosition() { if ((this.state == PlayerState_Running) || (this.state == PlayerState_Paused)) { double currentPosition = this.player.Position.TotalSeconds; //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaPosition, currentPosition),false); return currentPosition; } else { return 0; } } /// /// 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) { InvokeCallback(MediaState, state, false); //this.handler.InvokeCustomScript(new ScriptCallback(CallbackFunction, this.id, MediaState, state),false); } 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 (memoryStream == null || memoryStream.Length <= 0) { return; } long position = memoryStream.Position; memoryStream.Seek(4, SeekOrigin.Begin); memoryStream.Write(BitConverter.GetBytes((int)memoryStream.Length - 8), 0, 4); memoryStream.Seek(40, SeekOrigin.Begin); memoryStream.Write(BitConverter.GetBytes((int)memoryStream.Length - 44), 0, 4); memoryStream.Seek(position, SeekOrigin.Begin); 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) { //TODO: log or do something else throw; } } #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) if (this.dtXna != null) { this.dtXna.Stop(); this.dtXna = null; } } #endregion #endregion } }