TEST (AudioDataTest, PrependToInitialised) { KeyFinder::AudioData a; KeyFinder::AudioData b; a.setChannels(1); a.setFrameRate(1); ASSERT_THROW(a.prepend(b), KeyFinder::Exception); b.setChannels(2); b.setFrameRate(1); ASSERT_THROW(a.prepend(b), KeyFinder::Exception); b.setChannels(1); b.setFrameRate(2); ASSERT_THROW(a.prepend(b), KeyFinder::Exception); b.setChannels(1); b.setFrameRate(1); ASSERT_NO_THROW(a.prepend(b)); a.addToFrameCount(1); b.addToFrameCount(1); a.setSampleByFrame(0, 0, 10.0); b.setSampleByFrame(0, 0, 20.0); a.prepend(b); ASSERT_EQ(2, a.getFrameCount()); ASSERT_FLOAT_EQ(20.0, a.getSampleByFrame(0, 0)); ASSERT_FLOAT_EQ(10.0, a.getSampleByFrame(1, 0)); }
TEST (AudioDataTest, SliceFromBack) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(1); KeyFinder::AudioData* b = NULL; KeyFinder::AudioData* nullPtr = NULL; ASSERT_THROW(b = a.sliceSamplesFromBack(1), KeyFinder::Exception); ASSERT_EQ(nullPtr, b); a.addToFrameCount(10); ASSERT_THROW(b = a.sliceSamplesFromBack(11), KeyFinder::Exception); ASSERT_EQ(nullPtr, b); a.resetIterators(); float v = 0; while (a.writeIteratorWithinUpperBound()) { a.setSampleAtWriteIterator(v); a.advanceWriteIterator(); v += 1.0; } ASSERT_NO_THROW(b = a.sliceSamplesFromBack(5)); ASSERT_NE(nullPtr, b); ASSERT_EQ(5, a.getSampleCount()); ASSERT_EQ(5, b->getSampleCount()); ASSERT_FLOAT_EQ(5.0, b->getSample(0)); ASSERT_FLOAT_EQ(9.0, b->getSample(4)); delete b; }
TEST (LowPassFilterTest, WorksOnRepetitiveWaves) { // make two sine waves, but this time, several seconds long unsigned int samples = frameRate * 5; KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(samples); for (unsigned int i = 0; i < samples; i++) { float sample = 0.0; sample += sine_wave(i, highFrequency, frameRate, magnitude); // high freq sample += sine_wave(i, lowFrequency, frameRate, magnitude); // low freq a.setSample(i, sample); // ensure repetition of sine waves is perfect... if (i >= frameRate) { ASSERT_NEAR(a.getSample(i), a.getSample(i - frameRate), tolerance); } } KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; lpf->filter(a, w); delete lpf; // test for lower wave only for (unsigned int i = 0; i < samples; i++) { float expected = sine_wave(i, lowFrequency, frameRate, magnitude); ASSERT_NEAR(expected, a.getSample(i), tolerance); } }
TEST (AudioDataTest, DownsamplerInsistsOnMonophonicAudio) { KeyFinder::AudioData a; a.setChannels(2); a.setFrameRate(100); a.addToSampleCount(10); ASSERT_THROW(a.downsample(5), KeyFinder::Exception); a.reduceToMono(); ASSERT_NO_THROW(a.downsample(5)); }
TEST (LowPassFilterTest, InsistsOnMonophonicAudio) { KeyFinder::AudioData a; a.setChannels(2); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; ASSERT_THROW(lpf->filter(a, w), KeyFinder::Exception); a.reduceToMono(); ASSERT_NO_THROW(lpf->filter(a, w)); delete lpf; }
TEST (LowPassFilterTest, InitialisesNullBuffer) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; std::vector<double>* nullPtr = NULL; ASSERT_EQ(nullPtr, w.lpfBuffer); lpf->filter(a, w); ASSERT_NE(nullPtr, w.lpfBuffer); }
TEST (LowPassFilterTest, DoesntAlterAudioMetadata) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; lpf->filter(a, w); delete lpf; ASSERT_EQ(1, a.getChannels()); ASSERT_EQ(frameRate, a.getFrameRate()); ASSERT_EQ(frameRate, a.getSampleCount()); }
TEST (AudioDataTest, DiscardFromFront) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(1); ASSERT_THROW(a.discardFramesFromFront(1), KeyFinder::Exception); a.addToFrameCount(10); ASSERT_THROW(a.discardFramesFromFront(11), KeyFinder::Exception); ASSERT_NO_THROW(a.discardFramesFromFront(0)); a.setSampleByFrame(5, 0, 10.0); ASSERT_NO_THROW(a.discardFramesFromFront(5)); ASSERT_EQ(5, a.getFrameCount()); ASSERT_FLOAT_EQ(10.0, a.getSampleByFrame(0, 0)); }
TEST (AudioDataTest, PrependToNew) { KeyFinder::AudioData a; KeyFinder::AudioData b; ASSERT_NO_THROW(a.prepend(b)); ASSERT_EQ(0, a.getChannels()); ASSERT_EQ(0, a.getFrameRate()); b.setChannels(1); b.setFrameRate(1); b.addToFrameCount(1); ASSERT_NO_THROW(a.prepend(b)); ASSERT_EQ(1, a.getChannels()); ASSERT_EQ(1, a.getFrameRate()); ASSERT_EQ(1, a.getFrameCount()); }
TEST (AudioDataTest, DownsamplerResamplesIntegralRelationship) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(100); a.addToSampleCount(10); for (unsigned int i = 0; i < 5; i++) a.setSample(i, 100.0); for (unsigned int i = 5; i < 10; i++) a.setSample(i, 500.0); a.downsample(5); ASSERT_EQ(20, a.getFrameRate()); ASSERT_EQ(2, a.getSampleCount()); ASSERT_FLOAT_EQ(100.0, a.getSample(0)); ASSERT_FLOAT_EQ(500.0, a.getSample(1)); }
TEST (AudioDataTest, DownsamplerResamplesNonintegralRelationship) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(100); a.addToSampleCount(12); for (unsigned int i = 0; i < 5; i++) a.setSample(i, 100.0); for (unsigned int i = 5; i < 10; i++) a.setSample(i, 500.0); for (unsigned int i = 10; i < 12; i++) a.setSample(i, 1000.0); a.downsample(5); ASSERT_EQ(3, a.getSampleCount()); ASSERT_FLOAT_EQ(100.0, a.getSample(0)); ASSERT_FLOAT_EQ(500.0, a.getSample(1)); // this doesn't make total mathematical sense but I'm taking a shortcut for performance ASSERT_FLOAT_EQ(1000.0, a.getSample(2)); }
TEST (LowPassFilterTest, KillsHigherFreqs) { // make a high frequency sine wave, one second long KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); for (unsigned int i = 0; i < frameRate; i++) { a.setSample(i, sine_wave(i, highFrequency, frameRate, magnitude)); } KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; lpf->filter(a, w); delete lpf; // test for near silence for (unsigned int i = 0; i < frameRate; i++) { ASSERT_NEAR(0.0, a.getSample(i), tolerance); } }
TEST (LowPassFilterTest, MaintainsLowerFreqs) { // make a low frequency sine wave, one second long KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); for (unsigned int i = 0; i < frameRate; i++) { a.setSample(i, sine_wave(i, lowFrequency, frameRate, magnitude)); } KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; lpf->filter(a, w); delete lpf; // test for near perfect reproduction for (unsigned int i = 0; i < frameRate; i++) { float expected = sine_wave(i, lowFrequency, frameRate, magnitude); ASSERT_NEAR(expected, a.getSample(i), tolerance); } }
TEST (AudioDataTest, Iterators) { KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(1); a.addToSampleCount(10); a.setSample(0, 10.0); a.setSample(1, 20.0); a.setSample(3, 50.0); a.resetIterators(); // this is required before each use ASSERT_FLOAT_EQ(10.0, a.getSampleAtReadIterator()); a.setSampleAtWriteIterator(15.0); ASSERT_FLOAT_EQ(15.0, a.getSampleAtReadIterator()); a.advanceReadIterator(); a.advanceWriteIterator(); ASSERT_FLOAT_EQ(20.0, a.getSampleAtReadIterator()); a.setSampleAtWriteIterator(25.0); ASSERT_FLOAT_EQ(25.0, a.getSampleAtReadIterator()); a.advanceReadIterator(2); a.advanceWriteIterator(2); ASSERT_FLOAT_EQ(50.0, a.getSampleAtReadIterator()); a.setSampleAtWriteIterator(55.0); ASSERT_FLOAT_EQ(55.0, a.getSampleAtReadIterator()); a.resetIterators(); ASSERT_FLOAT_EQ(15.0, a.getSampleAtReadIterator()); a.setSampleAtWriteIterator(150.0); ASSERT_FLOAT_EQ(150.0, a.getSampleAtReadIterator()); ASSERT_TRUE(a.readIteratorWithinUpperBound()); ASSERT_TRUE(a.writeIteratorWithinUpperBound()); a.advanceReadIterator(10); a.advanceWriteIterator(10); ASSERT_FALSE(a.readIteratorWithinUpperBound()); ASSERT_FALSE(a.writeIteratorWithinUpperBound()); }
TEST (AudioDataTest, DownsamplerResamplesSineWave) { unsigned int frameRate = 10000; unsigned int frames = frameRate * 4; float freq = 20; float magnitude = 32768.0; unsigned int factor = 5; KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frames); for (unsigned int i = 0; i < frames; i++) a.setSample(i, sine_wave(i, freq, frameRate, magnitude)); a.downsample(factor); unsigned int newFrameRate = frameRate / factor; unsigned int newFrames = frames / factor; ASSERT_EQ(newFrameRate, a.getFrameRate()); ASSERT_EQ(newFrames, a.getSampleCount()); for (unsigned int i = 0; i < newFrames; i++) ASSERT_NEAR(sine_wave(i, freq, newFrameRate, magnitude), a.getSample(i), magnitude * 0.05); }
TEST (LowPassFilterTest, DoesBothAtOnce) { // make two sine waves, one second long KeyFinder::AudioData a; a.setChannels(1); a.setFrameRate(frameRate); a.addToSampleCount(frameRate); for (unsigned int i = 0; i < frameRate; i++) { float sample = 0.0; sample += sine_wave(i, highFrequency, frameRate, magnitude); // high freq sample += sine_wave(i, lowFrequency, frameRate, magnitude); // low freq a.setSample(i, sample); } KeyFinder::LowPassFilter* lpf = new KeyFinder::LowPassFilter(filterOrder, frameRate, cornerFrequency, filterFFT); KeyFinder::Workspace w; lpf->filter(a, w); delete lpf; // test for lower wave only for (unsigned int i = 0; i < frameRate; i++) { float expected = sine_wave(i, lowFrequency, frameRate, magnitude); ASSERT_NEAR(expected, a.getSample(i), tolerance); } }
KeyFinder::AudioData* LibAvDecoder::decodeFile(const QString& filePath){ QMutexLocker codecMutexLocker(&codecMutex); // mutex the preparatory section of this method AVCodec *codec = NULL; AVFormatContext *fCtx = NULL; AVCodecContext *cCtx = NULL; AVDictionary* dict = NULL; // convert filepath #ifdef Q_OS_WIN const wchar_t* filePathWc = reinterpret_cast<const wchar_t*>(filePath.constData()); const char* filePathCh = utf16_to_utf8(filePathWc); #else QByteArray encodedPath = QFile::encodeName(filePath); const char* filePathCh = encodedPath; #endif // open file int openInputResult = avformat_open_input(&fCtx, filePathCh, NULL, NULL); if(openInputResult != 0){ std::ostringstream ss; ss << "Could not open audio file (" << openInputResult << ")"; throw KeyFinder::Exception(ss.str()); } if(avformat_find_stream_info(fCtx,NULL) < 0){ throw KeyFinder::Exception("Could not find stream information"); } int audioStream = -1; for(int i=0; i<(signed)fCtx->nb_streams; i++){ if(fCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){ audioStream = i; break; } } if(audioStream == -1){ throw KeyFinder::Exception("Could not find an audio stream"); } // Determine stream codec cCtx = fCtx->streams[audioStream]->codec; codec = avcodec_find_decoder(cCtx->codec_id); if(codec == NULL){ throw KeyFinder::Exception("Audio stream has unsupported codec"); } // Open codec int codecOpenResult = avcodec_open2(cCtx, codec, &dict); if(codecOpenResult < 0){ std::ostringstream ss; ss << "Could not open audio codec: " << codec->long_name << " (" << codecOpenResult << ")"; throw KeyFinder::Exception(ss.str()); } ReSampleContext* rsCtx = av_audio_resample_init( cCtx->channels, cCtx->channels, cCtx->sample_rate, cCtx->sample_rate, AV_SAMPLE_FMT_S16, cCtx->sample_fmt, 0, 0, 0, 0); if(rsCtx == NULL){ throw KeyFinder::Exception("Could not create ReSampleContext"); } qDebug("Decoding %s (%s, %d)", filePathCh, av_get_sample_fmt_name(cCtx->sample_fmt), cCtx->sample_rate); codecMutexLocker.unlock(); // Prep buffer KeyFinder::AudioData *audio = new KeyFinder::AudioData(); audio->setFrameRate(cCtx->sample_rate); audio->setChannels(cCtx->channels); // Decode stream AVPacket avpkt; int badPacketCount = 0; while(true){ av_init_packet(&avpkt); if(av_read_frame(fCtx, &avpkt) < 0) break; if(avpkt.stream_index == audioStream){ try{ int result = decodePacket(cCtx, rsCtx, &avpkt, audio); if(result != 0){ if(badPacketCount < 100){ badPacketCount++; }else{ throw KeyFinder::Exception("100 bad packets"); } } }catch(KeyFinder::Exception& e){ throw e; } } av_free_packet(&avpkt); } codecMutexLocker.relock(); audio_resample_close(rsCtx); int codecCloseResult = avcodec_close(cCtx); if(codecCloseResult < 0){ qCritical("Error closing audio codec: %s (%d)", codec->long_name, codecCloseResult); } codecMutexLocker.unlock(); av_close_input_file(fCtx); return audio; }
KeyFinder::AudioData* LibAvDecoder::decodeFile(const QString& filePath, const int maxDuration){ QMutexLocker codecMutexLocker(&codecMutex); // mutex the preparatory section of this method AVCodec *codec = NULL; AVFormatContext *fCtx = NULL; AVCodecContext *cCtx = NULL; AVDictionary* dict = NULL; // convert filepath #ifdef Q_OS_WIN const wchar_t* filePathWc = reinterpret_cast<const wchar_t*>(filePath.constData()); const char* filePathCh = utf16_to_utf8(filePathWc); #else QByteArray encodedPath = QFile::encodeName(filePath); const char* filePathCh = encodedPath; #endif // open file int openInputResult = avformat_open_input(&fCtx, filePathCh, NULL, NULL); if(openInputResult != 0){ throw KeyFinder::Exception(GuiStrings::getInstance()->libavCouldNotOpenFile(openInputResult).toLocal8Bit().constData()); } if(avformat_find_stream_info(fCtx, NULL) < 0){ av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavCouldNotFindStreamInformation().toLocal8Bit().constData()); } int audioStream = -1; for(int i=0; i<(signed)fCtx->nb_streams; i++){ if(fCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){ audioStream = i; break; } } if(audioStream == -1){ av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavCouldNotFindAudioStream().toLocal8Bit().constData()); } // Determine duration int durationSeconds = fCtx->duration / AV_TIME_BASE; int durationMinutes = durationSeconds / 60; // First condition is a hack for bizarre overestimation of some MP3s if(durationMinutes < 720 && durationSeconds > maxDuration * 60){ av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->durationExceedsPreference(durationMinutes, durationSeconds % 60, maxDuration).toLocal8Bit().constData()); } // Determine stream codec cCtx = fCtx->streams[audioStream]->codec; codec = avcodec_find_decoder(cCtx->codec_id); if(codec == NULL){ av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavUnsupportedCodec().toLocal8Bit().constData()); } // Open codec int codecOpenResult = avcodec_open2(cCtx, codec, &dict); if(codecOpenResult < 0){ av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavCouldNotOpenCodec(codec->long_name, codecOpenResult).toLocal8Bit().constData()); } ReSampleContext* rsCtx = av_audio_resample_init( cCtx->channels, cCtx->channels, cCtx->sample_rate, cCtx->sample_rate, AV_SAMPLE_FMT_S16, cCtx->sample_fmt, 0, 0, 0, 0); if(rsCtx == NULL){ avcodec_close(cCtx); av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavCouldNotCreateResampleContext().toLocal8Bit().constData()); } qDebug("Decoding %s (%s, %d)", filePathCh, av_get_sample_fmt_name(cCtx->sample_fmt), cCtx->sample_rate); codecMutexLocker.unlock(); // Prep buffer KeyFinder::AudioData *audio = new KeyFinder::AudioData(); audio->setFrameRate(cCtx->sample_rate); audio->setChannels(cCtx->channels); // Decode stream AVPacket avpkt; int badPacketCount = 0; int badPacketThreshold = 100; while(true){ av_init_packet(&avpkt); if(av_read_frame(fCtx, &avpkt) < 0) break; if(avpkt.stream_index == audioStream){ try{ int result = decodePacket(cCtx, rsCtx, &avpkt, audio); if(result != 0){ if(badPacketCount < badPacketThreshold){ badPacketCount++; }else{ avcodec_close(cCtx); av_close_input_file(fCtx); throw KeyFinder::Exception(GuiStrings::getInstance()->libavTooManyBadPackets(badPacketThreshold).toLocal8Bit().constData()); } } }catch(KeyFinder::Exception& e){ throw e; } } av_free_packet(&avpkt); } codecMutexLocker.relock(); audio_resample_close(rsCtx); int codecCloseResult = avcodec_close(cCtx); if(codecCloseResult < 0){ qCritical("Error closing audio codec: %s (%d)", codec->long_name, codecCloseResult); } codecMutexLocker.unlock(); av_close_input_file(fCtx); return audio; }
TEST (AudioDataTest, FrameRate) { KeyFinder::AudioData a; a.setFrameRate(44100); ASSERT_EQ(44100, a.getFrameRate()); ASSERT_THROW(a.setFrameRate(0), KeyFinder::Exception); }
const char* kfinder_get_key(short signed int *samples, unsigned int nb_samples, short unsigned int frame_rate, short unsigned int nb_channels) { // Check input parameter. if ((samples == NULL) || (nb_samples == 0) || (frame_rate == 0) || (nb_channels == 0)) { return ""; } // Build the main computing object. KeyFinder::KeyFinder k; // Build an empty audio object KeyFinder::AudioData a; // Prepare the object for your audio stream a.setFrameRate(frame_rate); a.setChannels(nb_channels); a.addToSampleCount(nb_samples); // Copy your audio into the object (as float). for (unsigned int i = 0; i < nb_samples; i++) { a.setSample(i, (float)samples[i]); } // Run the analysis KeyFinder::key_t r; try { r = k.keyOfAudio(a); } catch(const std::exception& e) { cerr << "libKeyFinder: exception: " << e.what() << endl; return ""; } catch(...) { cerr << "libKeyFinder: unknown exception" << endl; return ""; } // And do something with the result! switch(r) { case KeyFinder::A_MAJOR: return "AM"; case KeyFinder::A_MINOR: return "Am"; case KeyFinder::B_FLAT_MAJOR: return "BbM"; case KeyFinder::B_FLAT_MINOR: return "Bbm"; case KeyFinder::B_MAJOR: return "BM"; case KeyFinder::B_MINOR: return "Bm"; case KeyFinder::C_MAJOR: return "CM"; case KeyFinder::C_MINOR: return "Cm"; case KeyFinder::D_FLAT_MAJOR: return "DbM"; case KeyFinder::D_FLAT_MINOR: return "Dbm"; case KeyFinder::D_MAJOR: return "DM"; case KeyFinder::D_MINOR: return "Dm"; case KeyFinder::E_FLAT_MAJOR: return "EbM"; case KeyFinder::E_FLAT_MINOR: return "Ebm"; case KeyFinder::E_MAJOR: return "EM"; case KeyFinder::E_MINOR: return "Em"; case KeyFinder::F_MAJOR: return "FM"; case KeyFinder::F_MINOR: return "Fm"; case KeyFinder::G_FLAT_MAJOR: return "GbM"; case KeyFinder::G_FLAT_MINOR: return "Gbm"; case KeyFinder::G_MAJOR: return "GM"; case KeyFinder::G_MINOR: return "Gm"; case KeyFinder::A_FLAT_MAJOR: return "AbM"; case KeyFinder::A_FLAT_MINOR: return "Abm"; case KeyFinder::SILENCE: return ""; default: return ""; } }