diff --git a/Build/Setup.wxs b/Build/Setup.wxs index 8df2564..8fb64bf 100644 --- a/Build/Setup.wxs +++ b/Build/Setup.wxs @@ -31,6 +31,7 @@ + @@ -54,6 +55,7 @@ + diff --git a/Floe.Audio/Exceptions.cs b/Floe.Audio/Exceptions.cs new file mode 100644 index 0000000..bac3e1b --- /dev/null +++ b/Floe.Audio/Exceptions.cs @@ -0,0 +1,17 @@ +using System; + +namespace Floe.Audio +{ + [Serializable] + public class FileFormatException : Exception + { + public FileFormatException() + { + } + + public FileFormatException(string message) + : base(message) + { + } + } +} diff --git a/Floe.Audio/FifoStream.cs b/Floe.Audio/FifoStream.cs new file mode 100644 index 0000000..865895b --- /dev/null +++ b/Floe.Audio/FifoStream.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Floe.Audio +{ + public class FifoStream : Stream + { + private const int BlockSize = 8192; + + private LinkedList _blocks; + private int _readIdx, _writeIdx; + private ManualResetEventSlim _pulse; + private bool _isDisposed; + + public FifoStream() + { + _blocks = new LinkedList(); + _pulse = new ManualResetEventSlim(false); + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return true; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + + public override int Read(byte[] buffer, int offset, int count) + { + _pulse.Wait(); + if (_isDisposed) + { + return 0; + } + int total = 0; + lock (_blocks) + { + while (count > 0) + { + int written; + written = Math.Min(count, (_blocks.First == _blocks.Last ? _writeIdx : BlockSize) - _readIdx); + Array.Copy(_blocks.First.Value, _readIdx, buffer, offset, written); + count -= written; + offset += written; + _readIdx += written; + + if (_readIdx >= BlockSize) + { + _blocks.RemoveFirst(); + _readIdx = 0; + } + total += written; + + if (_blocks.First == null || + (_blocks.First == _blocks.Last && _readIdx >= _writeIdx)) + { + _pulse.Reset(); + break; + } + } + } + return total; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count < 1) + { + return; + } + + lock (_blocks) + { + while (count > 0) + { + if (_blocks.Last == null || _writeIdx >= BlockSize) + { + _blocks.AddLast(new byte[BlockSize]); + _writeIdx = 0; + } + int written = Math.Min(count, BlockSize - _writeIdx); + Array.Copy(buffer, offset, _blocks.Last.Value, _writeIdx, written); + count -= written; + offset += written; + _writeIdx += written; + _pulse.Set(); + } + } + } + + public override void Close() + { + base.Close(); + _isDisposed = true; + _pulse.Set(); + } + } +} diff --git a/Floe.Audio/FilePlayer.cs b/Floe.Audio/FilePlayer.cs new file mode 100644 index 0000000..99709d7 --- /dev/null +++ b/Floe.Audio/FilePlayer.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Linq; +using Floe.Interop; + +namespace Floe.Audio +{ + public class FilePlayer : IDisposable + { + private const int WavBufferSamples = 3000; + private static readonly byte[] WavFileSignature = { 0x52, 0x49, 0x46, 0x46 }; // RIFF + private static readonly byte[] Mp3FileSignature = { 0x49, 0x44, 0x33 }; // ID3 + + private WaveOut _waveOut; + + public event EventHandler Done; + + public FilePlayer(string fileName) + { + var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); + var sig = new byte[4]; + try + { + fileStream.Read(sig, 0, 4); + fileStream.Seek(0, SeekOrigin.Begin); + if (WavFileSignature.SequenceEqual(sig.Take(WavFileSignature.Length))) + { + var wavStream = new WavFileStream(fileStream); + _waveOut = new WaveOut(wavStream, wavStream.Format, WavBufferSamples * wavStream.Format.FrameSize); + } + else if (Mp3FileSignature.SequenceEqual(sig.Take(Mp3FileSignature.Length))) + { + var mp3Stream = new Mp3FileStream(fileStream); + _waveOut = new WaveOut(mp3Stream, mp3Stream.Format, mp3Stream.Format.BlockSize + 1); + } + else + { + throw new FileFormatException("Unrecognized file format."); + } + } + catch (EndOfStreamException) + { + throw new FileFormatException("Premature end of file."); + } + _waveOut.EndOfStream += (sender, e) => + { + var handler = this.Done; + if(handler != null) + { + handler(this, EventArgs.Empty); + } + }; + } + + public void Start() + { + _waveOut.Start(); + } + + public void Close() + { + _waveOut.Close(); + } + + public void Dispose() + { + _waveOut.Dispose(); + } + + public static void PlayAsync(string fileName, Action callback = null, object state = null) + { + var player = new FilePlayer(fileName); + player.Done += (sender, e) => + { + if (callback != null) + { + callback(state); + } + player.Close(); + }; + player.Start(); + } + } +} diff --git a/Floe.Audio/Floe.Audio.csproj b/Floe.Audio/Floe.Audio.csproj new file mode 100644 index 0000000..c5d1b0d --- /dev/null +++ b/Floe.Audio/Floe.Audio.csproj @@ -0,0 +1,85 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {BDD20714-E82B-45C9-A310-BE57BA986F44} + Library + Properties + Floe.Audio + Floe.Audio + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + ..\Floe.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3CEFFCEB-C836-47CC-8B8A-EC5655E1B5B7} + Floe.Interop + + + {1D4AD463-4355-4DA6-B8E6-0E8BB75B50D9} + Floe.Net + + + + + \ No newline at end of file diff --git a/Floe.Audio/Mp3FileStream.cs b/Floe.Audio/Mp3FileStream.cs new file mode 100644 index 0000000..9138811 --- /dev/null +++ b/Floe.Audio/Mp3FileStream.cs @@ -0,0 +1,189 @@ +using System; +using System.IO; + +using Floe.Interop; + +namespace Floe.Audio +{ + public class Mp3FileStream : Stream + { + private static readonly short[, ,] MpegBitRates = new short[2, 3, 16] + { + { + { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, + { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 } + }, + { + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 }, + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 } + } + }; + + private static readonly int[,] MpegSampleRates = new int[3, 4] + { + { + 44100, 48000, 32000, 0 + }, + { + 22050, 24000, 16000, 0 + }, + { + 11025, 12000, 8000, 0 + } + }; + + private Stream _stream; + private WaveFormatMp3 _format; + + public Mp3FileStream(string fileName) + : this(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + } + + public Mp3FileStream(Stream stream) + { + _stream = stream; + var bytes = new byte[10]; + _stream.Read(bytes, 0, 10); + if (bytes[3] != 0x03 || bytes[4] != 0x00) // v2.3.0 + { + throw new FileFormatException("Unsupported ID3 version."); + } + if (bytes[5] != 0x00) + { + throw new FileFormatException(string.Format("Unsupported ID3 flags ({0})", bytes[5])); + } + + _stream.Seek((bytes[6] << 21) | (bytes[7] << 14) | (bytes[8] << 7) | bytes[9], SeekOrigin.Current); + + int channels, bitRate, sampleRate; + int size = this.ReadFrameHeader(out channels, out sampleRate, out bitRate); + _format = new WaveFormatMp3((short)channels, sampleRate, bitRate * 1000); + } + + public WaveFormatMp3 Format { get { return _format; } } + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return false; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } + + public override int Read(byte[] buffer, int offset, int count) + { + int blockSize = this.ReadFrameHeader(); + int numRead = _stream.Read(buffer, offset, Math.Min(count, blockSize)); + if (blockSize > count) + { + _stream.Seek(blockSize - count, SeekOrigin.Current); + } + return numRead; + } + + public override void Close() + { + _stream.Close(); + } + + private int ReadFrameHeader() + { + int channels, sampleRate, bitRate; + return this.ReadFrameHeader(out channels, out sampleRate, out bitRate); + } + + private int ReadFrameHeader(out int channels, out int sampleRate, out int bitRate) + { + channels = 0; + sampleRate = 0; + bitRate = 0; + + int b; + while (true) + { + if ((b = _stream.ReadByte()) == -1) + { + return 0; + } + else if (b != 0xff) + { + continue; + } + + if ((b = _stream.ReadByte()) == -1) + { + return 0; + } + if ((b & 0xe0) != 0xe0) + { + continue; + } + + int version = (b & 0x18) >> 3; + if (version == 1) + { + continue; + } + switch (version) + { + case 0: + version = 2; + break; + case 2: + version = 1; + break; + case 3: + version = 0; + break; + } + int layer = (b & 0x6) >> 1; + if (layer == 0) + { + continue; + } + switch (layer) + { + case 1: + layer = 2; + break; + case 2: + layer = 1; + break; + case 3: + layer = 0; + break; + } + + if ((b = _stream.ReadByte()) == -1) + { + return 0; + } + bitRate = MpegBitRates[version > 0 ? 1 : 0, layer, (b >> 4)]; + if (bitRate == 0) + { + continue; + } + + sampleRate = MpegSampleRates[version, (b & 0xc) >> 2]; + if (sampleRate == 0) + { + continue; + } + int padding = (b & 0x2) > 0 ? 1 : 0; + + if ((b = _stream.ReadByte()) == -1) + { + return 0; + } + channels = ((b & 0xc0) >> 6) == 3 ? 1 : 2; + + _stream.Seek(-4, SeekOrigin.Current); + return 144 * bitRate * 1000 / sampleRate + padding; + } + } + } +} diff --git a/Floe.Audio/Properties/AssemblyInfo.cs b/Floe.Audio/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ca08882 --- /dev/null +++ b/Floe.Audio/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Floe.Audio")] +[assembly: AssemblyDescription("Floe Voice Library")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Floe")] +[assembly: AssemblyCopyright("Copyright © 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("d97923f3-80f7-4015-8266-b664b7daadff")] + +[assembly: AssemblyVersion("1.6.0.0")] +[assembly: AssemblyFileVersion("1.6.0.0")] +[assembly: CLSCompliant(true)] diff --git a/Floe.Audio/Voice/CodecInfo.cs b/Floe.Audio/Voice/CodecInfo.cs new file mode 100644 index 0000000..e152c5b --- /dev/null +++ b/Floe.Audio/Voice/CodecInfo.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Floe.Interop; + +namespace Floe.Audio +{ + /// + /// Specifies a codec to use for transmitting voice. Currently, only GSM 6.10 is supported. + /// + public enum VoiceCodec + { + /// + /// Use the GSM 6.10 codec. + /// + Gsm610 + } + + public class CodecInfo + { + private const int Gsm610PayloadType = 3; + private const int Gsm610SamplesPerBlock = 320; + private const int Gsm610BytesPerBlock = 65; + private const int MinBufferLength = 25; // milliseconds + + public int PayloadType { get; private set; } + public int EncodedBufferSize { get; private set; } + public int DecodedBufferSize { get; private set; } + public int SampleRate { get; private set; } + public int SamplesPerPacket { get; private set; } + public WaveFormat EncodedFormat { get; private set; } + public WaveFormat DecodedFormat { get; private set; } + + public CodecInfo(VoiceCodec codec, int sampleRate) + { + switch (codec) + { + case VoiceCodec.Gsm610: + this.PayloadType = Gsm610PayloadType; + this.SampleRate = sampleRate; + + int blocksPerPacket = 1; + while ((blocksPerPacket * Gsm610SamplesPerBlock * 1000) / sampleRate <= MinBufferLength) + { + blocksPerPacket++; + } + + this.SamplesPerPacket = Gsm610SamplesPerBlock * blocksPerPacket; + this.EncodedBufferSize = Gsm610BytesPerBlock * blocksPerPacket; + this.DecodedBufferSize = this.SamplesPerPacket * 2; + this.EncodedFormat = new WaveFormatGsm610(sampleRate); + this.DecodedFormat = new WaveFormatPcm(sampleRate, 16, 1); + break; + default: + throw new ArgumentException("Unsupported codec."); + } + } + + public AudioConverter GetEncoder() + { + return new AudioConverter(this.DecodedBufferSize, this.DecodedFormat, this.EncodedFormat); + } + + public AudioConverter GetDecoder() + { + return new AudioConverter(this.EncodedBufferSize, this.EncodedFormat, this.DecodedFormat); + } + } +} diff --git a/Floe.Audio/Voice/Delegates.cs b/Floe.Audio/Voice/Delegates.cs new file mode 100644 index 0000000..5caa017 --- /dev/null +++ b/Floe.Audio/Voice/Delegates.cs @@ -0,0 +1,18 @@ +using System; +using System.Net; + +namespace Floe.Audio +{ + /// + /// Defines a method signature for a handler that determines whether audio should be encoded and transmitted. + /// + /// Returns true if transmission should occur, false otherwise. + public delegate bool TransmitPredicate(); + + /// + /// Defines a method signature for a handler that determines whether to process audio received from a given endpoint. + /// + /// The endpoint from which a packet was received. + /// Returns true if the packet should be processed, false otherwise. + public delegate bool ReceivePredicate(IPEndPoint endpoint); +} diff --git a/Floe.Audio/Voice/JitterBuffer.cs b/Floe.Audio/Voice/JitterBuffer.cs new file mode 100644 index 0000000..4bdab35 --- /dev/null +++ b/Floe.Audio/Voice/JitterBuffer.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +using Floe.Interop; + +namespace Floe.Audio +{ + class JitterBuffer : Stream + { + private const int MaxBufferSize = 200; + private const int Delay = 2; // number of spans + + private AudioConverter _decoder; + private ConcurrentQueue _incoming; + private LinkedList _buffer; + private int _timestamp, _span, _lostCount, _delay; + private bool _reset; + + public JitterBuffer(CodecInfo codec) + { + _span = codec.SamplesPerPacket; + _decoder = codec.GetDecoder(); + _incoming = new ConcurrentQueue(); + _buffer = new LinkedList(); + + this.Reset(); + } + + public float Gain { get; set; } + + public void Enqueue(VoicePacket packet) + { + _incoming.Enqueue(packet); + } + + public VoicePacket Dequeue() + { + VoicePacket packet; + while (_incoming.TryDequeue(out packet)) + { + this.Insert(packet); + } + + return this.GetPacket(); + } + + private void Insert(VoicePacket packet) + { + if (!_reset) + { + var node = _buffer.First; + while (node != null) + { + if (packet.TimeStamp + _span <= _timestamp) + { + node.Value.Dispose(); + _buffer.RemoveFirst(); + } + node = node.Next; + } + } + + if (_lostCount > 20) + { + this.Reset(); + } + + if (_reset || packet.TimeStamp + _span >= _timestamp) + { + if (_buffer.Count == MaxBufferSize) + { + _buffer.First.Value.Dispose(); + _buffer.RemoveFirst(); + } + var node = _buffer.First; + while (node != null && node.Value.TimeStamp <= packet.TimeStamp) + { + node = node.Next; + } + if (node == null) + { + _buffer.AddLast(packet); + } + else + { + _buffer.AddBefore(node, packet); + } + } + } + + private VoicePacket GetPacket() + { + if (_reset) + { + var oldest = _buffer.First; + if (oldest == null) + { + return null; + } + else + { + _reset = false; + _timestamp = oldest.Value.TimeStamp; + } + } + + if (_buffer.Count > 0 && _delay > 0) + { + _delay--; + return null; + } + + var node = _buffer.First; + if (node != null && node.Value.TimeStamp > _timestamp + _span) + { + node = null; + } + + if (node != null) + { + _lostCount = 0; + var packet = node.Value; + _buffer.Remove(node); + _timestamp = node.Value.TimeStamp + _span; + return packet; + } + + _lostCount++; + _timestamp += _span; + return null; + } + + public void Reset() + { + while (_buffer.Count > 0) + { + var packet = _buffer.First.Value; + packet.Dispose(); + _buffer.RemoveFirst(); + } + _lostCount = 0; + _reset = true; + _delay = Delay; + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return false; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } + + public override int Read(byte[] buffer, int offset, int count) + { + Array.Clear(buffer, 0, count); + + var packet = this.Dequeue(); + if (packet != null) + { + count = _decoder.Convert(packet.Data, packet.Data.Length, buffer); + packet.Dispose(); + float gain = this.Gain != 0f ? (float)Math.Pow(10, this.Gain / 20f) : 1f; + WavProcess.ApplyGain(gain, buffer, count); + } + + return count; + } + } +} diff --git a/Floe.Audio/Voice/VoiceClient.cs b/Floe.Audio/Voice/VoiceClient.cs new file mode 100644 index 0000000..e4c8817 --- /dev/null +++ b/Floe.Audio/Voice/VoiceClient.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +using Floe.Interop; +using Floe.Net; + +namespace Floe.Audio +{ + /// + /// Manages a voice session connecting to one or more peers. Recorded audio is encoded and sent to all peers, and received audio is played. + /// This class uses GSM 6.10 to compress the audio stream. + /// + public sealed class VoiceClient : RtpClient, IDisposable + { + private const long DummyIPAddress = 0x03030303; + private const int DummyPort = 3333; + + private VoiceIn _voiceIn; + private Dictionary _peers; + private float _outputVolume = 1f, _outputGain = 0f; + private VoicePacketPool _pool; + private ReceivePredicate _receivePredicate; + + /// + /// Construct a new voice session. + /// + /// The transmit codec. + /// The transmit quality (usually the sample rate). + /// An optional already-bound UDP client to use. If this is null, then a new client will be constructed. + /// An optional callback to determine whether to transmit each packet. An application may + /// use logic such as PTT (push-to-talk) or an automatic peak level-based approach. By default, all packets are transmitted. + public VoiceClient(CodecInfo codec, UdpClient client = null, + TransmitPredicate transmitPredicate = null, ReceivePredicate receivePredicate = null) + : base((byte)codec.PayloadType, codec.EncodedBufferSize, new IPEndPoint(new IPAddress(DummyIPAddress), DummyPort), client) + { + _peers = new Dictionary(); + _pool = new VoicePacketPool(); + _receivePredicate = receivePredicate; + _voiceIn = new VoiceIn(codec, this, transmitPredicate); + } + + /// + /// Gets or sets the volume at which voice chat renders. This is a value between 0 and 1. + /// + public float OutputVolume + { + get { return _outputVolume; } + set + { + if (_outputVolume != value) + { + _outputVolume = value; + foreach (var peer in _peers.Values) + { + peer.Volume = value; + } + } + } + } + + /// + /// Gets or sets the amount of gain (in decibels) to apply to the output. + /// + public float OutputGain + { + get { return _outputGain; } + set + { + if (_outputGain != value) + { + _outputGain = value; + foreach (var peer in _peers.Values) + { + peer.Gain = value; + } + } + } + } + + /// + /// Gets or sets the amount of gain (in decibels) to apply to the microphone input. + /// + public float InputGain { get { return _voiceIn.Gain; } set { _voiceIn.Gain = value; } } + + /// + /// Gets the current noise level from the microphone input. This could be used, for example, to activate transmission when the user talks. + /// + public float InputLevel { get { return _voiceIn.Level; } } + + public event EventHandler Error; + + /// + /// Open the voice session and begin sending/receiving data. + /// + public override void Open() + { + base.Open(); + _voiceIn.Start(); + } + + /// + /// Close the voice session. + /// + public override void Close() + { + base.Close(); + _voiceIn.Close(); + } + + /// + /// Add a peer to the session. + /// + /// The peer's audio codec. + /// The peer's audio quality (usually the sample rate). + /// The peer's public endpoint. + public void AddPeer(VoiceCodec codec, int quality, IPEndPoint endpoint) + { + base.AddPeer(endpoint); + var peer = new VoicePeer(codec, quality, _pool); + peer.Volume = _outputVolume; + peer.Gain = _outputGain; + _peers.Add(endpoint, peer); + } + + /// + /// Remove a peer from the session. + /// + /// The peer's public endpoint. + public new void RemovePeer(IPEndPoint endpoint) + { + if (base.RemovePeer(endpoint)) + { + var peer = _peers[endpoint]; + _peers.Remove(endpoint); + peer.Dispose(); + } + } + + protected override void OnReceived(IPEndPoint endpoint, short payloadType, int seqNumber, int timeStamp, byte[] payload, int count) + { + if (_receivePredicate == null || _receivePredicate(endpoint)) + { + _peers[endpoint].Enqueue(seqNumber, timeStamp, payload, count); + } + } + + protected override void OnError(Exception ex) + { + var handler = this.Error; + if (handler != null) + { + handler(this, new ErrorEventArgs(ex)); + } + } + + /// + /// Dispose the voice session. + /// + public override void Dispose() + { + base.Dispose(); + _voiceIn.Dispose(); + foreach (var peer in _peers.Values) + { + peer.Dispose(); + } + } + + ~VoiceClient() + { + this.Dispose(); + } + } +} diff --git a/Floe.Audio/Voice/VoiceIn.cs b/Floe.Audio/Voice/VoiceIn.cs new file mode 100644 index 0000000..5f4f6ab --- /dev/null +++ b/Floe.Audio/Voice/VoiceIn.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; + +using Floe.Interop; +using Floe.Net; + +namespace Floe.Audio +{ + class VoiceIn : Stream + { + private CodecInfo _codec; + private AudioConverter _encoder; + private RtpClient _client; + private TransmitPredicate _predicate; + private int _timeStamp; + private WaveIn _waveIn; + + public VoiceIn(CodecInfo codec, RtpClient client, TransmitPredicate predicate) + { + _codec = codec; + _client = client; + _predicate = predicate; + this.InitAudio(); + } + + public float Level { get; private set; } + + public float Gain { get; set; } + + public void Start() + { + _waveIn.Start(); + } + + public override void Close() + { + base.Close(); + _waveIn.Dispose(); + } + + private void InitAudio() + { + if (_waveIn != null) + { + _waveIn.Close(); + } + _waveIn = new WaveIn(this, _codec.DecodedFormat, _codec.DecodedBufferSize); + _encoder = new AudioConverter(_codec.DecodedBufferSize, _codec.DecodedFormat, _codec.EncodedFormat); + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return false; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } + + public override void Write(byte[] buffer, int offset, int count) + { + float gain = this.Gain != 0f ? (float)Math.Pow(10, this.Gain / 20f) : 1f; + this.Level = WavProcess.ApplyGain(gain, buffer, count); + + if (_client != null) + { + count = _encoder.Convert(buffer, count, buffer); + if (count >= _client.PayloadSize && (_predicate == null || _predicate())) + { + _client.Send(_timeStamp, buffer); + } + } + _timeStamp += _codec.SamplesPerPacket; + } + } +} diff --git a/Floe.Audio/Voice/VoiceLoopback.cs b/Floe.Audio/Voice/VoiceLoopback.cs new file mode 100644 index 0000000..256301c --- /dev/null +++ b/Floe.Audio/Voice/VoiceLoopback.cs @@ -0,0 +1,82 @@ +using System; + +using Floe.Interop; + +namespace Floe.Audio +{ + /// + /// Allows users to test their microphone by hearing themselves speak. This class encodes the audio + /// using the selected codec and quality and immediate decodes and plays it back. + /// + public class VoiceLoopback : FifoStream, IDisposable + { + private WaveIn _waveIn; + private WaveOut _waveOut; + private AudioConverter _encoder; + + /// + /// Construct a new voice loopback session. + /// + /// An audio codec to encode and decode with. + /// The encoding quality (usually the sample rate). + public VoiceLoopback(VoiceCodec codec, int quality) + { + var info = new CodecInfo(codec, quality); + _waveIn = new WaveIn(this, info.DecodedFormat, info.DecodedBufferSize); + _waveOut = new WaveOut(this, info.EncodedFormat, info.EncodedBufferSize); + _encoder = new AudioConverter(info.DecodedBufferSize, info.DecodedFormat, info.EncodedFormat); + } + + /// + /// Gets or sets the render volume. + /// + public float RenderVolume { get { return _waveOut.Volume; } set { _waveOut.Volume = value; } } + + /// + /// Gets or sets the microphone input gain. + /// + public float InputGain { get; set; } + + /// + /// Gets or sets the output gain. + /// + public float OutputGain { get; set; } + + /// + /// Starts the loopback session. + /// + public void Start() + { + _waveIn.Start(); + _waveOut.Start(); + } + + /// + /// Stops the loopback session. + /// + public override void Close() + { + base.Close(); + _waveIn.Dispose(); + _waveOut.Dispose(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return base.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + float gain = this.InputGain != 0f ? (float)Math.Pow(10, this.InputGain / 20f) : 1f; + WavProcess.ApplyGain(gain, buffer, count); + + if (offset != 0) + { + throw new ArgumentException("Offsets are not supported."); + } + count = _encoder.Convert(buffer, count, buffer); + base.Write(buffer, offset, count); + } + } +} diff --git a/Floe.Audio/Voice/VoicePacket.cs b/Floe.Audio/Voice/VoicePacket.cs new file mode 100644 index 0000000..b460ca2 --- /dev/null +++ b/Floe.Audio/Voice/VoicePacket.cs @@ -0,0 +1,34 @@ +using System; + +namespace Floe.Audio +{ + class VoicePacket : IDisposable + { + private VoicePacketPool _pool; + + public int SequenceNumber { get; private set; } + public int TimeStamp { get; set; } + public byte[] Data { get; private set; } + + internal VoicePacket(VoicePacketPool pool) + { + _pool = pool; + } + + internal void Init(int seqNumber, int timeStamp, byte[] payload, int count) + { + this.SequenceNumber = seqNumber; + this.TimeStamp = timeStamp; + if (this.Data == null || this.Data.Length != count) + { + this.Data = new byte[count]; + } + Array.Copy(payload, this.Data, count); + } + + public void Dispose() + { + _pool.Recycle(this); + } + } +} diff --git a/Floe.Audio/Voice/VoicePacketPool.cs b/Floe.Audio/Voice/VoicePacketPool.cs new file mode 100644 index 0000000..a136d30 --- /dev/null +++ b/Floe.Audio/Voice/VoicePacketPool.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; + +namespace Floe.Audio +{ + class VoicePacketPool + { + private ConcurrentStack _pool = new ConcurrentStack(); + + public VoicePacketPool() + { + _pool = new ConcurrentStack(); + } + + public VoicePacket Create(int seqNumber, int timeStamp, byte[] payload, int count) + { + VoicePacket packet; + if (!_pool.TryPop(out packet)) + { + packet = new VoicePacket(this); + } + packet.Init(seqNumber, timeStamp, payload, count); + return packet; + } + + public void Recycle(VoicePacket packet) + { + _pool.Push(packet); + } + } +} diff --git a/Floe.Audio/Voice/VoicePeer.cs b/Floe.Audio/Voice/VoicePeer.cs new file mode 100644 index 0000000..aa1f4e6 --- /dev/null +++ b/Floe.Audio/Voice/VoicePeer.cs @@ -0,0 +1,41 @@ +using System; + +using Floe.Interop; + +namespace Floe.Audio +{ + class VoicePeer : IDisposable + { + private WaveOut _waveOut; + private JitterBuffer _buffer; + private CodecInfo _codec; + private VoicePacketPool _pool; + + public VoicePeer(VoiceCodec codec, int quality, VoicePacketPool pool) + { + _codec = new CodecInfo(codec, quality); + _buffer = new JitterBuffer(_codec); + _pool = pool; + this.InitAudio(); + } + + public float Volume { get { return _waveOut.Volume; } set { _waveOut.Volume = value; } } + public float Gain { get { return _buffer.Gain; } set { _buffer.Gain = value; } } + + public void Enqueue(int seqNumber, int timeStamp, byte[] payload, int count) + { + _buffer.Enqueue(_pool.Create(seqNumber, timeStamp, payload, count)); + } + + private void InitAudio() + { + _waveOut = new WaveOut(_buffer, _codec.DecodedFormat, _codec.DecodedBufferSize); + _waveOut.Start(); + } + + public void Dispose() + { + _waveOut.Dispose(); + } + } +} diff --git a/Floe.Audio/WavFileStream.cs b/Floe.Audio/WavFileStream.cs new file mode 100644 index 0000000..aad9f8e --- /dev/null +++ b/Floe.Audio/WavFileStream.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; + +using Floe.Interop; + +namespace Floe.Audio +{ + public class WavFileStream : Stream + { + private Stream _stream; + private WaveFormat _format; + + public WavFileStream(string fileName) + : this(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + } + + public WavFileStream(Stream stream) + { + _stream = stream; + var bytes = new byte[20]; + if (_stream.Read(bytes, 0, bytes.Length) < bytes.Length || + BitConverter.ToInt32(bytes, 4) == 0 || + BitConverter.ToInt32(bytes, 8) != 0x45564157 || // "WAVE" + BitConverter.ToInt32(bytes, 12) != 0x20746d66) // "fmt " + { + throw new FileFormatException("Badly formed RIFF file."); + } + var fmtBytes = new byte[BitConverter.ToInt32(bytes, 16)]; + _stream.Read(fmtBytes, 0, fmtBytes.Length); + _format = new WaveFormat(fmtBytes); + + while (_stream.Read(bytes, 0, 8) == 8) + { + var dataType = BitConverter.ToInt32(bytes, 0); + var dataSize = BitConverter.ToInt32(bytes, 4); + if (dataType == 0x61746164) + { + return; + } + _stream.Seek(dataSize, SeekOrigin.Current); + } + throw new FileFormatException("Could not find wave data."); + } + + public WaveFormat Format { get { return _format; } } + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return false; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } + + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override void Close() + { + _stream.Close(); + } + } +} diff --git a/Floe.Audio/WavProcess.cs b/Floe.Audio/WavProcess.cs new file mode 100644 index 0000000..1e6d00a --- /dev/null +++ b/Floe.Audio/WavProcess.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Floe.Audio +{ + public static class WavProcess + { + public static float ApplyGain(float gain, byte[] buffer, int count) + { + float sum = 0f; + double min = (double)short.MinValue; + double max = (double)short.MaxValue; + for (int i = 0; i < count; i += 2) + { + short sample = BitConverter.ToInt16(buffer, i); + if (gain != 1f) + { + double adj = Math.Max(min, Math.Min(max, (double)sample * gain)); + sample = (short)adj; + buffer[i] = (byte)sample; + buffer[i + 1] = (byte)(sample >> 8); + } + sum += (float)Math.Pow(2, (double)sample / (double)short.MaxValue); + } + return (float)Math.Sqrt(sum); + } + } +} diff --git a/Floe.Audio/WaveInMeter.cs b/Floe.Audio/WaveInMeter.cs new file mode 100644 index 0000000..0028697 --- /dev/null +++ b/Floe.Audio/WaveInMeter.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using Floe.Interop; + +namespace Floe.Audio +{ + public class WaveLevelEventArgs : EventArgs + { + public float Level { get; internal set; } + } + + public class WaveInMeter : IDisposable + { + private class MeterStream : Stream + { + private WaveInMeter _meter; + + public MeterStream(WaveInMeter meter) + { + _meter = meter; + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return true; } } + public override void Flush() { throw new NotImplementedException(); } + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } + + public override void Write(byte[] buffer, int offset, int count) + { + float sum = 0f; + for (int i = 0; i < count; i += 2) + { + float sample = (float)((short)(buffer[i + 1] << 8) | (short)(buffer[i])) / (float)short.MaxValue; + sum += sample * sample; + } + sum = (float)Math.Sqrt(sum); + _meter.OnLevelUpdated(sum); + return; + } + } + + private WaveIn _waveIn; + private WaveLevelEventArgs _args; + + public WaveInMeter(int samples) + { + _args = new WaveLevelEventArgs(); + var format = new WaveFormatPcm(44100, 16, 1); + _waveIn = new WaveIn(new MeterStream(this), format, format.FrameSize * samples); + _waveIn.Start(); + } + + public event EventHandler LevelUpdated; + + public void Dispose() + { + _waveIn.Dispose(); + } + + internal void OnLevelUpdated(float level) + { + var handler = this.LevelUpdated; + if (handler != null) + { + _args.Level = level; + handler(this, _args); + } + } + } +} diff --git a/Floe.Configuration/ColorsElement.cs b/Floe.Configuration/ColorsElement.cs index dd8e02d..4cabc10 100644 --- a/Floe.Configuration/ColorsElement.cs +++ b/Floe.Configuration/ColorsElement.cs @@ -190,6 +190,13 @@ public string WindowForeground set { this["windowForeground"] = value; OnPropertyChanged("WindowForeground"); } } + [ConfigurationProperty("highlight", DefaultValue = "#3399FF")] + public string Highlight + { + get { return (string)this["highlight"]; } + set { this["highlight"] = value; OnPropertyChanged("Highlight"); } + } + [ConfigurationProperty("color0", DefaultValue = "#FFFFFF")] public string Color0 { @@ -301,6 +308,13 @@ public string Color15 get { return (string)this["color15"]; } set { this["color15"] = value; OnPropertyChanged("Palette"); } } + + [ConfigurationProperty("transmit", DefaultValue = "#00FF00")] + public string Transmit + { + get { return (string)this["transmit"]; } + set { this["transmit"] = value; OnPropertyChanged("Transmit"); } + } public ChatPalette Palette { diff --git a/Floe.Configuration/TabStripPosition.cs b/Floe.Configuration/Enums.cs similarity index 100% rename from Floe.Configuration/TabStripPosition.cs rename to Floe.Configuration/Enums.cs diff --git a/Floe.Configuration/Floe.Configuration.csproj b/Floe.Configuration/Floe.Configuration.csproj index 9245100..fae244a 100644 --- a/Floe.Configuration/Floe.Configuration.csproj +++ b/Floe.Configuration/Floe.Configuration.csproj @@ -52,12 +52,15 @@ + - + + + diff --git a/Floe.Configuration/NetworkElement.cs b/Floe.Configuration/NetworkElement.cs new file mode 100644 index 0000000..ff413e5 --- /dev/null +++ b/Floe.Configuration/NetworkElement.cs @@ -0,0 +1,57 @@ +using System; +using System.Configuration; + +namespace Floe.Configuration +{ + public class NetworkElement : ConfigurationElement + { + [ConfigurationProperty("useSocks5Proxy", DefaultValue = false)] + public bool UseSocks5Proxy + { + get { return (bool)this["useSocks5Proxy"]; } + set { this["useSocks5Proxy"] = value; } + } + + [ConfigurationProperty("proxyHostname", DefaultValue = "127.0.0.1")] + public string ProxyHostname + { + get + { + if (((string)this["proxyHostname"]).Trim().Length < 1) + { + this["proxyHostname"] = "127.0.0.1"; + } + return (string)this["proxyHostname"]; + } + set { this["proxyHostname"] = value; } + } + + [ConfigurationProperty("proxyPort", DefaultValue = 1080)] + public int ProxyPort + { + get { return (int)this["proxyPort"]; } + set + { + if (value <= 0 || value > ushort.MaxValue) + { + throw new ArgumentException("The port must be within the range of 1 - 65535."); + } + this["proxyPort"] = value; + } + } + + [ConfigurationProperty("proxyUsername", DefaultValue = "")] + public string ProxyUsername + { + get { return (string)this["proxyUsername"]; } + set { this["proxyUsername"] = value; } + } + + [ConfigurationProperty("proxyPassword", DefaultValue = "")] + public string ProxyPassword + { + get { return (string)this["proxyPassword"]; } + set { this["proxyPassword"] = value; } + } + } +} diff --git a/Floe.Configuration/Properties/AssemblyInfo.cs b/Floe.Configuration/Properties/AssemblyInfo.cs index 86df790..3965cc9 100644 --- a/Floe.Configuration/Properties/AssemblyInfo.cs +++ b/Floe.Configuration/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [assembly: AssemblyTitle("Floe.Configuration.dll")] diff --git a/Floe.Configuration/SettingsSection.cs b/Floe.Configuration/SettingsSection.cs index eff4911..1aba14a 100644 --- a/Floe.Configuration/SettingsSection.cs +++ b/Floe.Configuration/SettingsSection.cs @@ -63,5 +63,26 @@ public DccElement Dcc get { return (DccElement)this["dcc"]; } set { this["dcc"] = value; } } + + [ConfigurationProperty("sounds")] + public SoundsElement Sounds + { + get { return (SoundsElement)this["sounds"]; } + set { this["sounds"] = value; } + } + + [ConfigurationProperty("network")] + public NetworkElement Network + { + get { return (NetworkElement)this["network"]; } + set { this["network"] = value; } + } + + [ConfigurationProperty("voice")] + public VoiceElement Voice + { + get { return (VoiceElement)this["voice"]; } + set { this["voice"] = value; } + } } } diff --git a/Floe.Configuration/SoundsElement.cs b/Floe.Configuration/SoundsElement.cs new file mode 100644 index 0000000..f99b2b4 --- /dev/null +++ b/Floe.Configuration/SoundsElement.cs @@ -0,0 +1,93 @@ +using System; +using System.Configuration; + +namespace Floe.Configuration +{ + public class SoundsElement : ConfigurationElement + { + private const string DefaultDing = "%SYSTEMROOT%\\Media\\Windows Ding.wav"; + + [ConfigurationProperty("isEnabled", DefaultValue=true)] + public bool IsEnabled + { + get { return (bool)this["isEnabled"]; } + set { this["isEnabled"] = value; } + } + + [ConfigurationProperty("connect", DefaultValue="")] + public string Connect + { + get { return (string)this["connect"]; } + set { this["connect"] = value; } + } + + [ConfigurationProperty("disconnect", DefaultValue=DefaultDing)] + public string Disconnect + { + get { return (string)this["disconnect"]; } + set { this["disconnect"] = value; } + } + + [ConfigurationProperty("privateMessage", DefaultValue=DefaultDing)] + public string PrivateMessage + { + get { return (string)this["privateMessage"]; } + set { this["privateMessage"] = value; } + } + + [ConfigurationProperty("dccRequest", DefaultValue=DefaultDing)] + public string DccRequest + { + get { return (string)this["dccRequest"]; } + set { this["dccRequest"] = value; } + } + + [ConfigurationProperty("dccComplete", DefaultValue="")] + public string DccComplete + { + get { return (string)this["dccComplete"]; } + set { this["dccComplete"] = value; } + } + + [ConfigurationProperty("dccError", DefaultValue = DefaultDing)] + public string DccError + { + get { return (string)this["dccError"]; } + set { this["dccError"] = value; } + } + + [ConfigurationProperty("notice", DefaultValue="")] + public string Notice + { + get { return (string)this["notice"]; } + set { this["notice"] = value; } + } + + [ConfigurationProperty("activeAlert", DefaultValue="")] + public string ActiveAlert + { + get { return (string)this["activeAlert"]; } + set { this["activeAlert"] = value; } + } + + [ConfigurationProperty("inactiveAlert", DefaultValue = DefaultDing)] + public string InactiveAlert + { + get { return (string)this["inactiveAlert"]; } + set { this["inactiveAlert"] = value; } + } + + [ConfigurationProperty("beep", DefaultValue = DefaultDing)] + public string Beep + { + get { return (string)this["beep"]; } + set { this["beep"] = value; } + } + + public string GetPathByName(string eventName) + { + var path = this[eventName] as string; + return string.IsNullOrEmpty(path) ? null : Environment.ExpandEnvironmentVariables(path); + } + } +} diff --git a/Floe.Configuration/VoiceElement.cs b/Floe.Configuration/VoiceElement.cs new file mode 100644 index 0000000..bffe223 --- /dev/null +++ b/Floe.Configuration/VoiceElement.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel; +using System.Configuration; + +namespace Floe.Configuration +{ + // These settings are deprecated. This class still exists so that people who have + // these settings in their configuration can still launch the application. + public class VoiceElement : ConfigurationElement, INotifyPropertyChanged + { + [ConfigurationProperty("quality", DefaultValue=21760)] + public int Quality + { + get { return (int)this["quality"]; } + set { this["quality"] = value; } + } + + [ConfigurationProperty("pushToTalk", DefaultValue=true)] + public bool PushToTalk + { + get { return (bool)this["pushToTalk"]; } + set { this["pushToTalk"] = value; } + } + + [ConfigurationProperty("talkKey", DefaultValue="")] + public string TalkKey + { + get { return (string)this["talkKey"]; } + set { this["talkKey"] = value; } + } + + [ConfigurationProperty("useStun", DefaultValue = true)] + public bool UseStun + { + get { return (bool)this["useStun"]; } + set { this["useStun"] = value; } + } + + [ConfigurationProperty("stunServer", DefaultValue = "stun.failurefiles.com")] + public string StunServer + { + get { return (string)this["stunServer"]; } + set { this["stunServer"] = value; } + } + + [ConfigurationProperty("altStunServer", DefaultValue = "")] + public string AltStunServer + { + get { return (string)this["altStunServer"]; } + set { this["altStunServer"] = value; } + } + + [ConfigurationProperty("playbackVolume", DefaultValue = 1f)] + public float PlaybackVolume + { + get { return (float)this["playbackVolume"]; } + set { this["playbackVolume"] = value; OnPropertyChanged("PlaybackVolume"); } + } + + [ConfigurationProperty("talkLevel", DefaultValue = 0.2f)] + public float TalkLevel + { + get { return (float)this["talkLevel"]; } + set { this["talkLevel"] = value; } + } + + [ConfigurationProperty("inputGain", DefaultValue = 0f)] + public float InputGain + { + get { return (float)this["inputGain"]; } + set { this["inputGain"] = value; OnPropertyChanged("InputGain"); } + } + + [ConfigurationProperty("outputGain", DefaultValue = 0f)] + public float OutputGain + { + get { return (float)this["outputGain"]; } + set { this["outputGain"] = value; OnPropertyChanged("OutputGain"); } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged(string name) + { + var handler = this.PropertyChanged; + if (handler != null) + { + handler(this, new PropertyChangedEventArgs(name)); + } + } + } +} diff --git a/Floe.Interop/AssemblyInfo.cpp b/Floe.Interop/AssemblyInfo.cpp new file mode 100644 index 0000000..d5fc8b1 --- /dev/null +++ b/Floe.Interop/AssemblyInfo.cpp @@ -0,0 +1,24 @@ +#include "stdafx.h" + +using namespace System; +using namespace System::Reflection; +using namespace System::Runtime::CompilerServices; +using namespace System::Runtime::InteropServices; +using namespace System::Security::Permissions; + +[assembly:AssemblyTitleAttribute("Floe.Interop")]; +[assembly:AssemblyDescriptionAttribute("Floe Interop Library")]; +[assembly:AssemblyConfigurationAttribute("")]; +[assembly:AssemblyCompanyAttribute("")]; +[assembly:AssemblyProductAttribute("Floe")]; +[assembly:AssemblyCopyrightAttribute("Copyright (c) 2011")]; +[assembly:AssemblyTrademarkAttribute("")]; +[assembly:AssemblyCultureAttribute("")]; + +[assembly:AssemblyVersionAttribute("1.6.0.0")]; + +[assembly:ComVisible(false)]; + +[assembly:CLSCompliantAttribute(true)]; + +[assembly:SecurityPermission(SecurityAction::RequestMinimum, UnmanagedCode = true)]; \ No newline at end of file diff --git a/Floe.Interop/AudioConverter.cpp b/Floe.Interop/AudioConverter.cpp new file mode 100644 index 0000000..b95dd34 --- /dev/null +++ b/Floe.Interop/AudioConverter.cpp @@ -0,0 +1,94 @@ +#include "Stdafx.h" +#include "AudioConverter.h" + +namespace Floe +{ + namespace Interop + { + AudioConverter::AudioConverter(int maxSrcSize, WaveFormat ^srcFormat, ...array ^dstFormats) + { + m_count = dstFormats->Length; + if(m_count < 1) + { + throw gcnew System::ArgumentException("At least one destination format must be specified."); + } + + m_streams = new HACMSTREAM[m_count]; + m_headers = new LPACMSTREAMHEADER[m_count]; + + int srcSize, dstSize; + for(int i = 0; i < m_count; i++) + { + ThrowOnFailure(acmStreamOpen(&m_streams[i], 0, i == 0 ? srcFormat->Data : dstFormats[i-1]->Data, dstFormats[i]->Data, 0, 0, 0, 0)); + m_headers[i] = new ACMSTREAMHEADER(); + m_headers[i]->cbStruct = sizeof(ACMSTREAMHEADER); + m_headers[i]->cbSrcLength = m_headers[i]->dwSrcUser = srcSize = i == 0 ? maxSrcSize * 2 : m_headers[i-1]->cbDstLength; + m_headers[i]->pbSrc = (LPBYTE)malloc(srcSize); + ThrowOnFailure(acmStreamSize(m_streams[i], srcSize, (LPDWORD)&dstSize, ACM_STREAMSIZEF_SOURCE)); + m_headers[i]->cbDstLength = m_headers[i]->dwDstUser = dstSize; + m_headers[i]->pbDst = (LPBYTE)malloc(dstSize); + m_headers[i]->fdwStatus = 0; + m_headers[i]->cbSrcLengthUsed = m_headers[i]->cbDstLengthUsed = 0; + ThrowOnFailure(acmStreamPrepareHeader(m_streams[i], m_headers[i], 0)); + m_headers[i]->cbSrcLengthUsed = m_headers[i]->cbSrcLength; + } + } + + int AudioConverter::Convert(IntPtr srcBuffer, int size, [Out] IntPtr &dstBuffer) + { + for(int i = 0; i < m_count; i++) + { + int remainder = m_headers[i]->cbSrcLength - m_headers[i]->cbSrcLengthUsed; + if(remainder > 0) + { + memmove(m_headers[i]->pbSrc, m_headers[i]->pbSrc + m_headers[i]->cbSrcLengthUsed, remainder); + } + m_headers[i]->cbSrcLength = i == 0 ? size : m_headers[i-1]->cbDstLengthUsed; + memcpy(m_headers[i]->pbSrc + remainder, i == 0 ? (void*)srcBuffer : m_headers[i-1]->pbDst, m_headers[i]->cbSrcLength); + m_headers[i]->cbSrcLength += remainder; + ThrowOnFailure(acmStreamConvert(m_streams[i], m_headers[i], ACM_STREAMCONVERTF_BLOCKALIGN)); + } + dstBuffer = (IntPtr)m_headers[m_count-1]->pbDst; + return m_headers[m_count-1]->cbDstLengthUsed; + } + + int AudioConverter::Convert(array^ srcBuffer, int size, array^ dstBuffer) + { + pin_ptr pSrc = &srcBuffer[0]; + IntPtr pDst; + size = this->Convert((IntPtr)pSrc, size, pDst); + if(dstBuffer->Length < size) + { + throw gcnew InteropException("Destination buffer too small."); + } + Marshal::Copy(pDst, dstBuffer, 0, size); + return size; + } + + AudioConverter::~AudioConverter() + { + if(m_headers != 0 && m_streams != 0) + { + for(int i = 0; i < m_count; i++) + { + m_headers[i]->cbSrcLength = m_headers[i]->dwSrcUser; + m_headers[i]->cbDstLength = m_headers[i]->dwDstUser; + acmStreamUnprepareHeader(m_streams[i], m_headers[i], 0); + acmStreamClose(m_streams[i], 0); + delete m_headers[i]->pbSrc; + delete m_headers[i]->pbDst; + delete m_headers[i]; + } + delete m_headers; + delete m_streams; + m_headers = 0; + m_streams = 0; + } + } + + AudioConverter::!AudioConverter() + { + this->~AudioConverter(); + } + } +} diff --git a/Floe.Interop/AudioConverter.h b/Floe.Interop/AudioConverter.h new file mode 100644 index 0000000..53becf2 --- /dev/null +++ b/Floe.Interop/AudioConverter.h @@ -0,0 +1,47 @@ +#pragma once +#include "Stdafx.h" +#include "Common.h" + +namespace Floe +{ + namespace Interop + { + using System::IntPtr; + using System::Byte; + using namespace System::Collections::Generic; + using namespace System::Runtime::InteropServices; + + public ref class AudioConverter + { + private: + HACMSTREAM *m_streams; + LPACMSTREAMHEADER *m_headers; + int m_count; + + public: + AudioConverter(int maxSrcSize, WaveFormat ^srcFormat, ...array ^dstFormats); + int Convert(IntPtr srcBuffer, int size, [Out] IntPtr &dstBuffer); + int Convert(array^ srcBuffer, int size, array^ dstBuffer); + + property int SourceBufferSize + { + int get() + { + return m_headers[0]->dwSrcUser; + } + } + + property int DestBufferSize + { + int get() + { + return m_headers[m_count - 1]->dwDstUser; + } + } + + private: + ~AudioConverter(); + !AudioConverter(); + }; + } +} \ No newline at end of file diff --git a/Floe.Interop/Common.h b/Floe.Interop/Common.h new file mode 100644 index 0000000..15d38ea --- /dev/null +++ b/Floe.Interop/Common.h @@ -0,0 +1,63 @@ +#pragma once +#include "Stdafx.h" +#include "WaveFormat.h" + +using System::String; +using System::Exception; + +namespace Floe +{ + namespace Interop + { + public ref class InteropException : Exception + { + public: + InteropException(String^ message) + : Exception(message) + { + } + }; + + public ref class InteropErrorEventArgs : System::EventArgs + { + private: + Exception ^m_exception; + + internal: + InteropErrorEventArgs(Exception ^exception) : m_exception(exception) + { + } + + public: + property Exception ^Exception + { + System::Exception ^get() + { + return m_exception; + } + } + }; + + inline void ThrowOnFailure(HRESULT hr) + { + switch(hr) + { + case MMSYSERR_ALLOCATED: + throw gcnew InteropException("The multimedia device is already in use."); + } + + if(hr != 0) + { + throw gcnew InteropException(System::String::Format("System error: {0}", hr.ToString("X"))); + } + } + + inline void ThrowOnZero(HANDLE handle) + { + if(handle == 0) + { + throw gcnew InteropException("A null handle was returned."); + } + } + } +} \ No newline at end of file diff --git a/Floe.Interop/Floe.Interop.vcxproj b/Floe.Interop/Floe.Interop.vcxproj new file mode 100644 index 0000000..fa9d01d --- /dev/null +++ b/Floe.Interop/Floe.Interop.vcxproj @@ -0,0 +1,137 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {3CEFFCEB-C836-47CC-8B8A-EC5655E1B5B7} + v4.0 + ManagedCProj + FloeAudio + + + + DynamicLibrary + true + true + Unicode + + + DynamicLibrary + false + true + Unicode + + + + + + + + + + + + + true + + + false + + + true + + + ..\Floe.snk + + + + + false + false + false + ..\Floe.snk + ..\Floe.snk + + + + Level3 + Disabled + WIN32;_DEBUG;%(PreprocessorDefinitions) + Use + + + true + Ole32.lib;Msacm32.lib;winmm.lib;%(AdditionalDependencies) + ..\Floe.snk + + + + + + + + + + + + + Level3 + WIN32;NDEBUG;%(PreprocessorDefinitions) + Use + MultiThreadedDLL + + + true + Ole32.lib;Msacm32.lib;winmm.lib;%(AdditionalDependencies) + ..\Floe.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + + + + + + + + + + \ No newline at end of file diff --git a/Floe.Interop/InputButton.h b/Floe.Interop/InputButton.h new file mode 100644 index 0000000..094e237 --- /dev/null +++ b/Floe.Interop/InputButton.h @@ -0,0 +1,210 @@ +#pragma once + +namespace Floe +{ + namespace Interop + { + public enum class InputButton + { + A = 0x41, + Add = 0x6b, + Alt = 0x40000, + Apps = 0x5d, + Attn = 0xf6, + B = 0x42, + Back = 8, + BrowserBack = 0xa6, + BrowserFavorites = 0xab, + BrowserForward = 0xa7, + BrowserHome = 0xac, + BrowserRefresh = 0xa8, + BrowserSearch = 170, + BrowserStop = 0xa9, + C = 0x43, + Cancel = 3, + Capital = 20, + CapsLock = 20, + Clear = 12, + Control = 0x20000, + ControlKey = 0x11, + Crsel = 0xf7, + D = 0x44, + D0 = 0x30, + D1 = 0x31, + D2 = 50, + D3 = 0x33, + D4 = 0x34, + D5 = 0x35, + D6 = 0x36, + D7 = 0x37, + D8 = 0x38, + D9 = 0x39, + Decimal = 110, + Delete = 0x2e, + Divide = 0x6f, + Down = 40, + E = 0x45, + End = 0x23, + Enter = 13, + EraseEof = 0xf9, + Escape = 0x1b, + Execute = 0x2b, + Exsel = 0xf8, + F = 70, + F1 = 0x70, + F10 = 0x79, + F11 = 0x7a, + F12 = 0x7b, + F13 = 0x7c, + F14 = 0x7d, + F15 = 0x7e, + F16 = 0x7f, + F17 = 0x80, + F18 = 0x81, + F19 = 130, + F2 = 0x71, + F20 = 0x83, + F21 = 0x84, + F22 = 0x85, + F23 = 0x86, + F24 = 0x87, + F3 = 0x72, + F4 = 0x73, + F5 = 0x74, + F6 = 0x75, + F7 = 0x76, + F8 = 0x77, + F9 = 120, + FinalMode = 0x18, + G = 0x47, + H = 0x48, + HanguelMode = 0x15, + HangulMode = 0x15, + HanjaMode = 0x19, + Help = 0x2f, + Home = 0x24, + I = 0x49, + IMEAccept = 30, + IMEAceept = 30, + IMEConvert = 0x1c, + IMEModeChange = 0x1f, + IMENonconvert = 0x1d, + Insert = 0x2d, + J = 0x4a, + JunjaMode = 0x17, + K = 0x4b, + KanaMode = 0x15, + KanjiMode = 0x19, + KeyCode = 0xffff, + L = 0x4c, + LaunchApplication1 = 0xb6, + LaunchApplication2 = 0xb7, + LaunchMail = 180, + LButton = 1, + LControlKey = 0xa2, + Left = 0x25, + LineFeed = 10, + LMenu = 0xa4, + LShiftKey = 160, + LWin = 0x5b, + M = 0x4d, + MButton = 4, + MediaNextTrack = 0xb0, + MediaPlayPause = 0xb3, + MediaPreviousTrack = 0xb1, + MediaStop = 0xb2, + Menu = 0x12, + Modifiers = -65536, + Multiply = 0x6a, + N = 0x4e, + Next = 0x22, + NoName = 0xfc, + None = 0, + NumLock = 0x90, + NumPad0 = 0x60, + NumPad1 = 0x61, + NumPad2 = 0x62, + NumPad3 = 0x63, + NumPad4 = 100, + NumPad5 = 0x65, + NumPad6 = 0x66, + NumPad7 = 0x67, + NumPad8 = 0x68, + NumPad9 = 0x69, + O = 0x4f, + Oem1 = 0xba, + Oem102 = 0xe2, + Oem2 = 0xbf, + Oem3 = 0xc0, + Oem4 = 0xdb, + Oem5 = 220, + Oem6 = 0xdd, + Oem7 = 0xde, + Oem8 = 0xdf, + OemBackslash = 0xe2, + OemClear = 0xfe, + OemCloseBrackets = 0xdd, + Oemcomma = 0xbc, + OemMinus = 0xbd, + OemOpenBrackets = 0xdb, + OemPeriod = 190, + OemPipe = 220, + Oemplus = 0xbb, + OemQuestion = 0xbf, + OemQuotes = 0xde, + OemSemicolon = 0xba, + Oemtilde = 0xc0, + P = 80, + Pa1 = 0xfd, + Packet = 0xe7, + PageDown = 0x22, + PageUp = 0x21, + Pause = 0x13, + Play = 250, + Print = 0x2a, + PrintScreen = 0x2c, + Prior = 0x21, + ProcessKey = 0xe5, + Q = 0x51, + R = 0x52, + RButton = 2, + RControlKey = 0xa3, + Return = 13, + Right = 0x27, + RMenu = 0xa5, + RShiftKey = 0xa1, + RWin = 0x5c, + S = 0x53, + Scroll = 0x91, + Select = 0x29, + SelectMedia = 0xb5, + Separator = 0x6c, + Shift = 0x10000, + ShiftKey = 0x10, + Sleep = 0x5f, + Snapshot = 0x2c, + Space = 0x20, + Subtract = 0x6d, + T = 0x54, + Tab = 9, + U = 0x55, + Up = 0x26, + V = 0x56, + VolumeDown = 0xae, + VolumeMute = 0xad, + VolumeUp = 0xaf, + W = 0x57, + X = 0x58, + XButton1 = 5, + XButton2 = 6, + Y = 0x59, + Z = 90, + Zoom = 0xfb, + LMouseButton = 0x80000, + RMouseButton = 0x80001, + MMouseButton = 0x80002, + MouseButton4 = 0x80003, + MouseButton5 = 0x80004 + }; + } +} diff --git a/Floe.Interop/RawInput.cpp b/Floe.Interop/RawInput.cpp new file mode 100644 index 0000000..90fae76 --- /dev/null +++ b/Floe.Interop/RawInput.cpp @@ -0,0 +1,128 @@ +#include "Stdafx.h" +#include "RawInput.h" + +namespace Floe +{ + namespace Interop + { + void RawInput::Initialize(Window ^window) + { + RAWINPUTDEVICE devices[2]; + devices[0].usUsagePage = 1; + devices[0].dwFlags = RIDEV_INPUTSINK; + devices[0].hwndTarget = (HWND)(void*)(gcnew WindowInteropHelper(window))->Handle; + devices[1] = devices[0]; + devices[0].usUsage = 6; + devices[1].usUsage = 2; + RegisterRawInputDevices(&devices[0], 2, sizeof(RAWINPUTDEVICE)); + } + + void RawInput::HandleInput(IntPtr lParam) + { + RAWINPUT input; + int size; + GetRawInputData((HRAWINPUT)(void*)lParam, RID_INPUT, 0, (PUINT)&size, sizeof(RAWINPUTHEADER)); + if(size > sizeof(RAWINPUT)) + { + return; + } + GetRawInputData((HRAWINPUT)(void*)lParam, RID_INPUT, &input, (PUINT)&size, sizeof(RAWINPUTHEADER)); + InputButton key = (InputButton)0; + bool isKeyDown = false; + switch(input.header.dwType) + { + case RIM_TYPEKEYBOARD: + key = (InputButton)input.data.keyboard.VKey; + switch(key) + { + case InputButton::ShiftKey: + key = (InputButton)MapVirtualKey(input.data.keyboard.MakeCode, MAPVK_VSC_TO_VK_EX); + break; + case InputButton::Menu: + key = (input.data.keyboard.Flags & RI_KEY_E0) > 0 ? InputButton::RMenu : InputButton::LMenu; + break; + case InputButton::ControlKey: + key = (input.data.keyboard.Flags & RI_KEY_E0) > 0 ? InputButton::RControlKey : InputButton::LControlKey; + break; + case InputButton::Enter: + key = (input.data.keyboard.Flags & RI_KEY_E0) > 0 ? InputButton::Separator : InputButton::Enter; + break; + } + isKeyDown = (input.data.keyboard.Flags & RI_KEY_BREAK) == 0; + break; + case RIM_TYPEMOUSE: + if((input.data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN) > 0) + { + key = InputButton::LMouseButton; + isKeyDown = true; + } + else if ((input.data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP) > 0) + { + key = InputButton::LMouseButton; + } + else if((input.data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN) > 0) + { + key = InputButton::RMouseButton; + isKeyDown = true; + } + else if ((input.data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP) > 0) + { + key = InputButton::RMouseButton; + } + else if((input.data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN) > 0) + { + key = InputButton::MMouseButton; + isKeyDown = true; + } + else if ((input.data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP) > 0) + { + key = InputButton::MMouseButton; + } + else if((input.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN) > 0) + { + key = InputButton::MouseButton4; + isKeyDown = true; + } + else if ((input.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP) > 0) + { + key = InputButton::MouseButton4; + } + else if((input.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN) > 0) + { + key = InputButton::MouseButton5; + isKeyDown = true; + } + else if ((input.data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP) > 0) + { + key = InputButton::MouseButton5; + } + break; + } + + if(m_eventArgs == nullptr) + { + m_eventArgs = gcnew RawInputEventArgs(); + } + m_eventArgs->Handled = false; + + if((int)key > 0) + { + m_eventArgs->Button = key; + if(isKeyDown) + { + ButtonDown(nullptr, m_eventArgs); + } + else + { + ButtonUp(nullptr, m_eventArgs); + } + } + + if(!m_eventArgs->Handled) + { + PRAWINPUT pInput = &input; + DefRawInputProc(&pInput, 1, sizeof(RAWINPUT)); + } + } + } +} diff --git a/Floe.Interop/RawInput.h b/Floe.Interop/RawInput.h new file mode 100644 index 0000000..4ba8172 --- /dev/null +++ b/Floe.Interop/RawInput.h @@ -0,0 +1,62 @@ +#pragma once +#include "Stdafx.h" +#include "InputButton.h" + +namespace Floe +{ + namespace Interop + { + using System::Windows::Window; + using System::Windows::Interop::WindowInteropHelper; + using System::IntPtr; + + public ref class RawInputEventArgs : System::EventArgs + { + private: + InputButton m_button; + bool m_handled; + + public: + property InputButton Button + { + InputButton get() + { + return m_button; + } + internal: + void set(InputButton button) + { + m_button = button; + } + } + + property bool Handled + { + bool get() + { + return m_handled; + } + void set(bool handled) + { + m_handled = handled; + } + } + + internal: + RawInputEventArgs() {} + }; + + public ref class RawInput + { + private: + static RawInputEventArgs ^m_eventArgs; + + public: + static void Initialize(Window ^window); + static void HandleInput(IntPtr lParam); + + static event System::EventHandler ^ButtonDown; + static event System::EventHandler ^ButtonUp; + }; + } +} diff --git a/Floe.Interop/Stdafx.cpp b/Floe.Interop/Stdafx.cpp new file mode 100644 index 0000000..f6d2fe6 --- /dev/null +++ b/Floe.Interop/Stdafx.cpp @@ -0,0 +1,5 @@ +// stdafx.cpp : source file that includes just the standard includes +// Floe.Audio.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" diff --git a/Floe.Interop/Stdafx.h b/Floe.Interop/Stdafx.h new file mode 100644 index 0000000..7242a04 --- /dev/null +++ b/Floe.Interop/Stdafx.h @@ -0,0 +1,11 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, +// but are changed infrequently + +#pragma once + + +#include +#include +#include +#include diff --git a/Floe.Interop/WaveFormat.h b/Floe.Interop/WaveFormat.h new file mode 100644 index 0000000..24c097e --- /dev/null +++ b/Floe.Interop/WaveFormat.h @@ -0,0 +1,211 @@ +#pragma once + +namespace Floe +{ + namespace Interop + { + using System::IntPtr; + using System::Byte; + + public ref class WaveFormat + { + private: + WAVEFORMATEX *m_format; + + public: + WaveFormat(WAVEFORMATEX *format) + { + this->Init(format); + } + + WaveFormat(IntPtr format) + { + this->Init((WAVEFORMATEX*)(void*)format); + } + + WaveFormat(array^ format) + { + pin_ptr ptr = &format[0]; + this->Init((WAVEFORMATEX*)ptr); + } + + property WAVEFORMATEX *Data + { + WAVEFORMATEX *get() + { + return m_format; + } + } + + property IntPtr Handle + { + IntPtr get() + { + return (IntPtr)m_format; + } + } + + property short FormatTag + { + short get() + { + return m_format->wFormatTag; + } + } + + property int SampleRate + { + int get() + { + return m_format->nSamplesPerSec; + } + } + + property short BitsPerSample + { + short get() + { + return m_format->wBitsPerSample; + } + } + + property short Channels + { + short get() + { + return m_format->nChannels; + } + } + + property short FrameSize + { + short get() + { + return m_format->nBlockAlign; + } + } + + property int ByteRate + { + int get() + { + return m_format->nAvgBytesPerSec; + } + } + + protected: + WaveFormat() {} + + void Init(WAVEFORMATEX *format) + { + int size = sizeof(WAVEFORMATEX) + format->cbSize; + m_format = (WAVEFORMATEX*)malloc(size); + memcpy(m_format, format, size); + } + + private: + ~WaveFormat() + { + if(m_format != 0) + { + delete m_format; + m_format = 0; + } + } + + !WaveFormat() + { + this->~WaveFormat(); + } + }; + + public ref class WaveFormatPcm : WaveFormat + { + public: + WaveFormatPcm(int sampleRate, int bitsPerSample, short channels) + { + WAVEFORMATEX format; + format.wFormatTag = WAVE_FORMAT_PCM; + format.nSamplesPerSec = sampleRate; + format.wBitsPerSample = bitsPerSample; + format.nChannels = channels; + format.cbSize = 0; + format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; + format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; + + this->Init(&format); + } + }; + + public ref class WaveFormatGsm610 : WaveFormat + { + public: + WaveFormatGsm610(int sampleRate) + { + if(sampleRate % 320 != 0) + { + throw gcnew System::ArgumentException("Invalid sample rate for GSM 6.10."); + } + + GSM610WAVEFORMAT format; + format.wfx.wFormatTag = WAVE_FORMAT_GSM610; + format.wfx.nChannels = 1; + format.wfx.nSamplesPerSec = sampleRate; + format.wfx.nAvgBytesPerSec = (sampleRate / 320) * 65; + format.wfx.nBlockAlign = 65; + format.wfx.wBitsPerSample = 0; + format.wfx.cbSize = 2; + format.wSamplesPerBlock = 320; + + this->Init((WAVEFORMATEX*)&format); + } + + property short SamplesPerBlock + { + short get() + { + return ((GSM610WAVEFORMAT*)this->Data)->wSamplesPerBlock; + } + } + }; + + public ref class WaveFormatMp3 : WaveFormat + { + public: + WaveFormatMp3(short channels, int sampleRate, int bitRate) + { + MPEGLAYER3WAVEFORMAT format; + format.wfx.wFormatTag = WAVE_FORMAT_MPEGLAYER3; + format.wfx.nChannels = channels; + format.wfx.nSamplesPerSec = sampleRate; + format.wfx.nAvgBytesPerSec = bitRate * 8 * sampleRate * channels; + format.wfx.nBlockAlign = 1; + format.wfx.wBitsPerSample = 0; + format.wfx.cbSize = MPEGLAYER3_WFX_EXTRA_BYTES; + format.wID = MPEGLAYER3_ID_MPEG; + format.fdwFlags = MPEGLAYER3_FLAG_PADDING_OFF; + format.nBlockSize = (144 * bitRate) / sampleRate; + format.nFramesPerBlock = 1; + format.nCodecDelay = 0; + + this->Init((WAVEFORMATEX*)&format); + } + + property int FramesPerBlock + { + int get() + { + return ((MPEGLAYER3WAVEFORMAT*)this->Data)->nFramesPerBlock; + } + } + + property int BlockSize + { + int get() + { + return ((MPEGLAYER3WAVEFORMAT*)this->Data)->nBlockSize; + } + } + }; + } +} diff --git a/Floe.Interop/WaveIn.cpp b/Floe.Interop/WaveIn.cpp new file mode 100644 index 0000000..c8cac15 --- /dev/null +++ b/Floe.Interop/WaveIn.cpp @@ -0,0 +1,122 @@ +#include "Stdafx.h" +#include "WaveIn.h" + +namespace Floe +{ + namespace Interop + { + WaveIn::WaveIn(Stream ^stream, WaveFormat ^format, int bufferSize) + { + m_stream = stream; + m_format = format; + m_bufferSize = bufferSize; + m_stop = gcnew AutoResetEvent(false); + } + + void WaveIn::Start() + { + m_thread = gcnew Thread(gcnew ThreadStart(this, &WaveIn::Loop)); + m_thread->IsBackground = true; + m_thread->Priority = ThreadPriority::Highest; + m_thread->Start(); + } + + void WaveIn::Pause() + { + waveInStop(m_wavHandle); + } + + void WaveIn::Resume() + { + waveInStart(m_wavHandle); + } + + void WaveIn::Close() + { + m_stop->Set(); + } + + void WaveIn::Loop() + { + using namespace System::Runtime::InteropServices; + + array ^bytes = gcnew array(m_bufferSize); + AutoResetEvent ^bufEvent = gcnew AutoResetEvent(false); + array ^handles = { m_stop, bufEvent }; + int bufIdx = 0; + HWAVEIN wavHandle; + WAVEHDR hdr[2]; + bool isInitialized = false; + + try + { + ThrowOnFailure(waveInOpen(&wavHandle, WAVE_MAPPER, m_format->Data, (int)bufEvent->Handle, 0, CALLBACK_EVENT)); + m_wavHandle = wavHandle; + for(int i = 0; i < 2; i++) + { + hdr[i].lpData = (LPSTR)new BYTE[m_bufferSize]; + hdr[i].dwBufferLength = m_bufferSize; + hdr[i].dwFlags = hdr[i].dwBytesRecorded = 0; + ThrowOnFailure(waveInPrepareHeader(wavHandle, &hdr[i], sizeof(WAVEHDR))); + } + isInitialized = true; + waveInStart(wavHandle); + + while(true) + { + switch(WaitHandle::WaitAny(handles)) + { + case 0: + return; + case 1: + for(int i = 0; i < 2; i++) + { + if((hdr[i].dwFlags & WHDR_INQUEUE) == 0) + { + int count = hdr[i].dwBytesRecorded; + if(count > 0) + { + Marshal::Copy((IntPtr)hdr[i].lpData, bytes, 0, count); + m_stream->Write(bytes, 0, count); + } + ThrowOnFailure(waveInAddBuffer(wavHandle, &hdr[i], sizeof(WAVEHDR))); + } + } + } + } + } + catch(InteropException ^ex) + { + this->Error(this, gcnew InteropErrorEventArgs(ex)); + } + finally + { + if(isInitialized) + { + waveInReset(wavHandle); + for(int i = 0; i < 2; i++) + { + waveInUnprepareHeader(wavHandle, &hdr[i], sizeof(WAVEHDR)); + delete[] (BYTE*)hdr[i].lpData; + } + waveInClose(wavHandle); + } + } + } + + WaveIn::~WaveIn() + { + this->Close(); + if(m_thread != nullptr) + { + m_thread->Join(); + m_thread = nullptr; + } + } + + WaveIn::!WaveIn() + { + this->~WaveIn(); + } + } +} diff --git a/Floe.Interop/WaveIn.h b/Floe.Interop/WaveIn.h new file mode 100644 index 0000000..499bcec --- /dev/null +++ b/Floe.Interop/WaveIn.h @@ -0,0 +1,36 @@ +#pragma once +#include "Stdafx.h" +#include "Common.h" + +namespace Floe +{ + namespace Interop + { + using namespace System::IO; + using namespace System::Threading; + + public ref class WaveIn + { + private: + Stream ^m_stream; + WaveFormat ^m_format; + Thread ^m_thread; + AutoResetEvent ^m_stop; + int m_bufferSize; + HWAVEIN m_wavHandle; + + public: + WaveIn(Stream ^stream, WaveFormat ^format, int bufferSize); + void Start(); + void Pause(); + void Resume(); + void Close(); + event System::EventHandler ^Error; + + private: + void Loop(); + ~WaveIn(); + !WaveIn(); + }; + } +} diff --git a/Floe.Interop/WaveOut.cpp b/Floe.Interop/WaveOut.cpp new file mode 100644 index 0000000..a4166a0 --- /dev/null +++ b/Floe.Interop/WaveOut.cpp @@ -0,0 +1,128 @@ +#include "Stdafx.h" +#include "WaveOut.h" + +namespace Floe +{ + namespace Interop + { + WaveOut::WaveOut(Stream ^stream, WaveFormat ^format, int bufferSize) + { + m_stream = stream; + m_format = format; + m_bufferSize = bufferSize; + m_stop = gcnew AutoResetEvent(false); + m_volume = 1.0f; + } + + void WaveOut::Start() + { + m_thread = gcnew Thread(gcnew ThreadStart(this, &WaveOut::Loop)); + m_thread->IsBackground = true; + m_thread->Priority = ThreadPriority::Highest; + m_thread->Start(); + } + + void WaveOut::Pause() + { + waveOutPause(m_wavHandle); + } + + void WaveOut::Resume() + { + waveOutRestart(m_wavHandle); + } + + void WaveOut::Close() + { + m_stop->Set(); + } + + void WaveOut::Loop() + { + using namespace System::Runtime::InteropServices; + + array ^bytes = gcnew array(m_bufferSize); + AutoResetEvent ^bufEvent = gcnew AutoResetEvent(false); + array ^handles = { m_stop, bufEvent }; + int bufIdx = 0; + HWAVEOUT wavHandle; + WAVEHDR hdr[2]; + bool isInitialized = false; + + try + { + ThrowOnFailure(waveOutOpen(&wavHandle, WAVE_MAPPER, m_format->Data, (int)bufEvent->Handle, 0, CALLBACK_EVENT)); + m_wavHandle = wavHandle; + for(int i = 0; i < 2; i++) + { + hdr[i].lpData = (LPSTR)new BYTE[m_bufferSize]; + hdr[i].dwBufferLength = m_bufferSize; + hdr[i].dwFlags = 0; + ThrowOnFailure(waveOutPrepareHeader(wavHandle, &hdr[i], sizeof(WAVEHDR))); + } + isInitialized = true; + this->Volume = m_volume; + + while(true) + { + switch(WaitHandle::WaitAny(handles)) + { + case 0: + return; + case 1: + bool eos = true; + for(int i = 0; i < 2; i++) + { + if((hdr[i].dwFlags & WHDR_INQUEUE) == 0) + { + hdr[i].dwBufferLength = (int)m_stream->Read(bytes, 0, m_bufferSize); + Marshal::Copy(bytes, 0, (IntPtr)hdr[i].lpData, hdr[i].dwBufferLength); + ThrowOnFailure(waveOutWrite(wavHandle, &hdr[i], sizeof(WAVEHDR))); + if(hdr[i].dwBufferLength > 0) + { + eos = false; + } + } + } + if(eos) + { + this->EndOfStream(this, System::EventArgs::Empty); + } + } + } + } + catch(InteropException ^ex) + { + this->Error(this, gcnew InteropErrorEventArgs(ex)); + } + finally + { + if(isInitialized) + { + waveOutReset(wavHandle); + for(int i = 0; i < 2; i++) + { + waveOutUnprepareHeader(wavHandle, &hdr[i], sizeof(WAVEHDR)); + delete[] (BYTE*)hdr[i].lpData; + } + waveOutClose(wavHandle); + } + } + } + + WaveOut::~WaveOut() + { + this->Close(); + if(m_thread != nullptr) + { + m_thread->Join(); + m_thread = nullptr; + } + } + + WaveOut::!WaveOut() + { + this->~WaveOut(); + } + } +} diff --git a/Floe.Interop/WaveOut.h b/Floe.Interop/WaveOut.h new file mode 100644 index 0000000..f83ba0b --- /dev/null +++ b/Floe.Interop/WaveOut.h @@ -0,0 +1,57 @@ +#pragma once +#include "Stdafx.h" +#include "Common.h" + +namespace Floe +{ + namespace Interop + { + using namespace System::IO; + using namespace System::Threading; + using System::Math; + + public ref class WaveOut + { + private: + Stream ^m_stream; + WaveFormat ^m_format; + Thread ^m_thread; + AutoResetEvent ^m_stop; + int m_bufferSize; + float m_volume; + HWAVEOUT m_wavHandle; + + public: + WaveOut(Stream ^stream, WaveFormat ^format, int bufferSize); + void Start(); + void Pause(); + void Resume(); + void Close(); + event System::EventHandler ^Error; + + property float Volume + { + float get() + { + DWORD vol; + waveOutGetVolume(m_wavHandle, &vol); + return (float)(vol & 0xffff) / 255.0f; + } + void set(float value) + { + m_volume = Math::Max(0.0f, Math::Min(1.0f, value)); + DWORD vol = (DWORD)(value * 0xffff); + vol |= (vol << 16); + waveOutSetVolume(m_wavHandle, vol); + } + } + + event System::EventHandler ^EndOfStream; + + private: + void Loop(); + ~WaveOut(); + !WaveOut(); + }; + } +} diff --git a/Floe.Net/Dcc/DccChat.cs b/Floe.Net/Dcc/DccChat.cs new file mode 100644 index 0000000..e2341ad --- /dev/null +++ b/Floe.Net/Dcc/DccChat.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Floe.Net +{ + /// + /// Provides event arguments for a DCC chat message event. + /// + public class DccChatEventArgs : EventArgs + { + public string Text { get; private set; } + + internal DccChatEventArgs(string text) + { + this.Text = text; + } + } + + /// + /// A class responsible for sending and receiving textual messages through a direct connection to another host. + /// + public class DccChat : DccOperation + { + private List _input = new List(512); + + /// + /// Fires when a line of text has been received from the remote host. + /// + public EventHandler MessageReceived; + + /// + /// Enqueue a line of text to be sent to the remote host. + /// + /// The line of text to send. + public void QueueMessage(string text) + { + byte[] data = Encoding.UTF8.GetBytes(text + "\u000d\u000a"); + this.QueueWrite(data, 0, data.Length); + } + + protected override void OnReceived(byte[] buffer, int count) + { + for (int i = 0; i < count; i++) + { + switch (buffer[i]) + { + case 0xa: + string input = Encoding.UTF8.GetString(_input.ToArray()); + _input.Clear(); + this.RaiseEvent(this.MessageReceived, new DccChatEventArgs(input)); + break; + case 0xd: + break; + default: + _input.Add(buffer[i]); + break; + } + } + } + } +} diff --git a/Floe.Net/Dcc/DccOperation.cs b/Floe.Net/Dcc/DccOperation.cs index 3ddaabe..c21bb1a 100644 --- a/Floe.Net/Dcc/DccOperation.cs +++ b/Floe.Net/Dcc/DccOperation.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; using System.Threading; @@ -19,14 +17,16 @@ public abstract class DccOperation : IDisposable { private const int ConnectTimeout = 60 * 1000; private const int ListenTimeout = 5 * 60 * 1000; - private const int BufferSize = 2048; + private const int BufferSize = 4096; private const int MinPort = 1024; private TcpListener _listener; private TcpClient _tcpClient; private Thread _socketThread; private ManualResetEvent _endHandle; - private Action _callback; + private ConcurrentQueue> _writeQueue; + private ManualResetEvent _writeHandle; + private SynchronizationContext _syncContext; private long _bytesTransferred; private NetworkStream _stream; @@ -34,8 +34,19 @@ public abstract class DccOperation : IDisposable public EventHandler Disconnected; public EventHandler Error; + /// + /// Gets the remote address. + /// public IPAddress Address { get; private set; } + + /// + /// Gets the remote port. + /// public int Port { get; private set; } + + /// + /// Gets the number of bytes transferred. This is typically only relevant for a file transfer operation. + /// public long BytesTransferred { get @@ -48,10 +59,12 @@ public long BytesTransferred } } - public DccOperation(Action callback = null) + protected DccOperation() { - _callback = callback; + _syncContext = SynchronizationContext.Current; _endHandle = new ManualResetEvent(false); + _writeHandle = new ManualResetEvent(false); + _writeQueue = new ConcurrentQueue>(); } /// @@ -65,11 +78,11 @@ public int Listen(int lowPort, int highPort) { if (lowPort > ushort.MaxValue || lowPort < MinPort) { - throw new ArgumentException("lowPort"); + throw new ArgumentException("Invalid port.", "lowPort"); } if (highPort > ushort.MaxValue || highPort < lowPort) { - throw new ArgumentException("highPort"); + throw new ArgumentException("Invalid port.", "highPort"); } while(true) @@ -148,7 +161,7 @@ public void Connect(IPAddress address, int port) { if (port <= 0 || port > ushort.MaxValue) { - throw new ArgumentException("port"); + throw new ArgumentException("Invalid port."); } this.Address = address; this.Port = port; @@ -169,6 +182,10 @@ public void Connect(IPAddress address, int port) this.OnError(ex); return; } + catch (NullReferenceException) + { + return; + } try { @@ -187,38 +204,31 @@ public void Connect(IPAddress address, int port) _socketThread.Start(); } - public bool Write(byte[] data, int offset, int size) + /// + /// Closes an active DCC session or cancels the listener. + /// + public void Dispose() { - var ar = _stream.BeginWrite(data, offset, size, null, null); - int index = WaitHandle.WaitAny(new[] { ar.AsyncWaitHandle, _endHandle }); - switch (index) + this.Close(); + if (_tcpClient != null) { - case 0: - _stream.EndWrite(ar); - return true; - default: - return false; + _tcpClient.Close(); + } + if (_listener != null) + { + _listener.Stop(); } } - public void Close() + protected void QueueWrite(byte[] data, int offset, int size) { - _endHandle.Set(); + _writeQueue.Enqueue(new Tuple(data, offset, size)); + _writeHandle.Set(); } - /// - /// Closes an active DCC session or cancels the listener. - /// - public void Dispose() + protected void Close() { - if (_tcpClient != null) - { - try - { - _tcpClient.Close(); - } - catch { } - } + _endHandle.Set(); } protected virtual void OnConnected() @@ -241,37 +251,17 @@ protected virtual void OnReceived(byte[] buffer, int count) { } - protected void Dispatch(Action handler, T arg) - { - if (_callback != null) - { - _callback(() => handler(arg)); - } - else - { - handler(arg); - } - } - - protected void Dispatch(Action handler) + protected virtual void OnSent(byte[] buffer, int offset, int count) { - if (_callback != null) - { - _callback(handler); - } - else - { - handler(); - } } protected void RaiseEvent(EventHandler evt, T arg) where T : EventArgs { if (evt != null) { - if (_callback != null) + if (_syncContext != null) { - _callback(() => evt(this, arg)); + _syncContext.Post((o) => evt(this, (T)o), arg); } else { @@ -284,9 +274,9 @@ protected void RaiseEvent(EventHandler evt) { if (evt != null) { - if (_callback != null) + if (_syncContext != null) { - _callback(() => evt(this, EventArgs.Empty)); + _syncContext.Post((o) => evt(this, EventArgs.Empty), null); } else { @@ -298,17 +288,35 @@ protected void RaiseEvent(EventHandler evt) private void SocketLoop() { var readBuffer = new byte[BufferSize]; + Tuple outgoing = null; + IAsyncResult arr = null, arw = null; + var handles = new WaitHandle[] { null, null, _endHandle }; try { while (_tcpClient.Connected) { - var ar = _stream.BeginRead(readBuffer, 0, BufferSize, null, null); - int idx = WaitHandle.WaitAny(new[] { ar.AsyncWaitHandle, _endHandle }); - switch (idx) + if (arr == null) + { + arr = _stream.BeginRead(readBuffer, 0, BufferSize, null, null); + } + _writeHandle.Reset(); + if (arw == null && _writeQueue.TryDequeue(out outgoing)) + { + if (outgoing.Item1 == null) + { + break; + } + arw = _stream.BeginWrite(outgoing.Item1, outgoing.Item2, outgoing.Item3, null, null); + } + handles[0] = arr.AsyncWaitHandle; + handles[1] = arw != null ? arw.AsyncWaitHandle : _writeHandle; + + switch (WaitHandle.WaitAny(handles)) { case 0: - int count = _stream.EndRead(ar); + int count = _stream.EndRead(arr); + arr = null; if (count <= 0) { return; @@ -316,8 +324,19 @@ private void SocketLoop() this.OnReceived(readBuffer, count); break; case 1: + if (arw != null) + { + _stream.EndWrite(arw); + arw = null; + this.OnSent(outgoing.Item1, outgoing.Item2, outgoing.Item3); + } + break; + case 2: + if (arw != null) + { + _stream.EndWrite(arw); + } return; - } } } diff --git a/Floe.Net/Dcc/DccSendReceiver.cs b/Floe.Net/Dcc/DccSendReceiver.cs new file mode 100644 index 0000000..7312f57 --- /dev/null +++ b/Floe.Net/Dcc/DccSendReceiver.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Net; +using System.Linq; + +namespace Floe.Net +{ + /// + /// A class responsible for receiving files from another source using the DCC XMIT protocol. + /// + public sealed class DccSendReceiver : DccOperation + { + private FileInfo _fileInfo; + private FileStream _fileStream; + + /// + /// Gets or sets a value indicating whether the specified file will be overwritten if it already exists and a resume is not possible. If this is set to false, + /// the file will be renamed. + /// + public bool ForceOverwrite { get; set; } + + /// + /// Gets the path to the file that was created. This may differ from the initial file path if it has been automatically renamed to avoid overwriting a file. + /// + public string FileSavedAs { get; private set; } + + /// + /// Construct a new DccSendReceiver. + /// + /// A reference to the file to save. If the file exists, a resume will be attempted. If the resume fails, the file will be renamed. + public DccSendReceiver(FileInfo fileInfo) + { + _fileInfo = fileInfo; + this.FileSavedAs = fileInfo.FullName; + } + + protected override void OnConnected() + { + base.OnConnected(); + + int i = 1; + string fileName = Path.GetFileNameWithoutExtension(_fileInfo.Name); + while (_fileInfo.Exists && !this.ForceOverwrite) + { + _fileInfo = new FileInfo( + Path.Combine(_fileInfo.DirectoryName, + string.Format("{0} ({1}).{2}", fileName, i++, _fileInfo.Extension))); + } + this.FileSavedAs = _fileInfo.FullName; + _fileStream = new FileStream(_fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Read); + } + + protected override void OnReceived(byte[] buffer, int count) + { + base.OnReceived(buffer, count); + + _fileStream.Write(buffer, 0, count); + this.BytesTransferred += count; + byte[] writeBuffer = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)this.BytesTransferred)); + this.QueueWrite(writeBuffer, 0, 4); + } + + protected override void OnDisconnected() + { + base.OnDisconnected(); + this.CloseFile(); + } + + protected override void OnError(Exception ex) + { + base.OnError(ex); + this.CloseFile(); + } + + private void CloseFile() + { + if (_fileStream != null) + { + _fileStream.Dispose(); + } + } + } +} diff --git a/Floe.Net/Dcc/DccSendSender.cs b/Floe.Net/Dcc/DccSendSender.cs new file mode 100644 index 0000000..3a8afd2 --- /dev/null +++ b/Floe.Net/Dcc/DccSendSender.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Net; +using System.Linq; + +namespace Floe.Net +{ + /// + /// This class is responsible for sending files via the DCC SEND protocol. Unlike most implementations, + /// this class does not care about receiving acknowledgements for each chunk of file, because that is + /// worthless. Resume is not supported. + /// + public sealed class DccSendSender : DccOperation + { + private const int SendChunkSize = 4096; + + private FileInfo _fileInfo; + private FileStream _fileStream; + private byte[] _buffer = new byte[SendChunkSize]; + + /// + /// Construct a new DccSendSender. + /// + /// A reference to the file to send. + public DccSendSender(FileInfo fileInfo) + { + _fileInfo = fileInfo; + } + + protected override void OnConnected() + { + base.OnConnected(); + + _fileStream = new FileStream(_fileInfo.FullName, FileMode.Open, FileAccess.Read); + this.WriteDataBlock(); + } + + protected override void OnSent(byte[] buffer, int offset, int count) + { + this.BytesTransferred += count; + this.WriteDataBlock(); + } + + protected override void OnDisconnected() + { + base.OnDisconnected(); + this.CloseFile(); + } + + protected override void OnError(Exception ex) + { + base.OnError(ex); + this.CloseFile(); + } + + private void CloseFile() + { + if (_fileStream != null) + { + _fileStream.Dispose(); + } + } + + private void WriteDataBlock() + { + if (_fileStream.Position < _fileStream.Length) + { + int count = _fileStream.Read(_buffer, 0, SendChunkSize); + this.QueueWrite(_buffer, 0, count); + } + else + { + this.Close(); + } + } + } +} diff --git a/Floe.Net/Dcc/DccXmitReceiver.cs b/Floe.Net/Dcc/DccXmitReceiver.cs index 1261c88..8af86bf 100644 --- a/Floe.Net/Dcc/DccXmitReceiver.cs +++ b/Floe.Net/Dcc/DccXmitReceiver.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.IO; using System.Net; @@ -40,9 +37,7 @@ public sealed class DccXmitReceiver : DccOperation /// Construct a new DccXmitReceiver. /// /// A reference to the file to save. If the file exists, a resume will be attempted. If the resume fails, the file will be renamed. - /// An optional callback used to route events to another thread. - public DccXmitReceiver(FileInfo fileInfo, Action callback = null) - : base(callback) + public DccXmitReceiver(FileInfo fileInfo) { _fileInfo = fileInfo; this.FileSavedAs = fileInfo.FullName; @@ -81,7 +76,7 @@ protected override void OnReceived(byte[] buffer, int count) this.FileSavedAs = _fileInfo.FullName; _fileStream = new FileStream(_fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Read); } - this.Write(resumeBytes, 0, 4); + this.QueueWrite(resumeBytes, 0, 4); _isTransferring = true; } } @@ -92,18 +87,6 @@ protected override void OnReceived(byte[] buffer, int count) } } - private void CloseFile() - { - if (_fileStream != null) - { - _fileStream.Dispose(); - if (_timeStamp > 0 && File.Exists(_fileInfo.FullName)) - { - File.SetLastWriteTimeUtc(_fileInfo.FullName, new DateTime(1970, 1, 1) + TimeSpan.FromSeconds(_timeStamp)); - } - } - } - protected override void OnDisconnected() { base.OnDisconnected(); @@ -115,5 +98,17 @@ protected override void OnError(Exception ex) base.OnError(ex); this.CloseFile(); } + + private void CloseFile() + { + if (_fileStream != null) + { + _fileStream.Dispose(); + if (_timeStamp > 0 && File.Exists(_fileInfo.FullName)) + { + File.SetLastWriteTimeUtc(_fileInfo.FullName, new DateTime(1970, 1, 1) + TimeSpan.FromSeconds(_timeStamp)); + } + } + } } } diff --git a/Floe.Net/Dcc/DccXmitSender.cs b/Floe.Net/Dcc/DccXmitSender.cs index 00835f5..d8553fd 100644 --- a/Floe.Net/Dcc/DccXmitSender.cs +++ b/Floe.Net/Dcc/DccXmitSender.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.IO; using System.Net; @@ -12,21 +9,20 @@ namespace Floe.Net /// public sealed class DccXmitSender : DccOperation { - private const int SendChunkSize = 2048; + private const int SendChunkSize = 4096; private bool _isTransferring = false; private FileInfo _fileInfo; private FileStream _fileStream; - private byte[] _resumeBytes = new Byte[4]; + private byte[] _resumeBytes = new byte[4]; + private byte[] _buffer = new byte[SendChunkSize]; private int _handshakeBytesReceived; /// /// Construct a new DccXmitSender. /// /// A reference to the file to send. - /// An optional callback used to route events to another thread. - public DccXmitSender(FileInfo fileInfo, Action callback = null) - : base(callback) + public DccXmitSender(FileInfo fileInfo) { _fileInfo = fileInfo; } @@ -36,7 +32,7 @@ protected override void OnConnected() base.OnConnected(); var timeStampBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)(_fileInfo.LastWriteTimeUtc - new DateTime(1970, 1, 1)).TotalSeconds)); - this.Write(timeStampBytes, 0, 4); + this.QueueWrite(timeStampBytes, 0, 4); } protected override void OnReceived(byte[] buffer, int count) @@ -58,50 +54,51 @@ protected override void OnReceived(byte[] buffer, int count) this.BytesTransferred = resume; } _isTransferring = true; - - buffer = new byte[SendChunkSize]; - try - { - while (_fileStream.Position < _fileStream.Length) - { - count = _fileStream.Read(buffer, 0, SendChunkSize); - this.BytesTransferred += count; - if (!this.Write(buffer, 0, count)) - { - return; - } - } - } - catch (IOException ex) - { - this.OnError(ex); - } - finally - { - this.Close(); - } + this.WriteDataBlock(); } } } - protected override void OnDisconnected() + protected override void OnSent(byte[] buffer, int offset, int count) { - base.OnDisconnected(); - - if (_fileStream != null) + if (_isTransferring) { - _fileStream.Dispose(); + this.BytesTransferred += count; + this.WriteDataBlock(); } } + protected override void OnDisconnected() + { + base.OnDisconnected(); + this.CloseFile(); + } + protected override void OnError(Exception ex) { base.OnError(ex); + this.CloseFile(); + } + private void CloseFile() + { if (_fileStream != null) { _fileStream.Dispose(); } } + + private void WriteDataBlock() + { + if (_fileStream.Position < _fileStream.Length) + { + int count = _fileStream.Read(_buffer, 0, SendChunkSize); + this.QueueWrite(_buffer, 0, count); + } + else + { + this.Close(); + } + } } } diff --git a/Floe.Net/Floe.Net.csproj b/Floe.Net/Floe.Net.csproj index 78ad2ba..8ebb2a8 100644 --- a/Floe.Net/Floe.Net.csproj +++ b/Floe.Net/Floe.Net.csproj @@ -47,21 +47,29 @@ + + + - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file