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: