blob: fe85e6ba0485e20732cb58090b36774ea9b7c15c [file] [log] [blame]
/*
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
}
}