From ac6381092dfafda993966d24db06c8d8edaeff84 Mon Sep 17 00:00:00 2001 From: Mojito Sorbet Date: Mon, 3 May 2010 17:12:15 +0000 Subject: [PATCH] Initial reorganizations of Media classes to support parcel, object, and speech Sounds. git-svn-id: https://radegast.googlecode.com/svn/branches/sounds@607 f7a694da-4d33-11de-9ad6-1127a62b9fcd --- Radegast/Core/Media/BufferSound.cs | 153 ++++++++++++++++++++++++ Radegast/Core/Media/MediaManager.cs | 70 ++++++++--- Radegast/Core/Media/MediaObject.cs | 75 +++++++++++- Radegast/Core/Media/Speech.cs | 157 +++++++++++++++++++++++++ Radegast/Core/Media/Stream.cs | 215 ++++++++++++++++++++++++++++++++++ Radegast/GUI/Consoles/MediaConsole.cs | 23 ++-- 6 files changed, 661 insertions(+), 32 deletions(-) create mode 100644 Radegast/Core/Media/BufferSound.cs create mode 100644 Radegast/Core/Media/Speech.cs create mode 100644 Radegast/Core/Media/Stream.cs diff --git a/Radegast/Core/Media/BufferSound.cs b/Radegast/Core/Media/BufferSound.cs new file mode 100644 index 0000000..3dfb74e --- /dev/null +++ b/Radegast/Core/Media/BufferSound.cs @@ -0,0 +1,153 @@ +// +// Radegast Metaverse Client +// Copyright (c) 2009, Radegast Development Team +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the application "Radegast", nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// $Id: Sound.cs 502 2010-03-14 23:13:46Z latifer $ +// +using System; +using System.Runtime.InteropServices; +using FMOD; +using OpenMetaverse; + +namespace Radegast.Media +{ + + public class BufferSound : MediaObject + { + /// + /// Is sound currently playing + /// + public bool Playing { get { return playing; } } + private bool playing = false; + + /// + /// Is sound currently paused + /// + public bool Paused { get { return paused; } } + private bool paused = false; + + private bool soundcreated = false; + private System.Timers.Timer timer; + + private FMOD.CREATESOUNDEXINFO extendedInfo; + + /// + /// Creates a new sound object + /// + /// Sound system + public BufferSound() + :base() + { + timer = new System.Timers.Timer(); + timer.Interval = 50d; + timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); + timer.Enabled = false; + } + + + /// + /// Releases resources of this sound object + /// + public override void Dispose() + { + if (timer != null) + { + timer.Enabled = false; + timer.Dispose(); + timer = null; + } + + if (sound != null) + { + sound.release(); + sound = null; + } + base.Dispose(); + } + + /// + /// Plays audio buffer + /// + /// URL of the stream + public void Play( byte[] buffer, bool loop) + { + extendedInfo.format = SOUND_FORMAT.PCM16; + + invoke(new SoundDelegate(delegate + { + MediaManager.FMODExec(system.createSound(buffer, + (MODE.HARDWARE | MODE._3D | MODE.NONBLOCKING), + ref extendedInfo, + ref sound)); + })); + } + + /// + /// Toggles sound pause + /// + public void TogglePaused() + { + if (channel != null) + { + channel.getPaused(ref paused); + channel.setPaused(!paused); + } + } + + void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + OPENSTATE openstate = 0; + uint percentbuffered = 0; + bool starving = false; + + try + { + if (soundcreated) + { + MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving)); + + if (openstate == OPENSTATE.READY && channel == null) + { + MediaManager.FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel)); + MediaManager.FMODExec(channel.setVolume(volume)); + } + } + + if (system != null) + { + system.update(); + } + } + catch (Exception ex) + { + playing = paused = false; + timer.Enabled = false; + Logger.Log("Error playing sound: ", Helpers.LogLevel.Debug, ex); + } + } + } +} diff --git a/Radegast/Core/Media/MediaManager.cs b/Radegast/Core/Media/MediaManager.cs index 8ae3df3..3979a2c 100644 --- a/Radegast/Core/Media/MediaManager.cs +++ b/Radegast/Core/Media/MediaManager.cs @@ -32,6 +32,7 @@ using System; using System.Collections.Generic; using System.Text; using FMOD; +using System.Threading; using OpenMetaverse; namespace Radegast.Media @@ -43,17 +44,56 @@ namespace Radegast.Media /// public bool SoundSystemAvailable { get { return soundSystemAvailable; } } private bool soundSystemAvailable = false; + private Thread soundThread; private List sounds = new List(); - /// - /// Parcel music stream player - /// - public Sound ParcelMusic { get { return parcelMusic; } set { parcelMusic = value; } } - private Sound parcelMusic; - public MediaManager(RadegastInstance instance) - : base(null) + : base() + { + // Start the background thread that does all the FMOD calls. + soundThread = new Thread(new ThreadStart(CommandLoop)); + soundThread.IsBackground = true; + soundThread.Name = "SoundThread"; + soundThread.Start(); + } + + private void CommandLoop() + { + SoundDelegate action; + + InitFMOD(); + if (!this.soundSystemAvailable) return; + + // Initialize the command queue. + queue = new Queue(); + + try + { + while (true) + { + // Wait for something to show up in the queue. + lock (queue) + { + while (queue.Count == 0) + { + Monitor.Wait(queue); + } + + action = queue.Dequeue(); + } + + // We have an action, so call it. + action(); + } + } + catch (Exception e) + { + System.Console.WriteLine("Sound shutdown " + e.Message); + } + } + + private void InitFMOD() { try { @@ -62,7 +102,10 @@ namespace Radegast.Media FMODExec(system.getVersion(ref version)); if (version < FMOD.VERSION.number) - throw new MediaException("You are using an old version of FMOD " + version.ToString("X") + ". This program requires " + FMOD.VERSION.number.ToString("X") + "."); + throw new MediaException("You are using an old version of FMOD " + + version.ToString("X") + + ". This program requires " + + FMOD.VERSION.number.ToString("X") + "."); // Assume no special hardware capabilities except 5.1 surround sound. FMOD.CAPS caps = FMOD.CAPS.NONE; @@ -140,16 +183,9 @@ namespace Radegast.Media public override void Dispose() { - if (parcelMusic != null) - { - if (!parcelMusic.Disposed) - parcelMusic.Dispose(); - parcelMusic = null; - } - - lock(sounds) + lock (sounds) { - for (int i=0; i public bool Disposed { get { return disposed; } } private bool disposed = false; + + /// All commands are made through queued delegate calls, so they + /// are guaranteed to take place in the same thread. FMOD requires this. + public delegate void SoundDelegate(); + + /// Queue of sound commands + /// + /// + protected static Queue queue; + + /// + /// FMOD channel controller, should not be used directly, add methods to Radegast.Media.Sound + /// + public Channel FMODChannel { get { return channel; } } + protected Channel channel = null; + + /// + /// FMOD sound object, should not be used directly, add methods to Radegast.Media.Sound + /// + public FMOD.Sound FMODSound { get { return sound; } } + protected FMOD.Sound sound = null; public FMOD.System FMODSystem { get { return system; } } /// - /// Base FMOD system object + /// Base FMOD system object, of which there is only one. /// - protected FMOD.System system = null; + protected static FMOD.System system = null; - public MediaObject(FMOD.System system) + public MediaObject() { - this.system = system; } public virtual void Dispose() { + if (sound != null) + { + sound.release(); + sound = null; + } + disposed = true; } + + public bool Active { get { return (sound != null); } } + + /// + /// Put a delegate call on the command queue. + /// + /// + protected void invoke(SoundDelegate action) + { + // Do nothing if queue not ready yet. + if (queue == null) return; + + // Put that on the queue and wake up the background thread. + lock (queue) + { + queue.Enqueue( action ); + Monitor.Pulse(queue); + } + + } + + /// + /// Common actgions for all sound types. + /// + protected float volume = 0.5f; + public float Volume { + get + { + return volume; + } + set + { + invoke(new SoundDelegate(delegate + { + MediaManager.FMODExec(channel.setVolume(value)); + volume = value; + })); + } + } } } diff --git a/Radegast/Core/Media/Speech.cs b/Radegast/Core/Media/Speech.cs new file mode 100644 index 0000000..8189ae0 --- /dev/null +++ b/Radegast/Core/Media/Speech.cs @@ -0,0 +1,157 @@ +// +// Radegast Metaverse Client +// Copyright (c) 2009, Radegast Development Team +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the application "Radegast", nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// $Id: Sound.cs 502 2010-03-14 23:13:46Z latifer $ +// +using System; +using System.Runtime.InteropServices; +using FMOD; +using OpenMetaverse; + +namespace Radegast.Media +{ + public class Speech : MediaObject + { + /// + /// Is sound currently playing + /// + public bool Playing { get { return playing; } } + private bool playing = false; + + /// + /// Is sound currently paused + /// + public bool Paused { get { return paused; } } + private bool paused = false; + + /// + /// Fired when a stream meta data is received + /// + /// Sender + /// Key, value are sent in e + public delegate void StreamInfoCallback(object sender, StreamInfoArgs e); + + private bool soundcreated = false; + private System.Timers.Timer timer; + + /// + /// Creates a new sound object + /// + /// Sound system + public Speech() + :base() + { + timer = new System.Timers.Timer(); + timer.Interval = 50d; + timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); + timer.Enabled = false; + } + + + /// + /// Releases resources of this sound object + /// + public override void Dispose() + { + if (timer != null) + { + timer.Enabled = false; + timer.Dispose(); + timer = null; + } + + if (sound != null) + { + sound.release(); + sound = null; + } + base.Dispose(); + } + + /// + /// Plays audio stream + /// + /// Name of a WAV file created by the synthesizer + public void Play(string filename) + { + if (!soundcreated) + { + MediaManager.FMODExec(system.createSound(filename, + (MODE.HARDWARE | MODE._2D | MODE.CREATESTREAM | MODE.NONBLOCKING), + ref sound)); + soundcreated = true; + timer.Enabled = true; + timer_Elapsed(null, null); + } + } + + /// + /// Toggles sound pause + /// + public void TogglePaused() + { + if (channel != null) + { + channel.getPaused(ref paused); + channel.setPaused(!paused); + } + } + + void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + OPENSTATE openstate = 0; + uint percentbuffered = 0; + bool starving = false; + + try + { + if (soundcreated) + { + MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving)); + + if (openstate == OPENSTATE.READY && channel == null) + { + MediaManager.FMODExec(system.playSound(CHANNELINDEX.FREE, sound, false, ref channel)); + MediaManager.FMODExec(channel.setVolume(volume)); + } + } + + if (system != null) + { + system.update(); + } + } + catch (Exception ex) + { + playing = paused = false; + timer.Enabled = false; + Logger.Log("Error playing speech: ", Helpers.LogLevel.Debug, ex); + } + } + } +} diff --git a/Radegast/Core/Media/Stream.cs b/Radegast/Core/Media/Stream.cs new file mode 100644 index 0000000..39a5240 --- /dev/null +++ b/Radegast/Core/Media/Stream.cs @@ -0,0 +1,215 @@ +// +// Radegast Metaverse Client +// Copyright (c) 2009, Radegast Development Team +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the application "Radegast", nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// $Id: Sound.cs 502 2010-03-14 23:13:46Z latifer $ +// +using System; +using System.Runtime.InteropServices; +using FMOD; +using OpenMetaverse; + +namespace Radegast.Media +{ + public class StreamInfoArgs : EventArgs + { + public string Key; + public string Value; + + public StreamInfoArgs(string key, string value) + { + Key = key; + Value = value; + } + } + + public class Stream : MediaObject + { + /// + /// Returns current position of the sound played in ms + /// Do not confuse with the spatial Position on other suonds. + /// + public uint Position { get { return position; } } + private uint position = 0; + + /// + /// Is sound currently playing + /// + public bool Playing { get { return playing; } } + private bool playing = false; + + /// + /// Is sound currently paused + /// + public bool Paused { get { return paused; } } + private bool paused = false; + + /// + /// Fired when a stream meta data is received + /// + /// Sender + /// Key, value are sent in e + public delegate void StreamInfoCallback(object sender, StreamInfoArgs e); + + /// + /// Fired when a stream meta data is received + /// + public event StreamInfoCallback OnStreamInfo; + + private System.Timers.Timer timer; + + /// + /// Creates a new sound object + /// + /// Sound system + public Stream() + :base() + { + timer = new System.Timers.Timer(); + timer.Interval = 50d; + timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); + timer.Enabled = false; + } + + + /// + /// Releases resources of this sound object + /// + public override void Dispose() + { + if (timer != null) + { + timer.Enabled = false; + timer.Dispose(); + timer = null; + } + + base.Dispose(); + } + + /// + /// Plays audio stream + /// + /// URL of the stream + public void PlayStream(string url) + { + if (!Active) + { + invoke( new SoundDelegate( + delegate { + MediaManager.FMODExec(system.createSound(url, + (MODE.HARDWARE | MODE._2D | MODE.CREATESTREAM | MODE.NONBLOCKING), ref sound)); + + timer.Enabled = true; + timer_Elapsed(null, null); + })); + } + } + + /// + /// Toggles sound pause + /// + public void TogglePaused() + { + invoke(new SoundDelegate(delegate + { + if (channel != null) + { + channel.getPaused(ref paused); + channel.setPaused(!paused); + } + })); + } + + void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + OPENSTATE openstate = 0; + uint percentbuffered = 0; + bool starving = false; + + try + { + if (Active) + { + // Query what the sound is doing. + MediaManager.FMODExec(sound.getOpenState(ref openstate, ref percentbuffered, ref starving)); + + // If a channel is not allocated yet, ask for one. + if (openstate == OPENSTATE.READY && channel == null) + { + // Allocate a channel and set initial volume. + MediaManager.FMODExec(system.playSound( CHANNELINDEX.FREE, sound, false, ref channel)); + volume = 0.5f; + MediaManager.FMODExec(channel.setVolume(volume)); + } + } + + if (channel != null) + { + // Do not try to get MP3 tags om Unix. It breaks. + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + for (; ; ) + { + TAG tag = new TAG(); + if (sound.getTag(null, -1, ref tag) != RESULT.OK) + { + break; + } + if (tag.datatype != TAGDATATYPE.STRING) + { + break; + } + else + { + if (OnStreamInfo != null) + try { OnStreamInfo(this, new StreamInfoArgs(tag.name.ToLower(), Marshal.PtrToStringAnsi(tag.data))); } + catch (Exception) { } + } + } + } + + // Get pause/play status + MediaManager.FMODExec(channel.getPaused(ref paused)); + MediaManager.FMODExec(channel.isPlaying(ref playing)); + MediaManager.FMODExec(channel.getPosition(ref position, TIMEUNIT.MS)); + } + + if (system != null) + { + system.update(); + } + } + catch (Exception ex) + { + playing = paused = false; + timer.Enabled = false; + Logger.Log("Error playing stream: ", Helpers.LogLevel.Debug, ex); + } + } + } +} diff --git a/Radegast/GUI/Consoles/MediaConsole.cs b/Radegast/GUI/Consoles/MediaConsole.cs index f9cb130..3dca62d 100644 --- a/Radegast/GUI/Consoles/MediaConsole.cs +++ b/Radegast/GUI/Consoles/MediaConsole.cs @@ -34,7 +34,8 @@ namespace Radegast private const int saveConfigTimeout = 3000; private bool playing; private string currentURL; - private MediaManager mngr; + //private MediaManager mngr; + private Media.Stream parcelStream; private readonly object parcelMusicLock = new object(); @@ -47,7 +48,7 @@ namespace Radegast Disposed += new EventHandler(MediaConsole_Disposed); this.instance = instance; - this.mngr = instance.MediaManager; + this.parcelStream = new Media.Stream(); s = instance.GlobalSettings; @@ -128,9 +129,9 @@ namespace Radegast lock (parcelMusicLock) { playing = false; - if (mngr.ParcelMusic != null) - mngr.ParcelMusic.Dispose(); - mngr.ParcelMusic = null; + if (parcelStream != null) + parcelStream.Dispose(); + parcelStream = null; lblStation.Tag = lblStation.Text = string.Empty; txtSongTitle.Text = string.Empty; } @@ -142,10 +143,10 @@ namespace Radegast { Stop(); playing = true; - mngr.ParcelMusic = new Sound(mngr.FMODSystem); - mngr.ParcelMusic.Volume = audioVolume; - mngr.ParcelMusic.PlayStream(currentURL); - mngr.ParcelMusic.OnStreamInfo += new Sound.StreamInfoCallback(ParcelMusic_OnStreamInfo); + parcelStream = new Media.Stream(); + parcelStream.Volume = audioVolume; + parcelStream.PlayStream(currentURL); + parcelStream.OnStreamInfo += new Media.Stream.StreamInfoCallback(ParcelMusic_OnStreamInfo); } } @@ -204,8 +205,8 @@ namespace Radegast { configTimer.Change(saveConfigTimeout, System.Threading.Timeout.Infinite); lock (parcelMusicLock) - if (mngr.ParcelMusic != null) - mngr.ParcelMusic.Volume = volAudioStream.Value/50f; + if (parcelStream != null) + parcelStream.Volume = volAudioStream.Value/50f; } private void txtAudioURL_TextChanged(object sender, EventArgs e) -- 2.11.0