f@5
|
1 /*
|
f@5
|
2
|
f@5
|
3 Copyright (C) 2016 Queen Mary University of London
|
f@5
|
4 Author: Fiore Martin
|
f@5
|
5
|
f@5
|
6 This file is part of Collidoscope.
|
f@5
|
7
|
f@5
|
8 Collidoscope is free software: you can redistribute it and/or modify
|
f@5
|
9 it under the terms of the GNU General Public License as published by
|
f@5
|
10 the Free Software Foundation, either version 3 of the License, or
|
f@5
|
11 (at your option) any later version.
|
f@5
|
12
|
f@5
|
13 This program is distributed in the hope that it will be useful,
|
f@5
|
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
f@5
|
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
f@5
|
16 GNU General Public License for more details.
|
f@5
|
17
|
f@5
|
18 You should have received a copy of the GNU General Public License
|
f@5
|
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
f@5
|
20
|
f@5
|
21 This file incorporates work covered by the following copyright and permission notice:
|
f@5
|
22
|
f@5
|
23 Copyright (c) 2014, The Cinder Project
|
f@5
|
24
|
f@5
|
25 This code is intended to be used with the Cinder C++ library, http://libcinder.org
|
f@5
|
26
|
f@5
|
27 Redistribution and use in source and binary forms, with or without modification, are permitted provided that
|
f@5
|
28 the following conditions are met:
|
f@5
|
29
|
f@5
|
30 * Redistributions of source code must retain the above copyright notice, this list of conditions and
|
f@5
|
31 the following disclaimer.
|
f@5
|
32 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
|
f@5
|
33 the following disclaimer in the documentation and/or other materials provided with the distribution.
|
f@5
|
34
|
f@5
|
35 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
|
f@5
|
36 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
f@5
|
37 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
f@5
|
38 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
f@5
|
39 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
f@5
|
40 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
f@5
|
41 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
f@5
|
42 POSSIBILITY OF SUCH DAMAGE.
|
f@5
|
43
|
f@5
|
44 */
|
f@5
|
45
|
f@0
|
46 #include "BufferToWaveRecorderNode.h"
|
f@0
|
47 #include "cinder/audio/Context.h"
|
f@0
|
48 #include "cinder/audio/Target.h"
|
f@0
|
49
|
f@0
|
50
|
f@0
|
51
|
f@0
|
52 // ----------------------------------------------------------------------------------------------------
|
f@0
|
53 // MARK: - BufferRecorderNode
|
f@0
|
54 // ----------------------------------------------------------------------------------------------------
|
f@0
|
55
|
f@0
|
56 namespace {
|
f@0
|
57
|
f@0
|
58 const size_t DEFAULT_RECORD_BUFFER_FRAMES = 44100;
|
f@0
|
59
|
f@0
|
60 void resizeBufferAndShuffleChannels(ci::audio::BufferDynamic *buffer, size_t resultNumFrames)
|
f@0
|
61 {
|
f@0
|
62 const size_t currentNumFrames = buffer->getNumFrames();
|
f@0
|
63 const size_t sampleSize = sizeof(ci::audio::BufferDynamic::SampleType);
|
f@0
|
64
|
f@0
|
65 if (currentNumFrames < resultNumFrames) {
|
f@0
|
66 // if expanding, resize and then shuffle. Make sure to get the data pointer after the resize.
|
f@0
|
67 buffer->setNumFrames(resultNumFrames);
|
f@0
|
68 float *data = buffer->getData();
|
f@0
|
69
|
f@0
|
70 for (size_t ch = 1; ch < buffer->getNumChannels(); ch++) {
|
f@0
|
71 const size_t numZeroFrames = resultNumFrames - currentNumFrames;
|
f@0
|
72 const float *currentChannel = &data[ch * currentNumFrames];
|
f@0
|
73 float *resultChannel = &data[ch * resultNumFrames];
|
f@0
|
74
|
f@0
|
75 memmove(resultChannel, currentChannel, currentNumFrames * sampleSize);
|
f@0
|
76 memset(resultChannel - numZeroFrames, 0, numZeroFrames * sampleSize);
|
f@0
|
77 }
|
f@0
|
78 }
|
f@0
|
79 else if (currentNumFrames > resultNumFrames) {
|
f@0
|
80 // if shrinking, shuffle first and then resize.
|
f@0
|
81 float *data = buffer->getData();
|
f@0
|
82
|
f@0
|
83 for (size_t ch = 1; ch < buffer->getNumChannels(); ch++) {
|
f@0
|
84 const float *currentChannel = &data[ch * currentNumFrames];
|
f@0
|
85 float *resultChannel = &data[ch * resultNumFrames];
|
f@0
|
86
|
f@0
|
87 memmove(resultChannel, currentChannel, currentNumFrames * sampleSize);
|
f@0
|
88 }
|
f@0
|
89
|
f@0
|
90 const size_t numZeroFrames = (currentNumFrames - resultNumFrames) * buffer->getNumChannels();
|
f@0
|
91 memset(data + buffer->getSize() - numZeroFrames, 0, numZeroFrames * sampleSize);
|
f@0
|
92
|
f@0
|
93 buffer->setNumFrames(resultNumFrames);
|
f@0
|
94 }
|
f@0
|
95 }
|
f@0
|
96
|
f@0
|
97 }
|
f@0
|
98
|
f@0
|
99
|
f@0
|
100 BufferToWaveRecorderNode::BufferToWaveRecorderNode( std::size_t numChunks, double numSeconds )
|
f@0
|
101 : SampleRecorderNode( Format().channels( 1 ) ),
|
f@0
|
102 mLastOverrun( 0 ),
|
f@0
|
103 mNumChunks( numChunks ),
|
f@0
|
104 mNumSeconds( numSeconds ),
|
f@0
|
105 mRingBuffer( numChunks ),
|
f@0
|
106 mChunkMaxAudioVal( kMinAudioVal ),
|
f@0
|
107 mChunkMinAudioVal( kMaxAudioVal ),
|
f@0
|
108 mChunkSampleCounter( 0 ),
|
f@0
|
109 mChunkIndex( 0 )
|
f@0
|
110 {
|
f@0
|
111
|
f@0
|
112 }
|
f@0
|
113
|
f@0
|
114 void BufferToWaveRecorderNode::initialize()
|
f@0
|
115 {
|
f@0
|
116 // adjust recorder buffer to match channels once initialized, since they could have changed since construction.
|
f@0
|
117 bool resize = mRecorderBuffer.getNumFrames() != 0;
|
f@0
|
118 mRecorderBuffer.setNumChannels( getNumChannels() );
|
f@0
|
119
|
f@0
|
120 // lenght of buffer is = number of seconds * sample rate
|
f@0
|
121 initBuffers( size_t( mNumSeconds * (double)getSampleRate() ) );
|
f@0
|
122
|
f@0
|
123 // How many samples each chunk contains. That is it calculates the min and max of
|
f@0
|
124 // This is calculated here and not in the initializer list because it uses getNumFrames()
|
f@0
|
125 // FIXME probably could be done in constructor body
|
f@0
|
126 mNumSamplesPerChunk = std::lround( float( getNumFrames() ) / mNumChunks );
|
f@0
|
127
|
f@0
|
128 // if the buffer had already been resized, zero out any possibly existing data.
|
f@0
|
129 if( resize )
|
f@0
|
130 mRecorderBuffer.zero();
|
f@0
|
131
|
f@0
|
132 mEnvRampLen = kRampTime * getSampleRate();
|
f@0
|
133 mEnvDecayStart = mRecorderBuffer.getNumFrames() - mEnvRampLen;
|
f@0
|
134 if ( mEnvRampLen <= 0 ){
|
f@0
|
135 mEnvRampRate = 0;
|
f@0
|
136 }
|
f@0
|
137 else{
|
f@0
|
138 mEnvRampRate = 1.0f / mEnvRampLen;
|
f@0
|
139 }
|
f@0
|
140 }
|
f@0
|
141
|
f@0
|
142 void BufferToWaveRecorderNode::initBuffers(size_t numFrames)
|
f@0
|
143 {
|
f@0
|
144 mRecorderBuffer.setSize( numFrames, getNumChannels() );
|
f@0
|
145 mCopiedBuffer = std::make_shared<ci::audio::BufferDynamic>( numFrames, getNumChannels() );
|
f@0
|
146 }
|
f@0
|
147
|
f@0
|
148 void BufferToWaveRecorderNode::start()
|
f@0
|
149 {
|
f@0
|
150 mWritePos = 0;
|
f@0
|
151 mChunkIndex = 0;
|
f@0
|
152 enable();
|
f@0
|
153 }
|
f@0
|
154
|
f@0
|
155 void BufferToWaveRecorderNode::stop()
|
f@0
|
156 {
|
f@0
|
157 disable();
|
f@0
|
158 }
|
f@0
|
159
|
f@0
|
160 void BufferToWaveRecorderNode::setNumSeconds(double numSeconds, bool shrinkToFit)
|
f@0
|
161 {
|
f@0
|
162 setNumFrames(size_t(numSeconds * (double)getSampleRate()), shrinkToFit);
|
f@0
|
163 }
|
f@0
|
164
|
f@0
|
165 double BufferToWaveRecorderNode::getNumSeconds() const
|
f@0
|
166 {
|
f@0
|
167 return (double)getNumFrames() / (double)getSampleRate();
|
f@0
|
168 }
|
f@0
|
169
|
f@0
|
170 void BufferToWaveRecorderNode::setNumFrames(size_t numFrames, bool shrinkToFit)
|
f@0
|
171 {
|
f@0
|
172 if (mRecorderBuffer.getNumFrames() == numFrames)
|
f@0
|
173 return;
|
f@0
|
174
|
f@0
|
175 std::lock_guard<std::mutex> lock(getContext()->getMutex());
|
f@0
|
176
|
f@0
|
177 if (mWritePos != 0)
|
f@0
|
178 resizeBufferAndShuffleChannels(&mRecorderBuffer, numFrames);
|
f@0
|
179 else
|
f@0
|
180 mRecorderBuffer.setNumFrames(numFrames);
|
f@0
|
181
|
f@0
|
182 if (shrinkToFit)
|
f@0
|
183 mRecorderBuffer.shrinkToFit();
|
f@0
|
184 }
|
f@0
|
185
|
f@0
|
186 ci::audio::BufferRef BufferToWaveRecorderNode::getRecordedCopy() const
|
f@0
|
187 {
|
f@0
|
188 // first grab the number of current frames, which may be increasing as the recording continues.
|
f@0
|
189 size_t numFrames = mWritePos;
|
f@0
|
190 mCopiedBuffer->setSize(numFrames, mRecorderBuffer.getNumChannels());
|
f@0
|
191
|
f@0
|
192 mCopiedBuffer->copy(mRecorderBuffer, numFrames);
|
f@0
|
193 return mCopiedBuffer;
|
f@0
|
194 }
|
f@0
|
195
|
f@0
|
196 void BufferToWaveRecorderNode::writeToFile(const ci::fs::path &filePath, ci::audio::SampleType sampleType)
|
f@0
|
197 {
|
f@0
|
198 size_t currentWritePos = mWritePos;
|
f@0
|
199 ci::audio::BufferRef copiedBuffer = getRecordedCopy();
|
f@0
|
200
|
f@0
|
201 ci::audio::TargetFileRef target = ci::audio::TargetFile::create(filePath, getSampleRate(), getNumChannels(), sampleType);
|
f@0
|
202 target->write(copiedBuffer.get(), currentWritePos);
|
f@0
|
203 }
|
f@0
|
204
|
f@0
|
205 uint64_t BufferToWaveRecorderNode::getLastOverrun()
|
f@0
|
206 {
|
f@0
|
207 uint64_t result = mLastOverrun;
|
f@0
|
208 mLastOverrun = 0;
|
f@0
|
209 return result;
|
f@0
|
210 }
|
f@0
|
211
|
f@0
|
212
|
f@0
|
213 void BufferToWaveRecorderNode::process(ci::audio::Buffer *buffer)
|
f@0
|
214 {
|
f@0
|
215 size_t writePos = mWritePos;
|
f@0
|
216 size_t numWriteFrames = buffer->getNumFrames();
|
f@0
|
217
|
f@0
|
218 if ( writePos == 0 ){
|
f@0
|
219 RecordWaveMsg msg = makeRecordWaveMsg( Command::WAVE_START, 0, 0, 0 );
|
f@0
|
220 mRingBuffer.write( &msg, 1 );
|
f@0
|
221
|
f@0
|
222 // reset everything
|
f@0
|
223 mChunkMinAudioVal = kMaxAudioVal;
|
f@0
|
224 mChunkMaxAudioVal = kMinAudioVal;
|
f@0
|
225 mChunkSampleCounter = 0;
|
f@0
|
226 mChunkIndex = 0;
|
f@0
|
227 mEnvRamp = 0.0f;
|
f@0
|
228 }
|
f@0
|
229
|
f@0
|
230 // if buffer has too many frames (because we're nearly at the end or at the end )
|
f@0
|
231 // of mRecoderBuffer then numWriteFrames becomes the number of samples left to
|
f@0
|
232 // fill mRecorderBuffer. Which is 0 if the buffer is at the end.
|
f@0
|
233 if ( writePos + numWriteFrames > mRecorderBuffer.getNumFrames() )
|
f@0
|
234 numWriteFrames = mRecorderBuffer.getNumFrames() - writePos;
|
f@0
|
235
|
f@0
|
236 if ( numWriteFrames <= 0 )
|
f@0
|
237 return;
|
f@0
|
238
|
f@0
|
239
|
f@0
|
240 // apply envelope to the buffer at the edges to avoid clicks
|
f@0
|
241 if ( writePos < mEnvRampLen ){ // beginning of wave
|
f@0
|
242 for ( size_t i = 0; i < std::min( mEnvRampLen, numWriteFrames ); i++ ){
|
f@0
|
243 buffer->getData()[i] *= mEnvRamp;
|
f@0
|
244 mEnvRamp += mEnvRampRate;
|
f@0
|
245 if ( mEnvRamp > 1.0f )
|
f@0
|
246 mEnvRamp = 1.0f;
|
f@0
|
247 }
|
f@0
|
248 }
|
f@0
|
249 else if ( writePos + numWriteFrames > mEnvDecayStart ){ // end of wave
|
f@0
|
250 for ( size_t i = std::max( writePos, mEnvDecayStart ) - writePos; i < numWriteFrames; i++ ){
|
f@0
|
251 buffer->getData()[i] *= mEnvRamp;
|
f@0
|
252 mEnvRamp -= mEnvRampRate;
|
f@0
|
253 if ( mEnvRamp < 0.0f )
|
f@0
|
254 mEnvRamp = 0.0f;
|
f@0
|
255 }
|
f@0
|
256 }
|
f@0
|
257
|
f@0
|
258
|
f@0
|
259 mRecorderBuffer.copyOffset(*buffer, numWriteFrames, writePos, 0);
|
f@0
|
260
|
f@0
|
261 if ( numWriteFrames < buffer->getNumFrames() )
|
f@0
|
262 mLastOverrun = getContext()->getNumProcessedFrames();
|
f@0
|
263
|
f@0
|
264 /* find max and minimum of this buffer */
|
f@0
|
265 for ( size_t i = 0; i < numWriteFrames; i++ ){
|
f@0
|
266
|
f@0
|
267 if ( buffer->getData()[i] < mChunkMinAudioVal ){
|
f@0
|
268 mChunkMinAudioVal = buffer->getData()[i];
|
f@0
|
269 }
|
f@0
|
270
|
f@0
|
271 if ( buffer->getData()[i] > mChunkMaxAudioVal ){
|
f@0
|
272 mChunkMaxAudioVal = buffer->getData()[i];
|
f@0
|
273 }
|
f@0
|
274
|
f@0
|
275 if ( mChunkSampleCounter >= mNumSamplesPerChunk // if collected enough samples
|
f@0
|
276 || writePos + i >= mRecorderBuffer.getNumFrames() - 1 ){ // or at the end of recorder buffer
|
f@0
|
277 // send chunk to GUI
|
f@0
|
278 size_t chunkIndex = mChunkIndex.fetch_add( 1 );
|
f@0
|
279
|
f@0
|
280 RecordWaveMsg msg = makeRecordWaveMsg( Command::WAVE_CHUNK, chunkIndex, mChunkMinAudioVal, mChunkMaxAudioVal );
|
f@0
|
281 mRingBuffer.write( &msg, 1 );
|
f@0
|
282
|
f@0
|
283 // reset chunk info
|
f@0
|
284 mChunkMinAudioVal = kMaxAudioVal;
|
f@0
|
285 mChunkMaxAudioVal = kMinAudioVal;
|
f@0
|
286 mChunkSampleCounter = 0;
|
f@0
|
287 }
|
f@0
|
288 else{
|
f@0
|
289 mChunkSampleCounter++;
|
f@0
|
290 }
|
f@0
|
291 }
|
f@0
|
292
|
f@0
|
293 // check if write position has been reset by the GUI thread, if not write new value
|
f@0
|
294 const size_t writePosNew = writePos + numWriteFrames;
|
f@0
|
295 mWritePos.compare_exchange_strong( writePos, writePosNew );
|
f@0
|
296
|
f@0
|
297 }
|
f@0
|
298
|
f@0
|
299
|
f@0
|
300 const float BufferToWaveRecorderNode::kMinAudioVal = -1.0f;
|
f@0
|
301 const float BufferToWaveRecorderNode::kMaxAudioVal = 1.0f;
|
f@0
|
302 const float BufferToWaveRecorderNode::kRampTime = 0.02;
|
f@0
|
303
|
f@0
|
304
|