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: