TEST( BaseAudioEvent, MixBufferLoopeableEvent ) { BaseAudioEvent* audioEvent = new BaseAudioEvent(); int sourceSize = 16; AudioBuffer* sourceBuffer = new AudioBuffer( 1, sourceSize ); SAMPLE_TYPE* rawBuffer = sourceBuffer->getBufferForChannel( 0 ); fillAudioBuffer( sourceBuffer ); audioEvent->setBuffer( sourceBuffer, false ); audioEvent->setLoopeable( true ); audioEvent->setSampleLength( 16 * 4 ); // thus will loop 4 times audioEvent->positionEvent ( 0, 16, 0 ); // create an output buffer at a size smaller than the source buffer length int outputSize = ( int )(( double ) sourceSize * .4 ); AudioBuffer* targetBuffer = new AudioBuffer( sourceBuffer->amountOfChannels, outputSize ); int minBufferPos = audioEvent->getSampleStart(); int bufferPos = minBufferPos; int maxBufferPos = audioEvent->getSampleEnd(); // test the seamless mixing over multiple iterations for ( ; bufferPos < maxBufferPos; bufferPos += outputSize ) { // mix buffer contents targetBuffer->silenceBuffers(); bool loopStarted = bufferPos + ( outputSize - 1 ) > maxBufferPos; int loopOffset = ( maxBufferPos - bufferPos ) + 1; audioEvent->mixBuffer( targetBuffer, bufferPos, minBufferPos, maxBufferPos, loopStarted, loopOffset, false ); // assert results SAMPLE_TYPE* mixedBuffer = targetBuffer->getBufferForChannel( 0 ); for ( int i = 0; i < outputSize; ++i ) { int compareOffset = ( bufferPos + i ) % sourceSize; EXPECT_EQ( rawBuffer[ compareOffset ], mixedBuffer[ i ] ) << "expected mixed buffer contents to equal the source contents at mixed offset " << i << " for source offset " << compareOffset; } } delete targetBuffer; delete sourceBuffer; delete audioEvent; }
void JavaUtilities::createSampleFromBuffer( jstring aKey, jint aBufferLength, jint aChannelAmount, jdoubleArray aBuffer, jdoubleArray aOptRightBuffer ) { AudioBuffer* sampleBuffer = new AudioBuffer( aChannelAmount, aBufferLength ); int i = 0; // get a pointer to the Java array jdouble* c_array; c_array = JavaBridge::getEnvironment()->GetDoubleArrayElements( aBuffer, 0 ); // exception checking if ( c_array == NULL ) return; // copy buffer contents SAMPLE_TYPE* channelBuffer = sampleBuffer->getBufferForChannel( 0 ); for ( i = 0; i < aBufferLength; i++ ) channelBuffer[ i ] = ( SAMPLE_TYPE ) c_array[ i ]; // release the memory so Java can have it again JavaBridge::getEnvironment()->ReleaseDoubleArrayElements( aBuffer, c_array, 0 ); // stereo ? if ( aChannelAmount == 2 ) { c_array = JavaBridge::getEnvironment()->GetDoubleArrayElements( aOptRightBuffer, 0 ); // exception checking if ( c_array == NULL ) return; // copy buffer contents channelBuffer = sampleBuffer->getBufferForChannel( 1 ); for ( i = 0; i < aBufferLength; i++ ) channelBuffer[ i ] = ( SAMPLE_TYPE ) c_array[ i ]; // release the memory so Java can have it again JavaBridge::getEnvironment()->ReleaseDoubleArrayElements( aOptRightBuffer, c_array, 0 ); } // convert jstring to std::string const char* s = JavaBridge::getEnvironment()->GetStringUTFChars( aKey, NULL ); std::string theKey = s; JavaBridge::getEnvironment()->ReleaseStringUTFChars( aKey, s ); SampleManager::setSample( theKey, sampleBuffer ); }
AudioBuffer* AudioBuffer::clone() { AudioBuffer* output = new AudioBuffer( amountOfChannels, bufferSize ); for ( int i = 0; i < amountOfChannels; ++i ) { SAMPLE_TYPE* sourceBuffer = getBufferForChannel( i ); SAMPLE_TYPE* targetBuffer = output->getBufferForChannel( i ); memcpy( targetBuffer, sourceBuffer, bufferSize * sizeof( SAMPLE_TYPE )); } return output; }
/** * write the contents of the write buffer into * an output file, this will only write content * up until the point if was written to in case * the buffer wasn't full yet */ void writeBufferToFile( int aSampleRate, int aNumChannels, bool broadcastUpdate ) { // quick assertion if ( cachedBuffer == 0 ) return; // copy string contents for appending of filename std::string outputFile = std::string( outputDirectory.c_str()); int bufferSize = outputBufferSize; // recorded less than maximum available in buffer ? cut silence // by writing recording into temporary buffers if ( outputWriterIndex < bufferSize ) { bufferSize = outputWriterIndex; AudioBuffer* tempBuffer = new AudioBuffer( aNumChannels, bufferSize ); for ( int i = 0; i < bufferSize; ++i ) { for ( int c = 0; c < aNumChannels; ++c ) tempBuffer->getBufferForChannel( c )[ i ] = cachedBuffer->getBufferForChannel( c )[ i ]; } WaveWriter::bufferToFile( outputFile.append( SSTR( AudioEngine::recordingFileId )), tempBuffer, aSampleRate ); // free memory allocated by temporary buffer delete tempBuffer; } else { WaveWriter::bufferToFile( outputFile.append( SSTR( AudioEngine::recordingFileId )), cachedBuffer, aSampleRate ); } flushOutput(); // free memory // broadcast update, pass buffer identifier to identify last recording if ( broadcastUpdate ) Notifier::broadcast( Notifications::RECORDING_STATE_UPDATED, AudioEngine::recordingFileId ); }
TEST( AudioEngine, Output ) { AudioEngine::test_program = 2; // help mocked OpenSL IO identify which test is running AudioEngine::test_successful = false; // prepare engine environment SequencerController* controller = new SequencerController(); controller->prepare( 16, 48000, 130.0f, 4, 4 ); // 130 BPM in 4/4 time at 48 kHz sample rate w/buffer size of 16 samples controller->setTempoNow( 130.0f, 4, 4 ); controller->rewind(); AudioEngine::volume = 1; // QQQ : later on we test mix volume ;) // create an AudioEvent that holds a simple waveform // the resulting 16 sample mono buffer contains the following samples: // // -1,-1,-1,-1,0,0,0,0,1,1,1,1,0,0,0,0 // // the event will last for an entire measure in duration BaseInstrument* instrument = new BaseInstrument(); BaseAudioEvent* event = new BaseAudioEvent( instrument ); AudioBuffer* buffer = new AudioBuffer( 1, 16 ); SAMPLE_TYPE* rawBuffer = buffer->getBufferForChannel( 0 ); for ( int i = 0; i < 4; ++i ) rawBuffer[ i ] = ( SAMPLE_TYPE ) -MAX_PHASE; for ( int i = 4; i < 8; ++i ) rawBuffer[ i ] = ( SAMPLE_TYPE ) 0; for ( int i = 8; i < 12; ++i ) rawBuffer[ i ] = ( SAMPLE_TYPE ) MAX_PHASE; for ( int i = 12; i < 16; ++i ) rawBuffer[ i ] = ( SAMPLE_TYPE ) 0; event->setBuffer( buffer, false ); event->setLoopeable( true ); event->setSampleLength( AudioEngine::samples_per_bar ); event->positionEvent( 0, 16, 0 ); event->addToSequencer(); // start the engine controller->setPlaying( true ); AudioEngine::start(); // evaluate results (assertions are made in mock_opensl_io.cpp) ASSERT_TRUE( AudioEngine::test_successful ) << "expected test to be successful"; EXPECT_EQ( 3, AudioEngine::test_program ) << "expected test program to have incremented"; // clean up controller->setPlaying( false ); AudioEngine::render_iterations = 0; delete controller; delete instrument; delete event; delete buffer; }
/** * starts the render thread * NOTE: the render thread is always active, even when the * sequencer is paused */ void start() { OPENSL_STREAM *p; p = android_OpenAudioDevice( AudioEngineProps::SAMPLE_RATE, AudioEngineProps::INPUT_CHANNELS, AudioEngineProps::OUTPUT_CHANNELS, AudioEngineProps::BUFFER_SIZE ); // hardware unavailable ? halt thread, trigger JNI callback for error handler if ( p == NULL ) { Observer::handleHardwareUnavailable(); return; } // audio hardware available, start render thread int buffer_size, i, c, ci; buffer_size = AudioEngineProps::BUFFER_SIZE; int outputChannels = AudioEngineProps::OUTPUT_CHANNELS; bool isMono = outputChannels == 1; std::vector<AudioChannel*> channels; std::vector<AudioChannel*> channels2; // used when loop starts for gathering events at the start range bool loopStarted = false; // whether the current buffer will exceed the end offset of the loop (read remaining samples from the start) int loopOffset = 0; // the offset within the current buffer where we start reading from the current loops start offset int loopAmount = 0; // amount of samples we must read from the current loops start offset float recbufferIn [ buffer_size ]; // used for recording from device input float outbuffer [ buffer_size * outputChannels ]; // the output buffer rendered by the hardware // generate buffers for temporary channel buffer writes AudioBuffer* channelBuffer = new AudioBuffer( outputChannels, buffer_size ); AudioBuffer* inbuffer = new AudioBuffer( outputChannels, buffer_size ); // accumulates all channels ("master strip") AudioBuffer* recbuffer = new AudioBuffer( AudioEngineProps::INPUT_CHANNELS, buffer_size ); thread = 1; // signal processors Finalizer* limiter = new Finalizer ( 2, 500, AudioEngineProps::SAMPLE_RATE, outputChannels ); LPFHPFilter* hpf = new LPFHPFilter(( float ) AudioEngineProps::SAMPLE_RATE, 55, outputChannels ); while ( thread ) { // erase previous buffer contents inbuffer->silenceBuffers(); // gather the audio events by the buffer range currently being processed int endPosition = bufferPosition + buffer_size; channels = sequencer::getAudioEvents( channels, bufferPosition, endPosition, true ); // read pointer exceeds maximum allowed offset ? => sequencer has started its loop // we must now also gather extra events at the start position of the seq. range loopStarted = endPosition > max_buffer_position; loopOffset = (( max_buffer_position + 1 ) - bufferPosition ); loopAmount = buffer_size - loopOffset; if ( loopStarted ) { // were we bouncing the audio ? save file and stop rendering if ( bouncing ) { DiskWriter::writeBufferToFile( AudioEngineProps::SAMPLE_RATE, AudioEngineProps::OUTPUT_CHANNELS, false ); // broadcast update via JNI, pass buffer identifier name to identify last recording Observer::handleBounceComplete( 1 ); thread = 0; // stop thread, halts rendering break; } else { endPosition -= max_buffer_position; channels2 = sequencer::getAudioEvents( channels2, min_buffer_position, min_buffer_position + buffer_size, false ); // er? the channels are magically merged by above invocation..., performing the insert below adds the same events TWICE*POP*!?!? //channels.insert( channels.end(), channels2.begin(), channels2.end() ); // merge the channels into one channels2.clear(); // would clear on next "getAudioEvents"-query... but why wait ? } } // record audio from Android device ? if ( recordFromDevice && AudioEngineProps::INPUT_CHANNELS > 0 ) { int recSamps = android_AudioIn( p, recbufferIn, AudioEngineProps::BUFFER_SIZE ); SAMPLE_TYPE* recBufferChannel = recbuffer->getBufferForChannel( 0 ); for ( int j = 0; j < recSamps; ++j ) { recBufferChannel[ j ] = recbufferIn[ j ];//static_cast<float>( recbufferIn[ j ] ); // merge recording into current input buffer for instant monitoring if ( monitorRecording ) { for ( int k = 0; k < outputChannels; ++k ) inbuffer->getBufferForChannel( k )[ j ] = recBufferChannel[ j ]; } } } // channel loop int j = 0; int channelAmount = channels.size(); for ( j; j < channelAmount; ++j ) { AudioChannel* channel = channels[ j ]; bool isCached = channel->hasCache; // whether this channel has a fully cached buffer bool mustCache = AudioEngineProps::CHANNEL_CACHING && channel->canCache() && !isCached; // whether to cache this channels output bool gotBuffer = false; int cacheReadPos = 0; // the offset we start ready from the channel buffer (when writing to cache) SAMPLE_TYPE channelVolume = ( SAMPLE_TYPE ) channel->mixVolume; std::vector<BaseAudioEvent*> audioEvents = channel->audioEvents; int amount = audioEvents.size(); // clear previous channel buffer content channelBuffer->silenceBuffers(); bool useChannelRange = channel->maxBufferPosition != 0; // channel has its own buffer range (i.e. drummachine) int maxBufferPosition = useChannelRange ? channel->maxBufferPosition : max_buffer_position; // we make a copy of the current buffer position indicator int bufferPos = bufferPosition; // ...in case the AudioChannels maxBufferPosition differs from the sequencer loop range // note that these buffer positions are always a full bar in length (as we loop measures) while ( bufferPos > maxBufferPosition ) bufferPos -= bytes_per_bar; // only render sequenced events when the sequencer isn't in the paused state // and the channel volume is actually at an audible level! ( > 0 ) if ( playing && amount > 0 && channelVolume > 0.0 ) { if ( !isCached ) { // write the audioEvent buffers into the main output buffer for ( int k = 0; k < amount; ++k ) { BaseAudioEvent* audioEvent = audioEvents[ k ]; if ( !audioEvent->isLocked()) // make sure we are allowed to query the contents { audioEvent->lock(); // prevent buffer mutations during this read cycle audioEvent->mixBuffer( channelBuffer, bufferPos, min_buffer_position, maxBufferPosition, loopStarted, loopOffset, useChannelRange ); audioEvent->unlock(); // release lock } } } else { channel->readCachedBuffer( channelBuffer, bufferPos ); } } // perform live rendering for this instrument if ( channel->hasLiveEvents ) { int lAmount = channel->liveEvents.size(); // the volume of the live events is divided by the channel mix as a live event // is played on the same instrument, but just as a different voice (note the // events can have their own mix level) float lAmp = channel->mixVolume > 0.0 ? MAX_PHASE / channel->mixVolume : MAX_PHASE; for ( int k = 0; k < lAmount; ++k ) { BaseAudioEvent* vo = channel->liveEvents[ k ]; channelBuffer->mergeBuffers( vo->synthesize( buffer_size ), 0, 0, lAmp ); } } // apply the processing chains processors / modulators ProcessingChain* chain = channel->processingChain; std::vector<BaseProcessor*> processors = chain->getActiveProcessors(); for ( int k = 0; k < processors.size(); k++ ) { BaseProcessor* processor = processors[ k ]; bool canCacheProcessor = processor->isCacheable(); // only apply processor when we're not caching or cannot cache its output if ( !isCached || !canCacheProcessor ) { // cannot cache this processor and we're caching ? write all contents // of the channelBuffer into the channels cache if ( mustCache && !canCacheProcessor ) mustCache = !writeChannelCache( channel, channelBuffer, cacheReadPos ); processors[ k ]->process( channelBuffer, channel->isMono ); } } // write cache if it didn't happen yet ;) (bus processors are (currently) non-cacheable) if ( mustCache ) mustCache = !writeChannelCache( channel, channelBuffer, cacheReadPos ); // write the channel buffer into the combined output buffer, apply channel volume // note live events are always audible as their volume is relative to the instrument if ( channel->hasLiveEvents && channelVolume == 0.0 ) channelVolume = MAX_PHASE; inbuffer->mergeBuffers( channelBuffer, 0, 0, channelVolume ); } // TODO: create bus processors for these ? // apply high pass filtering to prevent extreme low rumbling and nasty filter offsets hpf->process( inbuffer, buffer_size ); // limit the audio to prevent clipping limiter->process( inbuffer, isMono ); // write the accumulated buffers into the output buffer for ( i = 0, c = 0; i < buffer_size; i++, c += outputChannels ) { for ( ci = 0; ci < outputChannels; ci++ ) { float sample = ( float ) inbuffer->getBufferForChannel( ci )[ i ] * volume; // apply master volume // extreme limiting (still above the thresholds?) if ( sample < -MAX_PHASE ) sample = -MAX_PHASE; else if ( sample > +MAX_PHASE ) sample = +MAX_PHASE; outbuffer[ c + ci ] = sample; } // update the buffer pointers and sequencer position if ( playing ) { if ( ++bufferPosition % bytes_per_tick == 0 ) handleSequencerPositionUpdate( android_GetTimestamp( p )); if ( bufferPosition > max_buffer_position ) bufferPosition = min_buffer_position; } } // render the buffer in the audio hardware (unless we're bouncing as writing the output // makes it both unnecessarily audible and stalls this thread's execution if ( !bouncing ) android_AudioOut( p, outbuffer, buffer_size * AudioEngineProps::OUTPUT_CHANNELS ); // record the output if recording state is active if ( playing && ( recordOutput || recordFromDevice )) { if ( recordFromDevice ) // recording from device input ? > write the record buffer DiskWriter::appendBuffer( recbuffer ); else // recording global output ? > write the combined buffer DiskWriter::appendBuffer( inbuffer ); // exceeded maximum recording buffer amount ? > write current recording if ( DiskWriter::bufferFull() || haltRecording ) { int amountOfChannels = recordFromDevice ? AudioEngineProps::INPUT_CHANNELS : outputChannels; DiskWriter::writeBufferToFile( AudioEngineProps::SAMPLE_RATE, amountOfChannels, true ); if ( !haltRecording ) { DiskWriter::generateOutputBuffer(); // allocate new buffer for next iteration ++recordingFileId; } else { haltRecording = false; } } } // tempo update queued ? if ( queuedTempo != tempo ) handleTempoUpdate( queuedTempo, true ); } android_CloseAudioDevice( p ); // clear heap memory allocated before thread loop delete inbuffer; delete channelBuffer; delete limiter; delete hpf; }
TEST( BaseAudioEvent, MixBuffer ) { BaseAudioEvent* audioEvent = new BaseAudioEvent(); int sampleLength = randomInt( 8, 24 ); int sampleStart = randomInt( 0, ( int )( sampleLength / 2 )); audioEvent->setSampleStart ( sampleStart ); audioEvent->setSampleLength( sampleLength ); int sampleEnd = audioEvent->getSampleEnd(); AudioBuffer* buffer = fillAudioBuffer( new AudioBuffer( randomInt( 1, 4 ), sampleLength )); audioEvent->setBuffer( buffer, true ); float volume = randomFloat(); audioEvent->setVolume( volume ); //std::cout << " ss: " << sampleStart << " se: " << sampleEnd << " sl: " << sampleLength << " ch: " << buffer->amountOfChannels; // create a temporary buffer to write output in, ensure it is smaller than the event buffer AudioBuffer* targetBuffer = new AudioBuffer( buffer->amountOfChannels, randomInt( 2, 4 )); int buffersToWrite = targetBuffer->bufferSize; ASSERT_FALSE( bufferHasContent( targetBuffer )) << "expected target buffer to be silent after creation, but it has content"; // test 1. mix without loopable range int maxBufferPos = sampleLength * 2; // use a "loop range" larger than the size of the events length int minBufferPos = randomInt( 0, maxBufferPos / 2 ); int bufferPos = randomInt( minBufferPos, maxBufferPos - 1 ); bool loopStarted = false; int loopOffset = 0; // if the random bufferPosition wasn't within the events sampleStart and sampleEnd range, we expect no content bool expectContent = ( bufferPos >= sampleStart && bufferPos <= sampleEnd ) || (( bufferPos + buffersToWrite ) >= sampleStart && ( bufferPos + buffersToWrite ) <= sampleEnd ); //std::cout << " expected content: " << expectContent << " for buffer size: " << buffersToWrite; //std::cout << " min: " << minBufferPos << " max: " << maxBufferPos << " cur: " << bufferPos; audioEvent->mixBuffer( targetBuffer, bufferPos, minBufferPos, maxBufferPos, loopStarted, loopOffset, false ); // validate buffer contents after mixing if ( expectContent ) { for ( int c = 0, ca = targetBuffer->amountOfChannels; c < ca; ++c ) { SAMPLE_TYPE* buffer = targetBuffer->getBufferForChannel( c ); SAMPLE_TYPE* sourceBuffer = audioEvent->getBuffer()->getBufferForChannel( c ); SAMPLE_TYPE expectedSample; for ( int i = 0; i < buffersToWrite; ++i ) { int r = i + bufferPos; // read pointer for the source buffer if ( r >= maxBufferPos && !loopStarted ) r -= ( maxBufferPos - minBufferPos ); if ( r >= sampleStart && r <= sampleEnd ) { r -= sampleStart; // substract audioEvent start position expectedSample = sourceBuffer[ r ] * volume; } else { expectedSample = 0.0; } SAMPLE_TYPE sample = buffer[ i ]; EXPECT_EQ( expectedSample, sample ) << "expected mixed sample at " << i << " to be equal to the calculated expected sample at read offset " << r; } } } else { ASSERT_FALSE( bufferHasContent( targetBuffer )) << "expected target buffer to contain no content after mixing for an out-of-range buffer position"; } // test 2. mixing within a loopable range (implying sequencer is starting a loop) targetBuffer->silenceBuffers(); ASSERT_FALSE( bufferHasContent( targetBuffer )) << "expected target buffer to be silent after silencing, but it still has content"; bufferPos = randomInt( minBufferPos, maxBufferPos - 1 ); loopStarted = true; loopOffset = ( maxBufferPos - bufferPos ) + 1; // pre calculate at which buffer iterator the looping will commence // loopStartIteratorPosition describes at which sequencer position the loop starts // loopStartWritePointer describes at which position in the targetBuffer the loop is written to // amountOfLoopedWrites is the amount of samples written in the loop // loopStartReadPointer describes at which position the samples from the source audioEvent will be read when loop starts // loopStartReadPointerEnd describes the last position the samples from the source audioEvent will be read for the amount of loop writes int loopStartIteratorPosition = maxBufferPos + 1; int loopStartWritePointer = loopOffset; int loopStartReadPointer = minBufferPos; int amountOfLoopedWrites = ( bufferPos + buffersToWrite ) - loopStartIteratorPosition; int loopStartReadPointerEnd = ( loopStartReadPointer + amountOfLoopedWrites ) - 1; expectContent = ( bufferPos >= sampleStart && bufferPos <= sampleEnd ) || (( bufferPos + buffersToWrite ) >= sampleStart && ( bufferPos + buffersToWrite ) <= sampleEnd ) || ( loopStartIteratorPosition > maxBufferPos && ( ( loopStartReadPointer >= sampleStart && loopStartReadPointer <= sampleEnd ) || ( loopStartReadPointerEnd >= sampleStart && loopStartReadPointerEnd <= sampleEnd ))); audioEvent->mixBuffer( targetBuffer, bufferPos, minBufferPos, maxBufferPos, loopStarted, loopOffset, false ); //std::cout << " expected content: " << expectContent << " for buffer size: " << buffersToWrite; //std::cout << " min: " << minBufferPos << " max: " << maxBufferPos << " cur: " << bufferPos << " loop offset: " << loopOffset; if ( expectContent ) { for ( int c = 0, ca = targetBuffer->amountOfChannels; c < ca; ++c ) { SAMPLE_TYPE* buffer = targetBuffer->getBufferForChannel( c ); SAMPLE_TYPE* sourceBuffer = audioEvent->getBuffer()->getBufferForChannel( c ); for ( int i = 0; i < buffersToWrite; ++i ) { SAMPLE_TYPE expectedSample = 0.0; int r = i + bufferPos; // read pointer for the source buffer if ( i >= loopOffset ) r = minBufferPos + ( i - loopOffset ); if ( r >= sampleStart && r <= sampleEnd ) { r -= sampleStart; // substract audioEvent start position expectedSample = sourceBuffer[ r ] * volume; } SAMPLE_TYPE sample = buffer[ i ]; EXPECT_EQ( expectedSample, sample ) << "expected mixed sample at " << i << " to be equal to the calculated expected sample at read " << "offset " << r << " ( sanitized from " << ( i + bufferPos ) << " )"; } } } else { ASSERT_FALSE( bufferHasContent( targetBuffer )) << "expected output buffer to contain no content after mixing for an out-of-range buffer position"; } delete audioEvent; delete targetBuffer; delete buffer; }