You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
649 lines
24 KiB
C#
649 lines
24 KiB
C#
/*
|
|
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, int value, bool removeHandler)
|
|
{
|
|
InvokeCallback(message, (double)value, removeHandler);
|
|
}
|
|
|
|
private void InvokeCallback(int message, double value, bool removeHandler)
|
|
{
|
|
var status = new Media.MediaStatus()
|
|
{
|
|
Id = this.id,
|
|
MsgType = message
|
|
};
|
|
|
|
if (message == MediaError)
|
|
{
|
|
status.Value = new Media.MediaError() { Code = (int)value };
|
|
}
|
|
else
|
|
{
|
|
status.Value = value;
|
|
}
|
|
|
|
this.handler.ReportStatus(status);
|
|
}
|
|
|
|
/// <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.AbsoluteUri.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)
|
|
{
|
|
if (player != null)
|
|
{
|
|
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
|
|
}
|
|
}
|