\r
public class BufferSound : MediaObject\r
{\r
- /// <summary>\r
- /// Is sound currently playing\r
- /// </summary>\r
- public bool Playing { get { return playing; } }\r
- private bool playing = false;\r
-\r
- /// <summary>\r
- /// Is sound currently paused\r
- /// </summary>\r
- public bool Paused { get { return paused; } }\r
- private bool paused = false;\r
-\r
- private bool soundcreated = false;\r
- private System.Timers.Timer timer;\r
-\r
private FMOD.CREATESOUNDEXINFO extendedInfo;\r
+ private FMOD.VECTOR position;\r
+\r
+ public Sound Sound { get { return sound; } }\r
\r
/// <summary>\r
/// Creates a new sound object\r
/// </summary>\r
/// <param name="system">Sound system</param>\r
- public BufferSound()\r
+ public BufferSound(byte[] buffer, bool loop, bool global, Vector3 worldpos )\r
:base()\r
{\r
- timer = new System.Timers.Timer();\r
- timer.Interval = 50d;\r
- timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);\r
- timer.Enabled = false;\r
- }\r
+ position = FromOMVSpace(worldpos);\r
\r
+ extendedInfo.format = SOUND_FORMAT.PCM16;\r
+ extendedInfo.nonblockcallback += DispatchNonBlockCallback;\r
\r
- /// <summary>\r
- /// Releases resources of this sound object\r
- /// </summary>\r
- public override void Dispose()\r
- {\r
- if (timer != null)\r
- {\r
- timer.Enabled = false;\r
- timer.Dispose();\r
- timer = null;\r
- }\r
-\r
- if (sound != null)\r
- {\r
- sound.release();\r
- sound = null;\r
- }\r
- base.Dispose();\r
- }\r
+ // Set flags to determine how it will be played.\r
+ FMOD.MODE mode = FMOD.MODE.SOFTWARE | FMOD.MODE._3D;\r
+ position = FromOMVSpace(worldpos);\r
\r
- /// <summary>\r
- /// Plays audio buffer\r
- /// </summary>\r
- /// <param name="url">URL of the stream</param>\r
- public void Play( byte[] buffer, bool loop)\r
- {\r
- extendedInfo.format = SOUND_FORMAT.PCM16;\r
+ // Set coordinate space interpretation.\r
+ if (global)\r
+ mode |= FMOD.MODE._3D_WORLDRELATIVE;\r
+ else\r
+ mode |= FMOD.MODE._3D_HEADRELATIVE;\r
\r
invoke(new SoundDelegate(delegate\r
{\r
- MediaManager.FMODExec(system.createSound(buffer,\r
- (MODE.HARDWARE | MODE._3D | MODE.NONBLOCKING),\r
+ FMODExec(system.createSound(buffer,\r
+ mode,\r
ref extendedInfo,\r
ref sound));\r
+\r
+ // Register for callbacks.\r
+ RegisterSound(sound);\r
+\r
+ if (loop)\r
+ FMODExec(sound.setLoopCount(-1));\r
}));\r
}\r
\r
+ public BufferSound( BufferSound shared, bool loop, Vector3 worldpos)\r
+ : base()\r
+ {\r
+ position = FromOMVSpace(worldpos);\r
+\r
+ // Share a previously loaded sound.\r
+ Cloned = true;\r
+ sound = shared.Sound;\r
+\r
+ NonBlockCallbackHandler(RESULT.OK );\r
+ }\r
+\r
/// <summary>\r
- /// Toggles sound pause\r
+ /// Releases resources of this sound object\r
/// </summary>\r
- public void TogglePaused()\r
+ public override void Dispose()\r
{\r
- if (channel != null)\r
- {\r
- channel.getPaused(ref paused);\r
- channel.setPaused(!paused);\r
- }\r
+ base.Dispose();\r
}\r
\r
- void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)\r
+ protected override RESULT NonBlockCallbackHandler(RESULT instatus)\r
{\r
- OPENSTATE openstate = 0;\r
- uint percentbuffered = 0;\r
- bool starving = false;\r
+ if (instatus != RESULT.OK)\r
+ {\r
+ Logger.Log("Error opening stream: ", Helpers.LogLevel.Debug);\r
+ return RESULT.OK;\r
+ }\r
\r
try\r
{\r
- if (soundcreated)\r
- {\r
- MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving));\r
-\r
- if (openstate == OPENSTATE.READY && channel == null)\r
- {\r
- MediaManager.FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel));\r
- MediaManager.FMODExec(channel.setVolume(volume));\r
- }\r
- }\r
-\r
- if (system != null)\r
- {\r
- system.update();\r
- }\r
+ // Allocate a channel and set initial volume. Initially paused.\r
+ FMODExec(system.playSound(CHANNELINDEX.FREE, sound, true, ref channel));\r
+ volume = 0.5f;\r
+ FMODExec(channel.setVolume(volume));\r
+\r
+ // Take note of when the sound is finished playing.\r
+ FMODExec(channel.setCallback(EndCallback));\r
+\r
+ // Set speech attenuation limits.\r
+ FMODExec(sound.set3DMinMaxDistance(\r
+ 1.2f, // Any closer than this gets no louder\r
+ 8.0f)); // Further than this gets no softer.\r
+\r
+ // Set the sound point of origin.\r
+ FMODExec(channel.set3DAttributes(ref position, ref ZeroVector));\r
+\r
+ // Turn off pause mode. The sound will start playing now.\r
+ FMODExec(channel.setPaused(false));\r
}\r
catch (Exception ex)\r
{\r
- playing = paused = false;\r
- timer.Enabled = false;\r
- Logger.Log("Error playing sound: ", Helpers.LogLevel.Debug, ex);\r
+ Logger.Log("Error starting stream: ", Helpers.LogLevel.Debug, ex);\r
+ }\r
+\r
+ return RESULT.OK;\r
+ }\r
+\r
+ /// <summary>\r
+ /// Callback handler for reaching the end of a sound.\r
+ /// </summary>\r
+ /// <param name="channelraw"></param>\r
+ /// <param name="type"></param>\r
+ /// <param name="commanddata1"></param>\r
+ /// <param name="commanddata2"></param>\r
+ /// <returns></returns>\r
+ private RESULT EndCallback(\r
+ IntPtr channelraw,\r
+ CHANNEL_CALLBACKTYPE type,\r
+ IntPtr commanddata1,\r
+ IntPtr commanddata2)\r
+ {\r
+ // Ignore other callback types.\r
+ if (type != CHANNEL_CALLBACKTYPE.END) return RESULT.OK;\r
+\r
+ // Release the buffer to avoid a big memory leak.\r
+ //TODO Should be a reference count on cloned sound buffers.\r
+ if (!Cloned && sound != null)\r
+ {\r
+ sound.release();\r
+ sound = null;\r
}\r
+ channel = null;\r
+\r
+ return RESULT.OK;\r
}\r
+\r
}\r
}\r
public bool SoundSystemAvailable { get { return soundSystemAvailable; } }
private bool soundSystemAvailable = false;
private Thread soundThread;
+ RadegastInstance instance;
private List<MediaObject> sounds = new List<MediaObject>();
public MediaManager(RadegastInstance instance)
: base()
{
+ this.instance = instance;
+
// Start the background thread that does all the FMOD calls.
soundThread = new Thread(new ThreadStart(CommandLoop));
soundThread.IsBackground = true;
{
SoundDelegate action;
+ UpVector.x = 0.0f;
+ UpVector.y = 1.0f;
+ UpVector.z = 0.0f;
+ ZeroVector.x = 0.0f;
+ ZeroVector.y = 0.0f;
+ ZeroVector.z = 0.0f;
+
+ allSounds = new Dictionary<IntPtr,MediaObject>();
+ allChannels = new Dictionary<IntPtr, MediaObject>();
+
InitFMOD();
if (!this.soundSystemAvailable) return;
}
}
+ /// <summary>
+ /// Initialize the FMOD sound system.
+ /// </summary>
private void InitFMOD()
{
try
base.Dispose();
}
- public static void FMODExec(FMOD.RESULT result)
+ private void OnListenerUpdate()
{
- if (result != FMOD.RESULT.OK)
+ Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f );
+ float lastface = 0.0f;
+
+ while (true)
{
- throw new MediaException("FMOD error! " + result + " - " + FMOD.Error.String(result));
+ // TODO would be nice if there was an event for this.
+ Thread.Sleep(500);
+
+ if (system == null) continue;
+
+ AgentManager my = instance.Client.Self;
+
+ // If we are standing still, nothing to update.
+ if (my.SimPosition.Equals( lastpos ) &&
+ my.Movement.BodyRotation.W == lastface)
+ continue;
+
+ lastpos = my.SimPosition;
+ lastface = my.Movement.BodyRotation.W;
+
+ // Convert coordinate spaces.
+ FMOD.VECTOR listenerpos = FromOMVSpace(my.SimPosition);
+
+ // Get azimuth from the facing Quaternion. Note we assume the
+ // avatar is standing upright. Avatars in unusual positions
+ // hear things from unpredictable directions.
+ // By definition, facing.W = Cos( angle/2 )
+ double angle = 2.0 * Math.Acos(my.Movement.BodyRotation.W);
+ FMOD.VECTOR forward = new FMOD.VECTOR();
+ forward.x = (float)Math.Asin(angle);
+ forward.y = 0.0f;
+ forward.z = (float)Math.Acos(angle);
+
+ // Tell FMOD the new orientation.
+ invoke( new SoundDelegate( delegate
+ {
+ MediaManager.FMODExec( system.set3DListenerAttributes(
+ 0,
+ ref listenerpos, // Position
+ ref ZeroVector, // Velocity
+ ref forward, // Facing direction
+ ref UpVector )); // Top of head
+
+ system.update();
+ }));
}
}
+
}
public class MediaException : Exception
public Channel FMODChannel { get { return channel; } }
protected Channel channel = null;
+ protected FMOD.CREATESOUNDEXINFO extraInfo;
+
/// <summary>
/// FMOD sound object, should not be used directly, add methods to Radegast.Media.Sound
/// </summary>
public FMOD.Sound FMODSound { get { return sound; } }
protected FMOD.Sound sound = null;
+ protected static FMOD.VECTOR UpVector;
+ protected static FMOD.VECTOR ZeroVector;
+
public FMOD.System FMODSystem { get { return system; } }
/// <summary>
/// Base FMOD system object, of which there is only one.
public MediaObject()
{
+ extraInfo = new FMOD.CREATESOUNDEXINFO();
+ extraInfo.cbsize = 10;
+
}
+ protected bool Cloned = false;
public virtual void Dispose()
{
- if (sound != null)
+ if (!Cloned && sound != null)
{
sound.release();
sound = null;
/// Common actgions for all sound types.
/// </summary>
protected float volume = 0.5f;
- public float Volume {
+ public float Volume
+ {
get
{
return volume;
}));
}
}
+
+ public void Stop()
+ {
+ if (channel != null)
+ {
+ invoke(new SoundDelegate(delegate
+ {
+ MediaManager.FMODExec(channel.stop());
+ }));
+ }
+ }
+
+ /// <summary>
+ /// Convert OpenMetaVerse to FMOD coordinate space.
+ /// </summary>
+ /// <param name="omvV"></param>
+ /// <returns></returns>
+ protected FMOD.VECTOR FromOMVSpace(OpenMetaverse.Vector3 omvV)
+ {
+ // OMV X is forward, Y is left, Z is up.
+ // FMOD Z is forward, X is right, Y is up.
+ FMOD.VECTOR v = new FMOD.VECTOR();
+ v.x = -omvV.Y;
+ v.y = omvV.Z;
+ v.z = omvV.X;
+ return v;
+ }
+
+ protected static Dictionary<IntPtr,MediaObject> allSounds;
+ protected static Dictionary<IntPtr, MediaObject> allChannels;
+ protected void RegisterSound(FMOD.Sound sound)
+ {
+ allSounds.Add(sound.getRaw(), this);
+ }
+ protected void RegisterChannel(FMOD.Channel channel)
+ {
+ allSounds.Add(channel.getRaw(), this);
+ }
+ protected void UnRegisterSound()
+ {
+ if (sound == null) return;
+ IntPtr raw = sound.getRaw();
+ if (allSounds.ContainsKey( raw ))
+ {
+ allSounds.Remove( raw );
+ }
+ }
+ protected void UnRegisterChannel()
+ {
+ if (channel == null) return;
+ IntPtr raw = channel.getRaw();
+ if (allChannels.ContainsKey(raw))
+ {
+ allChannels.Remove(raw);
+ }
+ }
+
+ /// <summary>
+ /// A callback for asynchronous FMOD calls.
+ /// </summary>
+ /// <returns></returns>
+ protected virtual FMOD.RESULT NonBlockCallbackHandler( RESULT result ) { return RESULT.OK; }
+ protected virtual FMOD.RESULT EndCallbackHandler() { return RESULT.OK; }
+
+ protected RESULT DispatchNonBlockCallback(IntPtr soundraw, RESULT result)
+ {
+ if (allSounds.ContainsKey(soundraw))
+ {
+ MediaObject sndobj = allSounds[soundraw];
+ return sndobj.NonBlockCallbackHandler( result );
+ }
+
+ return FMOD.RESULT.OK;
+ }
+
+ protected RESULT DispatchEndCallback(
+ IntPtr channelraw,
+ CHANNEL_CALLBACKTYPE type,
+ IntPtr commanddata1,
+ IntPtr commanddata2)
+ {
+ // Ignore other callback types.
+ if (type != CHANNEL_CALLBACKTYPE.END) return RESULT.OK;
+
+ if (allChannels.ContainsKey(channelraw))
+ {
+ MediaObject sndobj = allSounds[channelraw];
+ return sndobj.EndCallbackHandler();
+ }
+
+ return RESULT.OK;
+ }
+
+ public delegate RESULT SOUND_NONBLOCKCALLBACK(IntPtr soundraw, RESULT result);
+
+ protected static void FMODExec(FMOD.RESULT result)
+ {
+ if (result != FMOD.RESULT.OK)
+ {
+ throw new MediaException("FMOD error! " + result + " - " + FMOD.Error.String(result));
+ }
+ }
+
+
+
}
}
{\r
if (!soundcreated)\r
{\r
- MediaManager.FMODExec(system.createSound(filename,\r
+ FMODExec(system.createSound(filename,\r
(MODE.HARDWARE | MODE._2D | MODE.CREATESTREAM | MODE.NONBLOCKING),\r
ref sound));\r
soundcreated = true;\r
{\r
if (soundcreated)\r
{\r
- MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving));\r
+ FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving));\r
\r
if (openstate == OPENSTATE.READY && channel == null)\r
{\r
- MediaManager.FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel));\r
- MediaManager.FMODExec(channel.setVolume(volume));\r
+ FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel));\r
+ FMODExec(channel.setVolume(volume));\r
}\r
}\r
\r
public class Stream : MediaObject\r
{\r
/// <summary>\r
- /// Returns current position of the sound played in ms\r
- /// Do not confuse with the spatial Position on other suonds.\r
- /// </summary>\r
- public uint Position { get { return position; } }\r
- private uint position = 0;\r
-\r
- /// <summary>\r
- /// Is sound currently playing\r
- /// </summary>\r
- public bool Playing { get { return playing; } }\r
- private bool playing = false;\r
-\r
- /// <summary>\r
- /// Is sound currently paused\r
- /// </summary>\r
- public bool Paused { get { return paused; } }\r
- private bool paused = false;\r
-\r
- /// <summary>\r
/// Fired when a stream meta data is received\r
/// </summary>\r
/// <param name="sender">Sender</param>\r
/// </summary>\r
public event StreamInfoCallback OnStreamInfo;\r
\r
- private System.Timers.Timer timer;\r
-\r
/// <summary>\r
/// Creates a new sound object\r
/// </summary>\r
public Stream()\r
:base()\r
{\r
- timer = new System.Timers.Timer();\r
- timer.Interval = 50d;\r
- timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);\r
- timer.Enabled = false;\r
}\r
\r
\r
/// </summary>\r
public override void Dispose()\r
{\r
- if (timer != null)\r
- {\r
- timer.Enabled = false;\r
- timer.Dispose();\r
- timer = null;\r
- }\r
-\r
base.Dispose();\r
}\r
\r
/// <param name="url">URL of the stream</param>\r
public void PlayStream(string url)\r
{\r
- if (!Active)\r
+ // Stop old stream first.\r
+ if (channel != null)\r
{\r
- invoke( new SoundDelegate(\r
- delegate {\r
- MediaManager.FMODExec(system.createSound(url,\r
- (MODE.HARDWARE | MODE._2D | MODE.CREATESTREAM | MODE.NONBLOCKING), ref sound));\r
- \r
- timer.Enabled = true;\r
- timer_Elapsed(null, null);\r
+ invoke(new SoundDelegate(\r
+ delegate\r
+ {\r
+ FMODExec(channel.stop());\r
+ channel = null;\r
+ UnRegisterSound();\r
+ FMODExec(sound.release());\r
+ sound = null;\r
}));\r
}\r
+\r
+ extraInfo.nonblockcallback = new FMOD.SOUND_NONBLOCKCALLBACK(DispatchNonBlockCallback);\r
+\r
+ invoke( new SoundDelegate(\r
+ delegate {\r
+ FMODExec(\r
+ system.createSound(url,\r
+ (MODE.HARDWARE | MODE._2D | MODE.CREATESTREAM | MODE.NONBLOCKING),\r
+ ref extraInfo,\r
+ ref sound));\r
+ // Register for callbacks.\r
+ RegisterSound(sound);\r
+ }));\r
}\r
\r
/// <summary>\r
- /// Toggles sound pause\r
+ /// Callback when a stream has been loaded\r
/// </summary>\r
- public void TogglePaused()\r
+ /// <param name="instatus"></param>\r
+ /// <returns></returns>\r
+ protected override RESULT NonBlockCallbackHandler(RESULT instatus)\r
{\r
- invoke(new SoundDelegate(delegate\r
- {\r
- if (channel != null)\r
- {\r
- channel.getPaused(ref paused);\r
- channel.setPaused(!paused);\r
- }\r
- }));\r
- }\r
-\r
- void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)\r
- {\r
- OPENSTATE openstate = 0;\r
- uint percentbuffered = 0;\r
- bool starving = false;\r
-\r
- try\r
+ if (instatus != RESULT.OK)\r
{\r
- if (Active)\r
- {\r
- // Query what the sound is doing.\r
- MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving));\r
+ Logger.Log("Error opening stream: ", Helpers.LogLevel.Debug);\r
+ return RESULT.OK;\r
+ }\r
\r
- // If a channel is not allocated yet, ask for one.\r
- if (openstate == OPENSTATE.READY && channel == null)\r
+ invoke(new SoundDelegate(\r
+ delegate\r
+ {\r
+ try\r
{\r
// Allocate a channel and set initial volume.\r
- MediaManager.FMODExec(system.playSound( CHANNELINDEX.FREE, sound, false, ref channel));\r
+ FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel));\r
volume = 0.5f;\r
- MediaManager.FMODExec(channel.setVolume(volume));\r
- }\r
- }\r
+ FMODExec(channel.setVolume(volume));\r
\r
- if (channel != null)\r
- {\r
- // Do not try to get MP3 tags om Unix. It breaks.\r
- if (Environment.OSVersion.Platform != PlatformID.Unix)\r
- {\r
- for (; ; )\r
+ // If this is not Unix, try to get MP3 tags.\r
+ // getTag seems to break on Unix.\r
+ if (Environment.OSVersion.Platform != PlatformID.Unix)\r
{\r
TAG tag = new TAG();\r
- if (sound.getTag(null, -1, ref tag) != RESULT.OK)\r
- {\r
- break;\r
- }\r
- if (tag.datatype != TAGDATATYPE.STRING)\r
- {\r
- break;\r
- }\r
- else\r
+\r
+ while (sound.getTag(null, -1, ref tag) == RESULT.OK)\r
{\r
+ if (tag.datatype != TAGDATATYPE.STRING) continue;\r
+\r
+ // Tell listeners about the Stream tag. This can be\r
+ // displayed to the user.\r
if (OnStreamInfo != null)\r
- try { OnStreamInfo(this, new StreamInfoArgs(tag.name.ToLower(), Marshal.PtrToStringAnsi(tag.data))); }\r
+ try\r
+ {\r
+ OnStreamInfo(this, new StreamInfoArgs(tag.name.ToLower(), Marshal.PtrToStringAnsi(tag.data)));\r
+ }\r
catch (Exception) { }\r
}\r
}\r
- }\r
\r
- // Get pause/play status\r
- MediaManager.FMODExec(channel.getPaused(ref paused));\r
- MediaManager.FMODExec(channel.isPlaying(ref playing));\r
- MediaManager.FMODExec(channel.getPosition(ref position, TIMEUNIT.MS));\r
- }\r
+ system.update();\r
+ }\r
+ catch (Exception ex)\r
+ {\r
+ Logger.Log("Error playing stream: ", Helpers.LogLevel.Debug, ex);\r
+ }\r
+ }));\r
\r
- if (system != null)\r
- {\r
- system.update();\r
- }\r
- }\r
- catch (Exception ex)\r
- {\r
- playing = paused = false;\r
- timer.Enabled = false;\r
- Logger.Log("Error playing stream: ", Helpers.LogLevel.Debug, ex);\r
- }\r
+ return RESULT.OK;\r
}\r
}\r
}\r