diff options
Diffstat (limited to 'plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs')
| -rw-r--r-- | plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs b/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs new file mode 100644 index 00000000..882eb96e --- /dev/null +++ b/plugins/org.apache.cordova.media/src/wp/AudioPlayer.cs @@ -0,0 +1,647 @@ +/* + 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 +{ + + /// <summary> + /// Implements audio record and play back functionality. + /// </summary> + 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 + + + /// <summary> + /// The AudioHandler object + /// </summary> + private Media handler; + + /// <summary> + /// Temporary buffer to store audio chunk + /// </summary> + private byte[] buffer; + + /// <summary> + /// Xna game loop dispatcher + /// </summary> + DispatcherTimer dtXna; + + + /// <summary> + /// Output buffer + /// </summary> + private MemoryStream memoryStream; + + /// <summary> + /// The id of this player (used to identify Media object in JavaScript) + /// </summary> + private String id; + + /// <summary> + /// State of recording or playback + /// </summary> + private int state = PlayerState_None; + + /// <summary> + /// File name to play or record to + /// </summary> + private String audioFile = null; + + /// <summary> + /// Duration of audio + /// </summary> + private double duration = -1; + + /// <summary> + /// Audio player object + /// </summary> + private MediaElement player = null; + + /// <summary> + /// Audio source + /// </summary> + private Microphone recorder; + + /// <summary> + /// Internal flag specified that we should only open audio w/o playing it + /// </summary> + private bool prepareOnly = false; + + /// <summary> + /// Creates AudioPlayer instance + /// </summary> + /// <param name="handler">Media object</param> + /// <param name="id">player id</param> + public AudioPlayer(Media handler, String id) + { + this.handler = handler; + this.id = id; + } + + + /// <summary> + /// Destroys player and stop audio playing or recording + /// </summary> + 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); + } + + /// <summary> + /// Starts recording, data is stored in memory + /// </summary> + /// <param name="filePath"></param> + 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<EventArgs>(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); + } + } + + /// <summary> + /// Stops recording + /// </summary> + 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 + } + } + } + } + + /// <summary> + /// Starts or resume playing audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + /// <summary> + /// Starts or resume playing audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + 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); + } + } + } + + /// <summary> + /// Callback to be invoked when the media source is ready for playback + /// </summary> + 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 + } + } + + /// <summary> + /// Callback to be invoked when playback of a media source has completed + /// </summary> + private void MediaEnded(object sender, RoutedEventArgs arg) + { + this.SetState(PlayerState_Stopped); + } + + /// <summary> + /// Callback to be invoked when playback of a media source has failed + /// </summary> + 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); + } + + /// <summary> + /// Seek or jump to a new time in the track + /// </summary> + /// <param name="milliseconds">The new track position</param> + 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); + } + } + + /// <summary> + /// Set the volume of the player + /// </summary> + /// <param name="vol">volume 0.0-1.0, default value is 0.5</param> + public void setVolume(double vol) + { + if (this.player != null) + { + this.player.Volume = vol; + } + } + + /// <summary> + /// Pauses playing + /// </summary> + 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); + } + } + + + /// <summary> + /// Stops playing the audio file + /// </summary> + 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); + //} + } + + /// <summary> + /// Gets current position of playback + /// </summary> + /// <returns>current position</returns> + 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; + } + } + + /// <summary> + /// Gets the duration of the audio file + /// </summary> + /// <param name="filePath">The name of the audio file</param> + /// <returns>track duration</returns> + 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; + } + } + + /// <summary> + /// Sets the state and send it to JavaScript + /// </summary> + /// <param name="state">state</param> + 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 + + /// <summary> + /// Copies data from recorder to memory storages and updates recording state + /// </summary> + /// <param name="sender"></param> + /// <param name="e"></param> + private void recorderBufferReady(object sender, EventArgs e) + { + this.recorder.GetData(this.buffer); + this.memoryStream.Write(this.buffer, 0, this.buffer.Length); + } + + /// <summary> + /// Writes audio data from memory to isolated storage + /// </summary> + /// <returns></returns> + 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 + /// <summary> + /// Special initialization required for the microphone: XNA game loop + /// </summary> + 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(); + } + /// <summary> + /// Finalizes XNA game loop for microphone + /// </summary> + 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 + } +} |
