diff --git a/include/cinder/audio/FileOggVorbis.h b/include/cinder/audio/FileOggVorbis.h index 0978e3db63..d402e13d1f 100644 --- a/include/cinder/audio/FileOggVorbis.h +++ b/include/cinder/audio/FileOggVorbis.h @@ -73,7 +73,7 @@ class SourceFileOggVorbis : public SourceFile { //! TargetFile implementation for encoding ogg vorbis files. class TargetFileOggVorbis : public TargetFile { public: - TargetFileOggVorbis( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType ); + TargetFileOggVorbis( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative ); virtual ~TargetFileOggVorbis(); void performWrite( const Buffer *buffer, size_t numFrames, size_t frameOffset ) override; diff --git a/include/cinder/audio/SampleRecorderNode.h b/include/cinder/audio/SampleRecorderNode.h index 02deb87f04..b6dd44fd11 100644 --- a/include/cinder/audio/SampleRecorderNode.h +++ b/include/cinder/audio/SampleRecorderNode.h @@ -75,9 +75,9 @@ class CI_API BufferRecorderNode : public SampleRecorderNode { //! \brief Writes the currently recorded samples to a file at \a filePath //! - //! The encoding format is derived from \a filePath's extension and \a sampleType (default = SampleType::INT_16). + //! The encoding format is derived from \a filePath's extension, \a sampleType (default = SampleType::INT_16) and \a destSampleRate (default = getSampleRate()). //! \note throws AudioFileExc if the write request cannot be completed. - void writeToFile( const ci::fs::path &filePath, SampleType sampleType = SampleType::INT_16 ); + void writeToFile( const ci::fs::path &filePath, SampleType sampleType = SampleType::INT_16, size_t destSampleRate = 0 ); //! Returns the frame of the last buffer overrun or 0 if none since the last time this method was called. When this happens, it means the recorded buffer probably has skipped some frames. uint64_t getLastOverrun(); diff --git a/include/cinder/audio/Target.h b/include/cinder/audio/Target.h index 990edbdf0d..d6af1900f4 100644 --- a/include/cinder/audio/Target.h +++ b/include/cinder/audio/Target.h @@ -32,30 +32,44 @@ namespace cinder { namespace audio { typedef std::shared_ptr TargetFileRef; +namespace dsp { + class Converter; +} + //! Base class that is used to create and write to an audio destination. Currently only supports .wav encoding. class CI_API TargetFile { public: - static std::unique_ptr create( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType = SampleType::INT_16, const std::string &extension = "" ); - static std::unique_ptr create( const fs::path &path, size_t sampleRate, size_t numChannels, SampleType sampleType = SampleType::INT_16, const std::string &extension = "" ); - virtual ~TargetFile() {} + static std::unique_ptr create( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType = SampleType::INT_16, size_t sampleRateNative = 0, const std::string &extension = "" ); + static std::unique_ptr create( const fs::path &path, size_t sampleRate, size_t numChannels, SampleType sampleType = SampleType::INT_16, size_t sampleRateNative = 0, const std::string &extension = "" ); + virtual ~TargetFile(); void write( const Buffer *buffer ); void write( const Buffer *buffer, size_t numFrames ); void write( const Buffer *buffer, size_t numFrames, size_t frameOffset ); + //! Returns the user facing sample rate (input) size_t getSampleRate() const { return mSampleRate; } + //! Returns the true sample rate of the target file. \note Actual input samplerate may differ. \see getSampleRate() + size_t getSampleRateNative() const { return mSampleRateNative; } + size_t getNumChannels() const { return mNumChannels; } protected: - TargetFile( size_t sampleRate, size_t numChannels, SampleType sampleType ) - : mSampleRate( sampleRate ), mNumChannels( numChannels ), mSampleType( sampleType ) - {} + TargetFile( size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative = 0 ); // Implement to write \a numFrames frames of \a buffer to file. The writing begins at \a frameOffset. virtual void performWrite( const Buffer *buffer, size_t numFrames, size_t frameOffset ) = 0; + //! Implementations should override and return true if they can provide samplerate conversion. If false (default), a Converter will be used if needed. + virtual bool supportsConversion() { return false; } - size_t mSampleRate, mNumChannels; + // Sets up samplerate conversion if needed. + void setupSampleRateConversion(); + + size_t mSampleRate, mSampleRateNative, mNumChannels, mMaxFramesPerConversion; SampleType mSampleType; + + std::unique_ptr mConverter; + BufferDynamic mConverterSourceBuffer, mConverterDestBuffer; }; } } // namespace cinder::audio diff --git a/include/cinder/audio/cocoa/FileCoreAudio.h b/include/cinder/audio/cocoa/FileCoreAudio.h index 530917d73f..2e88fe7de9 100644 --- a/include/cinder/audio/cocoa/FileCoreAudio.h +++ b/include/cinder/audio/cocoa/FileCoreAudio.h @@ -69,7 +69,7 @@ class SourceFileCoreAudio : public SourceFile { class TargetFileCoreAudio : public TargetFile { public: - TargetFileCoreAudio( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, const std::string &extension ); + TargetFileCoreAudio( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative, const std::string &extension ); virtual ~TargetFileCoreAudio() {} void performWrite( const Buffer *buffer, size_t numFrames, size_t frameOffset ) override; diff --git a/src/cinder/audio/FileOggVorbis.cpp b/src/cinder/audio/FileOggVorbis.cpp index 4e147f92e6..6616a92139 100644 --- a/src/cinder/audio/FileOggVorbis.cpp +++ b/src/cinder/audio/FileOggVorbis.cpp @@ -192,15 +192,15 @@ long SourceFileOggVorbis::tellFn( void *datasource ) // TargetFileOggVorbis // ---------------------------------------------------------------------------------------------------- -TargetFileOggVorbis::TargetFileOggVorbis( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType ) - : cinder::audio::TargetFile( sampleRate, numChannels, sampleType ), mDataTarget( dataTarget ) +TargetFileOggVorbis::TargetFileOggVorbis( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative ) + : cinder::audio::TargetFile( sampleRate, numChannels, sampleType, sampleRateNative ), mDataTarget( dataTarget ) { CI_ASSERT( mDataTarget ); mStream = mDataTarget->getStream(); vorbis_info_init( &mVorbisInfo ); - auto status = vorbis_encode_init_vbr( &mVorbisInfo, getNumChannels(), getSampleRate(), mVorbisBaseQuality ); + auto status = vorbis_encode_init_vbr( &mVorbisInfo, getNumChannels(), getSampleRateNative(), mVorbisBaseQuality ); if ( status ) { throw AudioFormatExc( string( "TargetFileOggVorbis: invalid quality setting." ) ); } diff --git a/src/cinder/audio/SampleRecorderNode.cpp b/src/cinder/audio/SampleRecorderNode.cpp index e65d635ff7..d104bb9e79 100644 --- a/src/cinder/audio/SampleRecorderNode.cpp +++ b/src/cinder/audio/SampleRecorderNode.cpp @@ -162,12 +162,12 @@ BufferRef BufferRecorderNode::getRecordedCopy() const return mCopiedBuffer; } -void BufferRecorderNode::writeToFile( const fs::path &filePath, SampleType sampleType ) +void BufferRecorderNode::writeToFile( const fs::path &filePath, SampleType sampleType, size_t destSampleRate ) { size_t currentWritePos = mWritePos; BufferRef copiedBuffer = getRecordedCopy(); - audio::TargetFileRef target = audio::TargetFile::create( filePath, getSampleRate(), getNumChannels(), sampleType ); + audio::TargetFileRef target = audio::TargetFile::create( filePath, getSampleRate(), getNumChannels(), sampleType, destSampleRate ); target->write( copiedBuffer.get(), currentWritePos ); } diff --git a/src/cinder/audio/Target.cpp b/src/cinder/audio/Target.cpp index fc61e9294f..d88465f4e1 100644 --- a/src/cinder/audio/Target.cpp +++ b/src/cinder/audio/Target.cpp @@ -22,6 +22,7 @@ */ #include "cinder/audio/Target.h" +#include "cinder/audio/dsp/Converter.h" #include "cinder/CinderAssert.h" #include "cinder/audio/FileOggVorbis.h" @@ -39,7 +40,7 @@ namespace cinder { namespace audio { // TODO: these should be replaced with a generic registrar derived from the ImageIo stuff. -std::unique_ptr TargetFile::create( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, const std::string &extension ) +std::unique_ptr TargetFile::create( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative, const std::string &extension ) { #if ! defined( CINDER_UWP ) || ( _MSC_VER > 1800 ) std::string ext = dataTarget->getFilePathHint().extension().string(); @@ -49,24 +50,47 @@ std::unique_ptr TargetFile::create( const DataTargetRef &dataTarget, ext = ( ( ! ext.empty() ) && ( ext[0] == '.' ) ) ? ext.substr( 1, string::npos ) : ext; if ( ext == "ogg" ) { - return std::unique_ptr( new TargetFileOggVorbis( dataTarget, sampleRate, numChannels, sampleType ) ); + return std::unique_ptr( new TargetFileOggVorbis( dataTarget, sampleRate, numChannels, sampleType, sampleRateNative ) ); } else { #if defined( CINDER_COCOA ) - return std::unique_ptr( new cocoa::TargetFileCoreAudio( dataTarget, sampleRate, numChannels, sampleType, ext ) ); + return std::unique_ptr( new cocoa::TargetFileCoreAudio( dataTarget, sampleRate, numChannels, sampleType, sampleRateNative, ext ) ); #elif defined( CINDER_MSW ) + CI_ASSERT_MSG( sampleRateNative == 0 || sampleRateNative == sampleRate, "sample rate conversion not yet implemented on MSW" ); return std::unique_ptr( new msw::TargetFileMediaFoundation( dataTarget, sampleRate, numChannels, sampleType, ext ) ); #endif } } -std::unique_ptr TargetFile::create( const fs::path &path, size_t sampleRate, size_t numChannels, SampleType sampleType, const std::string &extension ) +std::unique_ptr TargetFile::create( const fs::path &path, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative, const std::string &extension ) { - return create( (DataTargetRef)writeFile( path ), sampleRate, numChannels, sampleType, extension ); + return create( (DataTargetRef)writeFile( path ), sampleRate, numChannels, sampleType, sampleRateNative, extension ); +} + +TargetFile::TargetFile( size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative ) + : mSampleRate( sampleRate ), mNumChannels( numChannels ), mSampleType( sampleType ), mSampleRateNative( sampleRateNative ), mMaxFramesPerConversion( 4092 ) +{ + setupSampleRateConversion(); +} + +TargetFile::~TargetFile() = default; + +void TargetFile::setupSampleRateConversion() +{ + if( ! mSampleRateNative ) { + mSampleRateNative = mSampleRate; + } + else if( mSampleRateNative != mSampleRate) { + if( ! supportsConversion() ) { + mConverter = audio::dsp::Converter::create( mSampleRate, mSampleRateNative, getNumChannels(), getNumChannels(), mMaxFramesPerConversion ); + mConverterSourceBuffer.setSize( mMaxFramesPerConversion, getNumChannels() ); + mConverterDestBuffer.setSize( mMaxFramesPerConversion, getNumChannels() ); + } + } } void TargetFile::write( const Buffer *buffer ) { - performWrite( buffer, buffer->getNumFrames(), 0 ); + write( buffer, buffer->getNumFrames(), 0 ); } void TargetFile::write( const Buffer *buffer, size_t numFrames ) @@ -76,7 +100,7 @@ void TargetFile::write( const Buffer *buffer, size_t numFrames ) CI_ASSERT_MSG( numFrames <= buffer->getNumFrames(), "numFrames out of bounds" ); - performWrite( buffer, numFrames, 0 ); + write( buffer, numFrames, 0 ); } void TargetFile::write( const Buffer *buffer, size_t numFrames, size_t frameOffset ) @@ -86,7 +110,30 @@ void TargetFile::write( const Buffer *buffer, size_t numFrames, size_t frameOffs CI_ASSERT_MSG( numFrames + frameOffset <= buffer->getNumFrames(), "numFrames + frameOffset out of bounds" ); - performWrite( buffer, numFrames, frameOffset ); + if( mConverter ) { + auto currFrame = frameOffset; + auto lastFrame = frameOffset + numFrames; + + // process buffer in chunks of mMaxFramesPerConversion + while( currFrame != lastFrame ) { + auto numSourceFrames = std::min( mMaxFramesPerConversion, lastFrame - currFrame ); + auto numDestFrames = size_t( numSourceFrames * (float)getSampleRateNative() / (float)getSampleRate() ); + + // copy buffer into temporary buffer to remove frame offset (needed for mConverter->convert) + mConverterSourceBuffer.copyOffset( *buffer, numSourceFrames, 0, currFrame ); + + mConverterSourceBuffer.setNumFrames( numSourceFrames ); + mConverterDestBuffer.setNumFrames( numDestFrames ); + tie( numSourceFrames, numDestFrames ) = mConverter->convert( &mConverterSourceBuffer, &mConverterDestBuffer ); + + performWrite( &mConverterDestBuffer, numDestFrames, 0 ); + + currFrame += numSourceFrames; + } + } + else { + performWrite( buffer, numFrames, frameOffset ); + } } } } // namespace cinder::audio diff --git a/src/cinder/audio/cocoa/FileCoreAudio.cpp b/src/cinder/audio/cocoa/FileCoreAudio.cpp index 3120405bec..754433378e 100644 --- a/src/cinder/audio/cocoa/FileCoreAudio.cpp +++ b/src/cinder/audio/cocoa/FileCoreAudio.cpp @@ -147,8 +147,8 @@ vector SourceFileCoreAudio::getSupportedExtensions() // MARK: - TargetFileCoreAudio // ---------------------------------------------------------------------------------------------------- -TargetFileCoreAudio::TargetFileCoreAudio( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, const std::string &extension ) - : TargetFile( sampleRate, numChannels, sampleType ) +TargetFileCoreAudio::TargetFileCoreAudio( const DataTargetRef &dataTarget, size_t sampleRate, size_t numChannels, SampleType sampleType, size_t sampleRateNative, const std::string &extension ) + : TargetFile( sampleRate, numChannels, sampleType, sampleRateNative ) { ::CFURLRef targetUrl = ci::cocoa::createCfUrl( Url( dataTarget->getFilePath().string() ) ); ::AudioFileTypeID fileType = getFileTypeIdFromExtension( extension ); @@ -156,10 +156,10 @@ TargetFileCoreAudio::TargetFileCoreAudio( const DataTargetRef &dataTarget, size_ ::AudioStreamBasicDescription fileAsbd; switch( mSampleType ) { case SampleType::INT_16: - fileAsbd = createInt16Asbd( mSampleRate, mNumChannels, true ); + fileAsbd = createInt16Asbd( getSampleRateNative(), mNumChannels, true ); break; case SampleType::FLOAT_32: - fileAsbd = createFloatAsbd( mSampleRate, mNumChannels, true ); + fileAsbd = createFloatAsbd( getSampleRateNative(), mNumChannels, true ); break; default: CI_ASSERT_NOT_REACHABLE(); @@ -175,7 +175,7 @@ TargetFileCoreAudio::TargetFileCoreAudio( const DataTargetRef &dataTarget, size_ ::CFRelease( targetUrl ); mExtAudioFile = ExtAudioFilePtr( audioFile ); - ::AudioStreamBasicDescription clientAsbd = createFloatAsbd( mSampleRate, mNumChannels, false ); + ::AudioStreamBasicDescription clientAsbd = createFloatAsbd( getSampleRateNative(), mNumChannels, false ); status = ::ExtAudioFileSetProperty( mExtAudioFile.get(), kExtAudioFileProperty_ClientDataFormat, sizeof( clientAsbd ), &clientAsbd ); CI_VERIFY( status == noErr );