f@5: /* f@5: f@5: Copyright (C) 2016 Queen Mary University of London f@5: Author: Fiore Martin f@5: f@5: This file is part of Collidoscope. f@5: f@5: Collidoscope is free software: you can redistribute it and/or modify f@5: it under the terms of the GNU General Public License as published by f@5: the Free Software Foundation, either version 3 of the License, or f@5: (at your option) any later version. f@5: f@5: This program is distributed in the hope that it will be useful, f@5: but WITHOUT ANY WARRANTY; without even the implied warranty of f@5: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the f@5: GNU General Public License for more details. f@5: f@5: You should have received a copy of the GNU General Public License f@5: along with this program. If not, see . f@5: f@5: This file incorporates work covered by the following copyright and permission notice: f@5: f@5: Copyright (c) 2014, The Cinder Project f@5: f@5: This code is intended to be used with the Cinder C++ library, http://libcinder.org f@5: f@5: Redistribution and use in source and binary forms, with or without modification, are permitted provided that f@5: the following conditions are met: f@5: f@5: * Redistributions of source code must retain the above copyright notice, this list of conditions and f@5: the following disclaimer. f@5: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and f@5: the following disclaimer in the documentation and/or other materials provided with the distribution. f@5: f@5: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED f@5: WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A f@5: PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR f@5: ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED f@5: TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) f@5: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING f@5: NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE f@5: POSSIBILITY OF SUCH DAMAGE. f@5: f@5: */ f@5: f@0: #include "BufferToWaveRecorderNode.h" f@0: #include "cinder/audio/Context.h" f@0: #include "cinder/audio/Target.h" f@0: f@0: f@0: f@0: // ---------------------------------------------------------------------------------------------------- f@0: // MARK: - BufferRecorderNode f@0: // ---------------------------------------------------------------------------------------------------- f@0: f@0: namespace { f@0: f@0: const size_t DEFAULT_RECORD_BUFFER_FRAMES = 44100; f@0: f@0: void resizeBufferAndShuffleChannels(ci::audio::BufferDynamic *buffer, size_t resultNumFrames) f@0: { f@0: const size_t currentNumFrames = buffer->getNumFrames(); f@0: const size_t sampleSize = sizeof(ci::audio::BufferDynamic::SampleType); f@0: f@0: if (currentNumFrames < resultNumFrames) { f@0: // if expanding, resize and then shuffle. Make sure to get the data pointer after the resize. f@0: buffer->setNumFrames(resultNumFrames); f@0: float *data = buffer->getData(); f@0: f@0: for (size_t ch = 1; ch < buffer->getNumChannels(); ch++) { f@0: const size_t numZeroFrames = resultNumFrames - currentNumFrames; f@0: const float *currentChannel = &data[ch * currentNumFrames]; f@0: float *resultChannel = &data[ch * resultNumFrames]; f@0: f@0: memmove(resultChannel, currentChannel, currentNumFrames * sampleSize); f@0: memset(resultChannel - numZeroFrames, 0, numZeroFrames * sampleSize); f@0: } f@0: } f@0: else if (currentNumFrames > resultNumFrames) { f@0: // if shrinking, shuffle first and then resize. f@0: float *data = buffer->getData(); f@0: f@0: for (size_t ch = 1; ch < buffer->getNumChannels(); ch++) { f@0: const float *currentChannel = &data[ch * currentNumFrames]; f@0: float *resultChannel = &data[ch * resultNumFrames]; f@0: f@0: memmove(resultChannel, currentChannel, currentNumFrames * sampleSize); f@0: } f@0: f@0: const size_t numZeroFrames = (currentNumFrames - resultNumFrames) * buffer->getNumChannels(); f@0: memset(data + buffer->getSize() - numZeroFrames, 0, numZeroFrames * sampleSize); f@0: f@0: buffer->setNumFrames(resultNumFrames); f@0: } f@0: } f@0: f@0: } f@0: f@0: f@0: BufferToWaveRecorderNode::BufferToWaveRecorderNode( std::size_t numChunks, double numSeconds ) f@0: : SampleRecorderNode( Format().channels( 1 ) ), f@0: mLastOverrun( 0 ), f@0: mNumChunks( numChunks ), f@0: mNumSeconds( numSeconds ), f@0: mRingBuffer( numChunks ), f@0: mChunkMaxAudioVal( kMinAudioVal ), f@0: mChunkMinAudioVal( kMaxAudioVal ), f@0: mChunkSampleCounter( 0 ), f@0: mChunkIndex( 0 ) f@0: { f@0: f@0: } f@0: f@0: void BufferToWaveRecorderNode::initialize() f@0: { f@0: // adjust recorder buffer to match channels once initialized, since they could have changed since construction. f@0: bool resize = mRecorderBuffer.getNumFrames() != 0; f@0: mRecorderBuffer.setNumChannels( getNumChannels() ); f@0: f@0: // lenght of buffer is = number of seconds * sample rate f@0: initBuffers( size_t( mNumSeconds * (double)getSampleRate() ) ); f@0: f@0: // How many samples each chunk contains. That is it calculates the min and max of f@0: // This is calculated here and not in the initializer list because it uses getNumFrames() f@0: // FIXME probably could be done in constructor body f@0: mNumSamplesPerChunk = std::lround( float( getNumFrames() ) / mNumChunks ); f@0: f@0: // if the buffer had already been resized, zero out any possibly existing data. f@0: if( resize ) f@0: mRecorderBuffer.zero(); f@0: f@0: mEnvRampLen = kRampTime * getSampleRate(); f@0: mEnvDecayStart = mRecorderBuffer.getNumFrames() - mEnvRampLen; f@0: if ( mEnvRampLen <= 0 ){ f@0: mEnvRampRate = 0; f@0: } f@0: else{ f@0: mEnvRampRate = 1.0f / mEnvRampLen; f@0: } f@0: } f@0: f@0: void BufferToWaveRecorderNode::initBuffers(size_t numFrames) f@0: { f@0: mRecorderBuffer.setSize( numFrames, getNumChannels() ); f@0: mCopiedBuffer = std::make_shared( numFrames, getNumChannels() ); f@0: } f@0: f@0: void BufferToWaveRecorderNode::start() f@0: { f@0: mWritePos = 0; f@0: mChunkIndex = 0; f@0: enable(); f@0: } f@0: f@0: void BufferToWaveRecorderNode::stop() f@0: { f@0: disable(); f@0: } f@0: f@0: void BufferToWaveRecorderNode::setNumSeconds(double numSeconds, bool shrinkToFit) f@0: { f@0: setNumFrames(size_t(numSeconds * (double)getSampleRate()), shrinkToFit); f@0: } f@0: f@0: double BufferToWaveRecorderNode::getNumSeconds() const f@0: { f@0: return (double)getNumFrames() / (double)getSampleRate(); f@0: } f@0: f@0: void BufferToWaveRecorderNode::setNumFrames(size_t numFrames, bool shrinkToFit) f@0: { f@0: if (mRecorderBuffer.getNumFrames() == numFrames) f@0: return; f@0: f@0: std::lock_guard lock(getContext()->getMutex()); f@0: f@0: if (mWritePos != 0) f@0: resizeBufferAndShuffleChannels(&mRecorderBuffer, numFrames); f@0: else f@0: mRecorderBuffer.setNumFrames(numFrames); f@0: f@0: if (shrinkToFit) f@0: mRecorderBuffer.shrinkToFit(); f@0: } f@0: f@0: ci::audio::BufferRef BufferToWaveRecorderNode::getRecordedCopy() const f@0: { f@0: // first grab the number of current frames, which may be increasing as the recording continues. f@0: size_t numFrames = mWritePos; f@0: mCopiedBuffer->setSize(numFrames, mRecorderBuffer.getNumChannels()); f@0: f@0: mCopiedBuffer->copy(mRecorderBuffer, numFrames); f@0: return mCopiedBuffer; f@0: } f@0: f@0: void BufferToWaveRecorderNode::writeToFile(const ci::fs::path &filePath, ci::audio::SampleType sampleType) f@0: { f@0: size_t currentWritePos = mWritePos; f@0: ci::audio::BufferRef copiedBuffer = getRecordedCopy(); f@0: f@0: ci::audio::TargetFileRef target = ci::audio::TargetFile::create(filePath, getSampleRate(), getNumChannels(), sampleType); f@0: target->write(copiedBuffer.get(), currentWritePos); f@0: } f@0: f@0: uint64_t BufferToWaveRecorderNode::getLastOverrun() f@0: { f@0: uint64_t result = mLastOverrun; f@0: mLastOverrun = 0; f@0: return result; f@0: } f@0: f@0: f@0: void BufferToWaveRecorderNode::process(ci::audio::Buffer *buffer) f@0: { f@0: size_t writePos = mWritePos; f@0: size_t numWriteFrames = buffer->getNumFrames(); f@0: f@0: if ( writePos == 0 ){ f@0: RecordWaveMsg msg = makeRecordWaveMsg( Command::WAVE_START, 0, 0, 0 ); f@0: mRingBuffer.write( &msg, 1 ); f@0: f@0: // reset everything f@0: mChunkMinAudioVal = kMaxAudioVal; f@0: mChunkMaxAudioVal = kMinAudioVal; f@0: mChunkSampleCounter = 0; f@0: mChunkIndex = 0; f@0: mEnvRamp = 0.0f; f@0: } f@0: f@0: // if buffer has too many frames (because we're nearly at the end or at the end ) f@0: // of mRecoderBuffer then numWriteFrames becomes the number of samples left to f@0: // fill mRecorderBuffer. Which is 0 if the buffer is at the end. f@0: if ( writePos + numWriteFrames > mRecorderBuffer.getNumFrames() ) f@0: numWriteFrames = mRecorderBuffer.getNumFrames() - writePos; f@0: f@0: if ( numWriteFrames <= 0 ) f@0: return; f@0: f@0: f@0: // apply envelope to the buffer at the edges to avoid clicks f@0: if ( writePos < mEnvRampLen ){ // beginning of wave f@0: for ( size_t i = 0; i < std::min( mEnvRampLen, numWriteFrames ); i++ ){ f@0: buffer->getData()[i] *= mEnvRamp; f@0: mEnvRamp += mEnvRampRate; f@0: if ( mEnvRamp > 1.0f ) f@0: mEnvRamp = 1.0f; f@0: } f@0: } f@0: else if ( writePos + numWriteFrames > mEnvDecayStart ){ // end of wave f@0: for ( size_t i = std::max( writePos, mEnvDecayStart ) - writePos; i < numWriteFrames; i++ ){ f@0: buffer->getData()[i] *= mEnvRamp; f@0: mEnvRamp -= mEnvRampRate; f@0: if ( mEnvRamp < 0.0f ) f@0: mEnvRamp = 0.0f; f@0: } f@0: } f@0: f@0: f@0: mRecorderBuffer.copyOffset(*buffer, numWriteFrames, writePos, 0); f@0: f@0: if ( numWriteFrames < buffer->getNumFrames() ) f@0: mLastOverrun = getContext()->getNumProcessedFrames(); f@0: f@0: /* find max and minimum of this buffer */ f@0: for ( size_t i = 0; i < numWriteFrames; i++ ){ f@0: f@0: if ( buffer->getData()[i] < mChunkMinAudioVal ){ f@0: mChunkMinAudioVal = buffer->getData()[i]; f@0: } f@0: f@0: if ( buffer->getData()[i] > mChunkMaxAudioVal ){ f@0: mChunkMaxAudioVal = buffer->getData()[i]; f@0: } f@0: f@0: if ( mChunkSampleCounter >= mNumSamplesPerChunk // if collected enough samples f@0: || writePos + i >= mRecorderBuffer.getNumFrames() - 1 ){ // or at the end of recorder buffer f@0: // send chunk to GUI f@0: size_t chunkIndex = mChunkIndex.fetch_add( 1 ); f@0: f@0: RecordWaveMsg msg = makeRecordWaveMsg( Command::WAVE_CHUNK, chunkIndex, mChunkMinAudioVal, mChunkMaxAudioVal ); f@0: mRingBuffer.write( &msg, 1 ); f@0: f@0: // reset chunk info f@0: mChunkMinAudioVal = kMaxAudioVal; f@0: mChunkMaxAudioVal = kMinAudioVal; f@0: mChunkSampleCounter = 0; f@0: } f@0: else{ f@0: mChunkSampleCounter++; f@0: } f@0: } f@0: f@0: // check if write position has been reset by the GUI thread, if not write new value f@0: const size_t writePosNew = writePos + numWriteFrames; f@0: mWritePos.compare_exchange_strong( writePos, writePosNew ); f@0: f@0: } f@0: f@0: f@0: const float BufferToWaveRecorderNode::kMinAudioVal = -1.0f; f@0: const float BufferToWaveRecorderNode::kMaxAudioVal = 1.0f; f@0: const float BufferToWaveRecorderNode::kRampTime = 0.02; f@0: f@0: