Last week I posted a sample voice recorder on CodeProject. The application would buffer the entire recording in memory before writing it to a file. A rather astute reader asked me what would happen if the user let the recording go long enough to fill up memory. The answer to that question is the application would crash due to an exception being trhown when it fails to allocate more memory and all of the recordingwould be lost. I had already been thinking of a sime reusable solution for doing this but I also offered to the user the following code sample to handle streaming directly to IsolatedStorage.
My two goals in writing it were to keep it simple and keep it portable/reusable. As far as usage goes I can’t think of any ways to make it any easier.
//To start a recording StreamingRecorder myRecorder = new StreamingRecorder(); myRecorder.Start("myFileName"); //To stop a recording(); myRecorder.Stop();
After the code has run you will have a WAVE file with a proper header ready to be consumed by a
SoundEffect
, MediaElement
, or whatever it is that you want to do with it.
In implementing this I must say that I have a hiher appreciation for how
MediaElement
‘s interface is designed. The starting and stopping process are not immediate. In otherwords when you call Start()
or Stop()
it is not until a few moments later that the request is fully processed. Because of the asynchronous nature of these processes I’ve implemented the event RecordingStateChanged
and the property RecordingState
so that I would know when a state change was complete. If you are familiar with the media element class then your recognize the similarity of this pattern.I’ll go into further details on how this works along with implemeting some other functionality (such as a
Pause
method) in a later post. But the code is in a working state now so I’m sharing it. 🙂Here is the source:
public class StreamingRecorder :INotifyPropertyChanged, IDisposable { object SyncLock = new object(); private Queue<MemoryStream> _availablBufferQueue; private Queue<MemoryStream> _writeBufferQueue; private int _bufferCount; private byte[] _audioBuffer; //private int _currentRecordingBufferIndex; private TimeSpan _bufferDuration; private int _bufferSize; private Stream _outputStream; private Microphone _currentMicrophone; private bool _ownsStream = false; private long _startPosition; public StreamingRecorder(TimeSpan? bufferDuration = null, int bufferCount=2) { _bufferDuration = bufferDuration.HasValue ? bufferDuration.Value : TimeSpan.FromSeconds(0); _bufferCount = bufferCount; _currentMicrophone= Microphone.Default; } private MemoryStream CurrentBuffer { get; set; } public void Start(string fileName) { var isoStore = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication(); var targetFile = isoStore.OpenFile(fileName, FileMode.Create); Start(targetFile, true); } public void Start(Stream outputStream, bool ownsStream=false) { _outputStream = outputStream; _ownsStream = ownsStream; _startPosition = outputStream.Position; Size = 0; //Create our recording buffers _availablBufferQueue = new Queue<MemoryStream>(); _writeBufferQueue = new Queue<MemoryStream>(); _audioBuffer = new byte[_currentMicrophone.GetSampleSizeInBytes(_currentMicrophone.BufferDuration)]; _bufferSize = _currentMicrophone.GetSampleSizeInBytes(_bufferDuration + _currentMicrophone.BufferDuration); for (var i = 0; i < _bufferCount; ++i) { _availablBufferQueue.Enqueue(new MemoryStream(_bufferSize)); } CurrentBuffer = _availablBufferQueue.Dequeue(); //Stuff a bogus wave header in the output stream as a space holder. //we will come back and make it valid later. For now the size is invalid. //I could have just as easily stuffed any set of values here as long as //the size of those values equaled 0x2C WaveHeaderWriter.WriteHeader(CurrentBuffer, -1, 1, _currentMicrophone.SampleRate); Size += (int)CurrentBuffer.Position; //Subscribe to the Microphone's buffer ready event and start listening. _currentMicrophone.BufferReady += new EventHandler<EventArgs>(_currentMicrophone_BufferReady); _currentMicrophone.Start(); } void _currentMicrophone_BufferReady(object sender, EventArgs e) { _currentMicrophone.GetData(_audioBuffer); //If the recorder is paused (not implemented) then don't add this audio chunk to // the output. If HasFlushed is set then the recording is actually ready to shut //down and we shouldn't accumulate anything more. if ((CurrentState != RecordingState.Paused)) { //Append the audio chunk to our current buffer CurrentBuffer.Write(_audioBuffer, 0, _audioBuffer.Length); //Increment the size of the recording. Size += _audioBuffer.Length; //If the buffer is full or if we are shutting down then we need to submit //the buffer to be written to the output stream. if ((CurrentBuffer.Length > _bufferSize)||(CurrentState==RecordingState.Stopping)) { SubmitToWriteBuffer(CurrentBuffer); //If we were shutting down then set a flag so that it is known that the last audio //chunk has been written. if (CurrentState == RecordingState.Stopping) { _currentMicrophone.Stop(); _currentMicrophone.BufferReady -= _currentMicrophone_BufferReady; } CurrentBuffer = _availablBufferQueue.Count > 0 ? _availablBufferQueue.Dequeue() : new MemoryStream(); } } } // CurrentState - generated from ObservableField snippet - Joel Ivory Johnson private RecordingState _currentState; public RecordingState CurrentState { get { return _currentState; } set { if (_currentState != value) { _currentState = value; OnPropertyChanged("CurrentState"); OnRecordingStateChanged(value); } } } //----- void WriteData(object a ) { lock(SyncLock) { while (_writeBufferQueue.Count > 0) { var item = _writeBufferQueue.Dequeue(); var buffer = item.GetBuffer(); _outputStream.Write(buffer, 0,(int) item.Length); item.SetLength(0); _availablBufferQueue.Enqueue(item); if (CurrentState == RecordingState.Stopping) { //Correct the information in the wave header. After it is //written set the file pointer back to the end of the file. long prePosition = _outputStream.Position; _outputStream.Seek(_startPosition, SeekOrigin.Begin); WaveHeaderWriter.WriteHeader(_outputStream,Size-44,1,_currentMicrophone.SampleRate); _outputStream.Seek(prePosition, SeekOrigin.Begin); _outputStream.Flush(); if (_ownsStream) _outputStream.Close(); CurrentState = RecordingState.Stopped; } } } } void SubmitToWriteBuffer(MemoryStream target) { //Do the writing on another thread so that processing on this thread can continue. _writeBufferQueue.Enqueue(target); ThreadPool.QueueUserWorkItem(new WaitCallback(WriteData)); } public void Pause() { if ((CurrentState != RecordingState.Paused) && (CurrentState != RecordingState.Recording)) { throw new Exception("you can't pause if you are not recording"); } CurrentState = RecordingState.Paused; } public void Stop() { CurrentState = RecordingState.Stopping; } // Size - generated from ObservableField snippet - Joel Ivory Johnson private int _size; public int Size { get { return _size; } set { if (_size != value) { _size = value; OnPropertyChanged("Size"); } } } //----- public long RemainingSpace { get { return System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication().AvailableFreeSpace; } } public TimeSpan RecordingDuration { get { return _currentMicrophone.GetSampleDuration((int)Size); } } public TimeSpan RemainingRecordingTime { get { return _currentMicrophone.GetSampleDuration((int)RemainingSpace); } } //------- public event EventHandler<RecordingStateChangedEventArgs> RecordingStateChanged; protected void OnRecordingStateChanged(RecordingState newState) { if(RecordingStateChanged!=null) { RecordingStateChanged(this, new RecordingStateChangedEventArgs(){NewState = newState}); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public void Dispose() { Stop(); } }