Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a unit test for Pitch Melodia #1405

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions test/src/unittests/extractor/test_tonalextractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env python

# Copyright (C) 2006-2021 Music Technology Group - Universitat Pompeu Fabra
#
# This file is part of Essentia
#
# Essentia is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the Affero GNU General Public License
# version 3 along with this program. If not, see http://www.gnu.org/licenses/
import os.path

from essentia_test import *
import numpy as np


class TestTonalExtractor(TestCase):

def testEmpty(self):
# Test if the algorithm handles an empty input signal correctly
with self.assertRaises(RuntimeError):
chords_changes_rate, _, _, chords_number_rate, _, _, _, _, _, _, _, key_strength = TonalExtractor()(np.array([], dtype=np.float32))

def testSilence(self):
# In this test we check three of the output parameters of type real
silence_vec = np.zeros(44100, dtype=np.single)
chords_changes_rate, _, _, chords_number_rate, _, _, _, _, _, _, _, key_strength = TonalExtractor()(silence_vec)
self.assertEqual(chords_changes_rate, 0.0)
self.assertGreaterEqual(chords_number_rate, 0.0)
self.assertEqual(key_strength, 0.0)

def testInvalidParameters(self):
# Test if the algorithm handles invalid parameters correctly
extractor = TonalExtractor()

# Test case 1: Negative frameSize
with self.subTest(msg="Negative frameSize"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=-1, hopSize=2048, tuningFrequency=440.0)

# Test case 2: Negative hopSize
with self.subTest(msg="Negative hopSize"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=4096, hopSize=-1, tuningFrequency=440.0)

# Test case 3: Negative tuningFrequency
with self.subTest(msg="Negative tuningFrequency"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=4096, hopSize=2048, tuningFrequency=-440.0)

# Test case 4: Zero frameSize and hopSize
with self.subTest(msg="Zero frameSize and hopSize"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=0, hopSize=0, tuningFrequency=440.0)

# Test case 5: Zero frameSize
with self.subTest(msg="Zero frameSize"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=0, hopSize=2048, tuningFrequency=440.0)

# Test case 6: Zero hopSize
with self.subTest(msg="Zero hopSize"):
with self.assertRaises(RuntimeError):
extractor.configure(frameSize=4096, hopSize=0, tuningFrequency=440.0)

# Test case 7: Non-negative parameters
with self.subTest(msg="Valid parameters"):
# This should not raise an exception
extractor.configure(frameSize=4096, hopSize=2048, tuningFrequency=440.0)

def testRandomInput(self):
n = 10
for _ in range(n):
rand_input = np.random.random(88200).astype(np.single) * 2 - 1
result = TonalExtractor()(rand_input)
expected_result = np.sum(rand_input * rand_input) ** 0.67
self.assertAlmostEqual(result[0], expected_result, 9.999e+02)

def testRegression(self):
frameSizes = [256, 512, 1024, 2048, 4096, 8192]
hopSizes = [128, 256, 512, 1024, 2048, 4096]

input_filename = join(testdata.audio_dir, "recorded", "dubstep.wav") # Replace 'testdata' with actual path
realAudio = MonoLoader(filename=input_filename)()

# Iterate through pairs of frameSize and corresponding hopSize
# TODO: Extend loop to try different tuningFrequency values
for fs, hs in zip(frameSizes, hopSizes):
with self.subTest(frameSize=fs, hopSize=hs):
# Process the algorithm on real audio with the current frameSize and hopSize
te = TonalExtractor()
te.configure(frameSize=fs, hopSize=hs)
chords_changes_rate, _, _, chords_number_rate, _, _, _, _, _, _, _, key_strength= te(realAudio)

# Perform assertions on one or more outputs
# Example: Assert that chords_changes_rate is a non-negative scalar
self.assertIsInstance(chords_changes_rate, (int, float))
self.assertGreaterEqual(chords_changes_rate, 0)
self.assertIsInstance(chords_number_rate, (int, float))
self.assertGreaterEqual(chords_number_rate, 0)
self.assertIsInstance(key_strength, (int, float))
self.assertGreaterEqual(key_strength, 0)
# You can add more assertions on other outputs as needed

def testRealAudio(self):

# These reference values could also be compared with the results of tonal extractors of alternative
# audio libraries (e.g. MadMom, libs from Alexander Lerch etc.)
# ccr = chord changes rate ; cnr = chord number rate; ks = key strength
mozart_ccr = 0.03400309011340141
mozart_cnr = 0.010819165036082268
mozart_ks = 0.8412253260612488

vivaldi_ccr = 0.052405908703804016
vivaldi_cnr = 0.004764173645526171
vivaldi_ks = 0.7122617959976196

thresh = 0.5

def test_on_real_audio(path, ccr, cnr, ks):
realAudio = MonoLoader(filename=path)()

# Use default configuration of algorothm
# This function could be extended to test for more outputs
# TODO: Extend to test non-scalar and string outputs:
# i.e. chords_histogram, chords_progression, chords_scale, chords_strength
# hpcp, hpcp_highres, key_key and key_scale
te = TonalExtractor()
chords_changes_rate, _, _, chords_number_rate, _, _, _, _, _, _, _, key_strength= te(realAudio)
self.assertIsInstance(chords_changes_rate, (int, float))
self.assertGreaterEqual(chords_changes_rate, 0)
self.assertAlmostEqual(chords_changes_rate, ccr, thresh)
self.assertIsInstance(chords_number_rate, (int, float))
self.assertGreaterEqual(chords_number_rate, 0)
self.assertAlmostEqual(chords_number_rate, cnr, thresh)
self.assertIsInstance(key_strength, (int, float))
self.assertGreaterEqual(key_strength, 0)
self.assertAlmostEqual(key_strength, ks, thresh)

test_on_real_audio(join(testdata.audio_dir, "recorded", "mozart_c_major_30sec.wav"), mozart_ccr, mozart_cnr, mozart_ks)
test_on_real_audio(join(testdata.audio_dir, "recorded", "Vivaldi_Sonata_5_II_Allegro.wav"), vivaldi_ccr, vivaldi_cnr, vivaldi_ks)

suite = allTests(TestTonalExtractor)

if __name__ == '__main__':
unittest.main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
181 changes: 175 additions & 6 deletions test/src/unittests/tonal/test_pitchmelodia.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,187 @@
# version 3 along with this program. If not, see http://www.gnu.org/licenses/



from essentia_test import *

import math as math
import numpy as np

class TestPitchMelodia(TestCase):

def testZero(self):
signal = zeros(256)
def testInvalidParam(self):
# Test for all the values above the boundary limits.
self.assertConfigureFails(PitchMelodia(), {'binResolution': -1})
self.assertConfigureFails(PitchMelodia(), {'binResolution': 0})
self.assertConfigureFails(PitchMelodia(), {'filterIterations': 0})
self.assertConfigureFails(PitchMelodia(), {'filterIterations': -1})
self.assertConfigureFails(PitchMelodia(), {'frameSize': -1})
self.assertConfigureFails(PitchMelodia(), {'frameSize': 0})
self.assertConfigureFails(PitchMelodia(), {'harmonicWeight': -1})
self.assertConfigureFails(PitchMelodia(), {'hopSize': 0})
self.assertConfigureFails(PitchMelodia(), {'hopSize': -1})
self.assertConfigureFails(PitchMelodia(), {'magnitudeCompression': -1})
self.assertConfigureFails(PitchMelodia(), {'magnitudeCompression': 0})
self.assertConfigureFails(PitchMelodia(), {'magnitudeCompression': 2})
self.assertConfigureFails(PitchMelodia(), {'magnitudeThreshold': -1})
self.assertConfigureFails(PitchMelodia(), {'maxFrequency': -1})
self.assertConfigureFails(PitchMelodia(), {'minDuration': 0})
self.assertConfigureFails(PitchMelodia(), {'minDuration': -1})
self.assertConfigureFails(PitchMelodia(), {'minFrequency': -1})
self.assertConfigureFails(PitchMelodia(), {'numberHarmonics': -1})
self.assertConfigureFails(PitchMelodia(), {'peakDistributionThreshold': -1})
self.assertConfigureFails(PitchMelodia(), {'peakDistributionThreshold': 2.1})
self.assertConfigureFails(PitchMelodia(), {'peakFrameThreshold': -1})
self.assertConfigureFails(PitchMelodia(), {'peakFrameThreshold': 2})
self.assertConfigureFails(PitchMelodia(), {'pitchContinuity': -1})
self.assertConfigureFails(PitchMelodia(), {'referenceFrequency': 0})
self.assertConfigureFails(PitchMelodia(), {'referenceFrequency': -1})
self.assertConfigureFails(PitchMelodia(), {'sampleRate': 0})
self.assertConfigureFails(PitchMelodia(), {'sampleRate': -1})
self.assertConfigureFails(PitchMelodia(), {'timeContinuity': 0})
self.assertConfigureFails(PitchMelodia(), {'timeContinuity': -1})

def testDefaultParameters(self):
signal = np.random.random(1024)
pitch, confidence = PitchMelodia()(signal)
self.assertAlmostEqualVector(pitch, [0., 0., 0.])
self.assertAlmostEqualVector(confidence, [0., 0., 0.])
# Assert that default parameters produce valid outputs
self.assertIsNotNone(pitch)
self.assertIsNotNone(confidence)

def testEmptyInput(self):
pitch, confidence = PitchMelodia()([])
self.assertEqualVector(pitch, [])
self.assertEqualVector(confidence, [])

def testZerosInput(self):
signal = zeros(1024)
pitch, confidence = PitchMelodia()(signal)
self.assertAlmostEqualVector(pitch, [0.] * 9)
self.assertAlmostEqualVector(confidence, [0.] * 9)

def testOnesInput(self):
signal = ones(1024)
pitch, confidence = PitchMelodia()(signal)
self.assertAlmostEqualVector(pitch, [0.] * 9)
self.assertAlmostEqualVector(confidence, [0.] * 9)

def testCustomParameters(self):
signal = np.random.random(2048)
# Use custom parameters
params = {
'binResolution': 5,
'filterIterations': 5,
'frameSize': 1024,
'guessUnvoiced': True,
'harmonicWeight': 0.9,
'hopSize': 256,
'magnitudeCompression': 0.5,
'magnitudeThreshold': 30,
'maxFrequency': 15000,
'minDuration': 50,
'minFrequency': 60,
'numberHarmonics': 15,
'peakDistributionThreshold': 1.0,
'peakFrameThreshold': 0.8,
'pitchContinuity': 30.0,
'referenceFrequency': 60,
'sampleRate': 22050,
'timeContinuity': 150
}
pitch, confidence = PitchMelodia(**params)(signal)
# Assert that custom parameters produce valid outputs
self.assertIsNotNone(pitch)
self.assertIsNotNone(confidence)

def testInputWithSilence(self):
rand_signal = np.random.random(512)
signal = np.concatenate([zeros(512), rand_signal, zeros(512)])
pitch, confidence = PitchMelodia()(signal)
# Assert that silent portions don't have pitch information
self.assertTrue(all(p == 0.0 for p in pitch[:512]))
self.assertTrue(all(c == 0.0 for c in confidence[:512]))

def testHighPitchResolution(self):
rand_signal = np.random.random(1024)
pitch, confidence = PitchMelodia(binResolution=1)(rand_signal)
# Assert that using high bin resolution produces valid outputs
self.assertIsNotNone(pitch)
self.assertIsNotNone(confidence)
self.assertEqual(len(pitch), 9)
self.assertEqual(len(confidence), 9)

def testRealCase(self):
filename = join(testdata.audio_dir, 'recorded', 'vignesh.wav')
audio = MonoLoader(filename=filename)()
pm = PitchMelodia()
pitch, pitchConfidence = pm(audio)

# np.save reference values for later np.loading
#np.save('pitchmelodiapitch.npy', pitch)
#np.save('pitchmelodiaconfidence.npy', pitchConfidence)

np.loadedPitchMelodiaPitch = np.load(join(filedir(), 'pitchmelodia/pitchmelodiapitch.npy'))
self.assertAlmostEqualVectorFixedPrecision(pitch, np.loadedPitchMelodiaPitch.tolist(), 8)

np.loadedPitchConfidence = np.load(join(filedir(), 'pitchmelodia/pitchmelodiaconfidence.npy'))
self.assertAlmostEqualVectorFixedPrecision(pitchConfidence, np.loadedPitchConfidence.tolist(), 8)

def testRealCaseEqualLoudness(self):
filename = join(testdata.audio_dir, 'recorded', 'vignesh.wav')
audio = MonoLoader(filename=filename)()
pm = PitchMelodia()
eq = EqualLoudness()
eqAudio = eq(audio)
pitch, pitchConfidence = pm(eqAudio)

# np.save reference values for later np.loading
#np.save('pitchmelodiapitch_eqloud.npy', pitch)
#np.save('pitchmelodiaconfidence_eqloud.npy', pitchConfidence)

np.loadedPitchMelodiaPitch = np.load(join(filedir(), 'pitchmelodia/pitchmelodiapitch_eqloud.npy'))
self.assertAlmostEqualVectorFixedPrecision(pitch, np.loadedPitchMelodiaPitch.tolist(), 8)

np.loadedPitchConfidence = np.load(join(filedir(), 'pitchmelodia/pitchmelodiaconfidence_eqloud.npy'))
self.assertAlmostEqualVectorFixedPrecision(pitchConfidence, np.loadedPitchConfidence.tolist(), 8)

def test110Hz(self):
signal = 0.5 * numpy.sin((array(range(10 * 4096))/44100.) * 110 * 2*math.pi)
pm = PitchMelodia()
pitch, confidence = pm(signal)
self.assertAlmostEqual(pitch[50], 110.0, 10)

def test110HzPeakThresholds(self):
signal = 0.5 * numpy.sin((array(range(10 * 4096))/44100.) * 110 * 2*math.pi)
pm_default = PitchMelodia()
pm_hw0 = PitchMelodia(peakFrameThreshold=0)
pm_hw1 = PitchMelodia(peakFrameThreshold=1)

pitch_default, confidence_default = pm_default(signal)
pitch_hw0, confidence_hw0 = pm_hw0(signal)
pitch_hw1, confidence_hw1 = pm_hw1(signal)

self.assertAlmostEqual(pitch_default[50], 110.0, 10)
self.assertAlmostEqual(pitch_hw0[50], 110.0, 10)
self.assertAlmostEqual(pitch_hw1[50], 110.0, 10)

def testDifferentPeaks(self):
signal_55Hz = 0.5 * numpy.sin((array(range(10 * 4096))/44100.) * 55 * 2*math.pi)
signal_85Hz = 0.5 * numpy.sin((array(range(10 * 4096))/44100.) * 85 * 2*math.pi)
signal = signal_55Hz + signal_85Hz
pm = PitchMelodia()
pitch, confidence = pm(signal)

for p in pitch[83:129]: # Adjusted the range to be more clear
self.assertGreater(p, 55)
self.assertLess(p, 85)

def testBelowReferenceFrequency1(self):
signal_50Hz = 1.5 * numpy.sin((array(range(10 * 4096))/44100.) * 50 * 2*math.pi)
pitch, confidence = PitchMelodia()(signal_50Hz)
self.assertAlmostEqual(pitch[10], 100.0, 2)

def testBelowReferenceFrequency2(self):
signal_30Hz = 1.5 * numpy.sin((array(range(10 * 4096))/44100.) * 30 * 2*math.pi)
pitch, confidence = PitchMelodia(referenceFrequency=40)(signal_30Hz)
self.assertAlmostEqual(pitch[10], 60.0, 2)

suite = allTests(TestPitchMelodia)

Expand Down
Loading