#region CPL License /* Nuclex Framework Copyright (C) 2002-2007 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.IO; namespace Nuclex.Support.Collections { /// Specialized memory stream for ring buffers /// /// This ring buffer class is specialized for binary data and attempts to achieve /// optimal efficiency when storing and retrieving chunks of several bytes /// at once. Typical use cases include audio and network buffers where one party /// is responsible for refilling the buffer at regular intervals while the other /// constantly streams data out of it. /// public class RingMemoryStream : Stream { /// Initializes a new ring memory stream /// Maximum capacity of the stream public RingMemoryStream(int capacity) { this.ringBuffer = new MemoryStream(capacity); this.ringBuffer.SetLength(capacity); this.empty = true; } /// Maximum amount of data that will fit into the ring memory stream /// /// Thrown if the new capacity is too small for the data already contained /// in the ring buffer. /// public long Capacity { get { return this.ringBuffer.Length; } set { int length = (int)Length; if(value < length) throw new ArgumentOutOfRangeException( "New capacity is less than the stream's length" ); // This could be done in a more efficient manner than just replacing // the entire buffer, but since this operation will probably be only called // once during the lifetime of the application, if at all, I don't see // the need to optimize it... MemoryStream newBuffer = new MemoryStream((int)value); newBuffer.SetLength(value); if(length > 0) Read(newBuffer.GetBuffer(), 0, length); this.ringBuffer.Close(); // Equals dispose of the old buffer this.ringBuffer = newBuffer; this.startIndex = 0; this.endIndex = length; } } /// Whether it's possible to read from this stream public override bool CanRead { get { return true; } } /// Whether this stream supports random access public override bool CanSeek { get { return false; } } /// Whether it's possible to write into this stream public override bool CanWrite { get { return true; } } /// Flushes the buffers and writes down unsaved data public override void Flush() { } /// Current length of the stream public override long Length { get { if((this.endIndex > this.startIndex) || this.empty) { return this.endIndex - this.startIndex; } else { return this.ringBuffer.Length - this.startIndex + this.endIndex; } } } /// Current cursor position within the stream /// Always public override long Position { get { throw new NotSupportedException("The ring buffer does not support seeking"); } set { throw new NotSupportedException("The ring buffer does not support seeking"); } } /// Reads data from the beginning of the stream /// Buffer in which to store the data /// Starting index at which to begin writing the buffer /// Number of bytes to read from the stream /// Die Number of bytes actually read public override int Read(byte[] buffer, int offset, int count) { // The end index lies behind the start index (usual case), so the // ring memory is not fragmented. Example: |-----<#######>-----| if((this.startIndex < this.endIndex) || this.empty) { // The Stream interface requires us to return less than the requested // number of bytes if we don't have enough data count = Math.Min(count, this.endIndex - this.startIndex); if(count > 0) { this.ringBuffer.Position = this.startIndex; this.ringBuffer.Read(buffer, offset, count); this.startIndex += count; if(this.startIndex == this.endIndex) setEmpty(); } // If the end index lies before the start index, the data in the // ring memory stream is fragmented. Example: |#####>-------<#####| } else { int linearAvailable = (int)this.ringBuffer.Length - this.startIndex; // Will this read process cross the end of the ring buffer, requiring us to // read the data in 2 steps? if(count > linearAvailable) { // The Stream interface requires us to return less than the requested // number of bytes if we don't have enough data count = Math.Min(count, linearAvailable + this.endIndex); this.ringBuffer.Position = this.startIndex; this.ringBuffer.Read(buffer, offset, linearAvailable); this.ringBuffer.Position = 0; this.startIndex = count - linearAvailable; this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex); // Nope, the amount of requested data can be read in one piece without // crossing the end of the ring buffer } else { this.ringBuffer.Position = this.startIndex; this.ringBuffer.Read(buffer, offset, count); this.startIndex += count; } if(this.startIndex == this.endIndex) setEmpty(); } return count; } /// Appends data to the end of the stream /// Buffer containing the data to append /// Starting index of the data in the buffer /// Number of bytes to write to the stream /// When the ring buffer is full public override void Write(byte[] buffer, int offset, int count) { // The end index lies behind the start index (usual case), so the // ring memory is not fragmented. Example: |-----<#######>-----| if((this.startIndex < this.endIndex) || this.empty) { int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex); // If the data to be written would cross the ring memory stream's end, // we have to check that there's enough space at the beginning of the // stream to contain the remainder of the data. if(count > linearAvailable) { if(count > (linearAvailable + this.startIndex)) throw new OverflowException("Data does not fit in buffer"); this.ringBuffer.Position = this.endIndex; this.ringBuffer.Write(buffer, offset, linearAvailable); this.ringBuffer.Position = 0; this.endIndex = count - linearAvailable; this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex); // All data can be appended at the current stream position without // crossing the ring memory stream's end } else { this.ringBuffer.Position = this.endIndex; this.ringBuffer.Write(buffer, offset, count); this.endIndex += count; } this.empty = false; // If the end index lies before the start index, the ring memory stream // has been fragmented. Hence, this means the gap into which we are about // to write cannot be fragmented. Example: |#####>-------<#####| } else { if(count > (this.startIndex - this.endIndex)) throw new OverflowException("Data does not fit in buffer"); // Because the gap isn't fragmented, we can be sure that a single // write call will suffice. this.ringBuffer.Position = this.endIndex; this.ringBuffer.Write(buffer, offset, count); this.endIndex += count; } } /// Jumps to the specified location within the stream /// Position to jump to /// Origin towards which to interpret the offset /// The new offset within the stream /// Always public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException("The ring buffer does not support seeking"); } /// Changes the length of the stream /// New length to resize the stream to /// Always public override void SetLength(long value) { throw new NotSupportedException("This operation is not supported"); } /// Resets the stream to its empty state private void setEmpty() { this.empty = true; this.startIndex = 0; this.endIndex = 0; } /// Internal stream containing the ring buffer data private MemoryStream ringBuffer; /// Start index of the data within the ring buffer private int startIndex; /// End index of the data within the ring buffer private int endIndex; /// Whether the ring buffer is empty /// /// This field is required to differentiate between the ring buffer being /// filled to the limit and being totally empty in the case that /// the start index and the end index are the same. /// private bool empty; } } // namespace Nuclex.Support.Collections