Files
2026-08-23 14:34:13 -07:00

577 lines
26 KiB
Python

import importlib.util
import json
import pathlib
import sys
import tempfile
import types
import unittest
from unittest import mock
import torch
AUDIO_NODE_PATH = pathlib.Path(__file__).parents[1] / "nodes" / "audio"
PACKAGE_NAME = "fl_audio_timeline_tests"
package = types.ModuleType(PACKAGE_NAME)
package.__path__ = [str(AUDIO_NODE_PATH)]
sys.modules[PACKAGE_NAME] = package
MODULE_PATH = AUDIO_NODE_PATH / "audio_timeline.py"
SPEC = importlib.util.spec_from_file_location(f"{PACKAGE_NAME}.audio_timeline", MODULE_PATH)
timeline = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = timeline
SPEC.loader.exec_module(timeline)
def audio_range_loader(audio):
def load(filename, start_sample, sample_count):
return pathlib.Path(filename), {
"waveform": audio["waveform"][..., start_sample:start_sample + sample_count].clone(),
"sample_rate": audio["sample_rate"],
}
return load
class AudioTimelineTests(unittest.TestCase):
def test_beat_this_results_drive_bpm_grid_downbeats_and_confidence(self):
audio = {
"waveform": torch.zeros(1, 1, 200),
"sample_rate": 100,
}
detected = {
"beat_times": [0.1, 0.6, 1.1, 1.6],
"downbeat_times": [0.1, 1.1],
"beat_confidences": [0.9, 0.8, 0.7, 0.6],
"downbeat_confidences": [0.95, 0.75],
"detector": {"name": "Beat This", "checkpoint_sha256": "abc"},
}
with (
mock.patch.object(timeline, "analyze_beat_this", return_value=detected),
mock.patch.object(timeline.librosa.onset, "onset_strength", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa.onset, "onset_detect", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa, "frames_to_time", return_value=timeline.np.array([])),
mock.patch.object(timeline, "_detect_drums", return_value={}),
mock.patch.object(timeline, "waveform_preview", return_value={}),
):
analysis = timeline.analyze_audio(audio)
self.assertAlmostEqual(analysis["bpm"], 120.0)
self.assertEqual(analysis["bpm_source"], "beat_this_intervals_median")
self.assertEqual(analysis["beat_times"], detected["beat_times"])
self.assertEqual(analysis["downbeat_times"], detected["downbeat_times"])
self.assertEqual(analysis["detected_beat_confidences"], detected["beat_confidences"])
self.assertEqual(analysis["detector"], detected["detector"])
def test_auxiliary_analysis_skips_beat_this(self):
audio = {
"waveform": torch.zeros(1, 1, 200),
"sample_rate": 100,
}
with (
mock.patch.object(timeline, "analyze_beat_this") as detector,
mock.patch.object(timeline.librosa.onset, "onset_strength", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa.onset, "onset_detect", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa, "frames_to_time", return_value=timeline.np.array([])),
mock.patch.object(timeline, "_detect_drums", return_value={}),
mock.patch.object(timeline, "waveform_preview", return_value={}),
):
analysis = timeline.analyze_audio(audio, detect_beats=False)
detector.assert_not_called()
self.assertNotIn("beat_times", analysis)
self.assertIn("onset_times", analysis)
def test_beat_this_can_use_the_master_mix_with_stem_reference_analysis(self):
stem = {"waveform": torch.zeros(1, 1, 200), "sample_rate": 100}
mix = {"waveform": torch.ones(1, 1, 400), "sample_rate": 200}
detected = {
"beat_times": [0.1, 0.6, 1.1],
"downbeat_times": [0.1],
"beat_confidences": [0.9, 0.9, 0.9],
"downbeat_confidences": [0.95],
"detector": {"name": "beat_this"},
}
with (
mock.patch.object(timeline, "analyze_beat_this", return_value=detected) as beat_this,
mock.patch.object(timeline.librosa.onset, "onset_strength", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa.onset, "onset_detect", return_value=timeline.np.array([])),
mock.patch.object(timeline.librosa, "frames_to_time", return_value=timeline.np.array([])),
mock.patch.object(timeline, "_detect_drums", return_value={}),
mock.patch.object(timeline, "waveform_preview", return_value={}),
):
timeline.analyze_audio(stem, beat_audio=mix)
self.assertEqual(beat_this.call_args.args[1], 200)
timeline.np.testing.assert_array_equal(beat_this.call_args.args[0], timeline.np.ones(400))
def test_cached_analysis_restores_an_existing_audio_file(self):
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
cache_path.write_text(
'{"audio_file":"song.wav"}',
encoding="utf-8",
)
with (
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(
timeline,
"resolve_audio_path",
return_value=pathlib.Path("song.wav"),
) as resolve,
):
filename = timeline.cached_analysis_audio_file("a" * 64)
self.assertEqual(filename, "song.wav")
resolve.assert_called_once_with("song.wav")
def test_cached_analysis_rejects_invalid_keys_and_missing_audio(self):
self.assertEqual(timeline.cached_analysis_audio_file("../analysis"), "")
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
cache_path.write_text("{}", encoding="utf-8")
with mock.patch.object(timeline, "_cache_path", return_value=cache_path):
self.assertEqual(timeline.cached_analysis_audio_file("a" * 64), "")
def test_beat_offset_shifts_only_the_regular_grid(self):
analysis = {
"audio_duration": 1.0,
"beat_times": [0.1, 0.9],
"detected_beat_times": [0.0, 0.95],
"onset_times": [0.2],
"drum_times": {"kick_times": [0.2]},
}
shifted = timeline.apply_beat_offset(analysis, fps=24.0, beat_offset_ms=200)
self.assertEqual(shifted["base_beat_times"], [0.1, 0.9])
self.assertEqual(shifted["beat_times"], [0.30000000000000004])
self.assertEqual(shifted["detected_beat_times"], [0.0, 0.95])
self.assertEqual(shifted["beat_frames"], [7])
self.assertEqual(shifted["detected_beat_frames"], [0, 23])
self.assertEqual(shifted["onset_times"], [0.2])
self.assertEqual(shifted["drum_times"], {"kick_times": [0.2]})
self.assertEqual(shifted["beat_offset_ms"], 200)
self.assertEqual(shifted["grid_interval_seconds"], 0.8)
def test_beat_offset_preserves_periodic_spacing_at_crop_boundaries(self):
analysis = {
"audio_duration": 1.0,
"beat_times": [0.0, 0.4, 0.8],
"detected_beat_times": [],
}
shifted = timeline.apply_beat_offset(analysis, fps=24.0, beat_offset_ms=-200)
self.assertEqual(shifted["beat_times"], [0.2, 0.6000000000000001])
self.assertEqual(shifted["base_beat_times"], [0.0, 0.4, 0.8])
self.assertAlmostEqual(
shifted["beat_times"][1] - shifted["beat_times"][0],
0.4,
)
def test_beat_offset_keeps_the_grid_visible_after_all_native_beats_leave(self):
analysis = {
"audio_duration": 0.5,
"bpm": 120.0,
"beat_times": [0.1],
"detected_beat_times": [0.1],
}
shifted = timeline.apply_beat_offset(analysis, fps=24.0, beat_offset_ms=1000)
self.assertAlmostEqual(shifted["beat_times"][0], 0.1)
self.assertEqual(shifted["detected_beat_times"], [0.1])
def test_beat_grid_density_uses_native_beats_as_its_source(self):
analysis = {
"bpm": 120.0,
"audio_duration": 2.0,
"beat_times": [0.0, 0.5, 1.0, 1.5],
"downbeat_times": [0.0, 1.0],
"detected_beat_times": [0.05, 0.55],
"detected_downbeat_times": [0.0, 1.0],
}
every_two = timeline.apply_beat_offset(
analysis,
fps=24.0,
beat_grid_density="every_2_beats",
)
every_beat = timeline.apply_beat_offset(
every_two,
fps=24.0,
beat_grid_density="every_beat",
)
subdivisions = timeline.apply_beat_offset(
analysis,
fps=24.0,
beat_offset_ms=100,
beat_grid_density="half_beat",
)
self.assertEqual(every_two["beat_times"], [0.0, 1.0])
self.assertEqual(every_two["downbeat_times"], [0.0, 1.0])
self.assertEqual(every_two["grid_bpm"], 60.0)
self.assertEqual(every_beat["beat_times"], [0.0, 0.5, 1.0, 1.5])
self.assertEqual(every_beat["downbeat_times"], [0.0, 1.0])
self.assertEqual(every_beat["grid_bpm"], 120.0)
self.assertEqual(
subdivisions["beat_times"],
[0.1, 0.35, 0.6, 0.85, 1.1, 1.35, 1.6],
)
self.assertEqual(subdivisions["detected_beat_times"], [0.05, 0.55])
self.assertEqual(subdivisions["downbeat_times"], [0.1, 1.1])
self.assertEqual(subdivisions["detected_downbeat_times"], [0.0, 1.0])
self.assertEqual(subdivisions["grid_bpm"], 240.0)
self.assertEqual(subdivisions["grid_interval_seconds"], 0.25)
self.assertEqual(subdivisions["beat_grid_density"], "half_beat")
def test_unknown_beat_grid_density_is_rejected(self):
with self.assertRaisesRegex(ValueError, "Unknown beat grid density"):
timeline.apply_beat_offset(
{"audio_duration": 1.0, "beat_times": [0.0]},
fps=24.0,
beat_grid_density="bars",
)
def test_crop_uses_video_frames_for_sample_boundaries(self):
audio = {
"waveform": torch.arange(0, 96000, dtype=torch.float32).reshape(1, 1, -1),
"sample_rate": 48000,
}
cropped, metadata = timeline.crop_audio(
audio,
fps=24.0,
trim_start_frame=12,
length_frames=24,
)
self.assertEqual(cropped["waveform"].shape, (1, 1, 48000))
self.assertEqual(cropped["waveform"][0, 0, 0], 24000)
self.assertEqual(metadata["source_start"], 0.5)
self.assertEqual(metadata["audio_duration"], 1.0)
self.assertEqual(metadata["length_frames"], 24)
def test_crop_rejects_ranges_past_source_end(self):
audio = {
"waveform": torch.zeros(1, 1, 48000),
"sample_rate": 48000,
}
with self.assertRaisesRegex(ValueError, "exceeds"):
timeline.crop_audio(
audio,
fps=24.0,
trim_start_frame=12,
length_frames=24,
)
def test_waveform_preview_keeps_minimum_and_maximum_pairs(self):
preview = timeline.waveform_preview(
torch.tensor([-1.0, 0.5, -0.25, 1.0]).numpy(),
sample_rate=4,
)
self.assertEqual(preview["version"], 1)
self.assertEqual(preview["duration"], 1.0)
self.assertEqual(len(preview["peaks"]) % 2, 0)
self.assertEqual(min(preview["peaks"]), -32767)
self.assertEqual(max(preview["peaks"]), 32767)
def test_stem_analysis_still_returns_the_master_audio_crop(self):
master = {"waveform": torch.ones(1, 1, 48000), "sample_rate": 48000}
stem = {"waveform": torch.full((1, 1, 48000), 2.0), "sample_rate": 48000}
analysis = {
"bpm": 120.0,
"beat_times": [0.0, 0.5],
"audio_duration": 1.0,
"waveform_preview": {"version": 1, "duration": 1.0, "scale": 32767, "peaks": [0, 1]},
"drum_times": {},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)),
mock.patch.object(timeline, "load_audio_file_range", side_effect=audio_range_loader(master)),
mock.patch.object(timeline, "load_cached_stem", return_value=stem) as load_stem,
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(timeline, "analyze_audio", return_value=analysis) as analyze,
):
_, cropped = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
length_frames=24,
analysis_source="drums",
)
_, second_crop = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
trim_start_frame=12,
length_frames=12,
analysis_source="drums",
)
self.assertEqual(cropped["waveform"].mean(), 1.0)
self.assertEqual(second_crop["waveform"].mean(), 1.0)
self.assertEqual(analyze.call_args.args[0]["waveform"].mean(), 2.0)
self.assertEqual(analyze.call_args.kwargs["beat_audio"]["waveform"].mean(), 1.0)
self.assertEqual(analyze.call_count, 1)
self.assertEqual(load_stem.call_count, 1)
def test_cached_analysis_without_sample_rate_keeps_full_decode_fallback(self):
master = {"waveform": torch.ones(1, 1, 48000), "sample_rate": 48000}
source_analysis = {
"audio_file": "song.wav",
"source_duration": 1.0,
"audio_duration": 1.0,
"beat_times": [],
"downbeat_times": [],
"detected_beat_times": [],
"onset_times": [],
"drum_times": {},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
cache_path.write_text(json.dumps(source_analysis), encoding="utf-8")
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)) as load_full,
mock.patch.object(timeline, "load_audio_file_range") as load_range,
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
):
_, cropped = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
trim_start_frame=6,
length_frames=12,
detect_beats=False,
)
load_full.assert_called_once_with("song.wav")
load_range.assert_not_called()
self.assertEqual(cropped["waveform"].shape[-1], 24000)
def test_offset_changes_reuse_the_base_analysis_cache(self):
master = {"waveform": torch.ones(1, 1, 48000), "sample_rate": 48000}
analysis = {
"bpm": 120.0,
"beat_times": [0.1, 0.6],
"detected_beat_times": [0.1, 0.6],
"onset_times": [],
"audio_duration": 1.0,
"waveform_preview": {"version": 1, "duration": 1.0, "scale": 32767, "peaks": [0, 1]},
"drum_times": {},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)),
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(timeline, "analyze_audio", return_value=analysis) as analyze,
):
first, _ = timeline.analyze_audio_file("song.wav", fps=24.0)
shifted, _ = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
beat_offset_ms=100,
)
self.assertEqual(analyze.call_count, 1)
self.assertEqual(first["beat_times"], [0.1, 0.6])
self.assertEqual(shifted["beat_times"], [0.2, 0.7])
self.assertEqual(first["cache_key"], shifted["cache_key"])
self.assertFalse(first["analysis_cache_hit"])
self.assertTrue(shifted["analysis_cache_hit"])
def test_density_changes_reuse_the_base_analysis_cache(self):
master = {"waveform": torch.ones(1, 1, 48000), "sample_rate": 48000}
analysis = {
"bpm": 120.0,
"beat_times": [0.0, 0.25, 0.5, 0.75],
"detected_beat_times": [],
"onset_times": [],
"audio_duration": 1.0,
"waveform_preview": {"version": 1, "duration": 1.0, "scale": 32767, "peaks": [0, 1]},
"drum_times": {},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)),
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(timeline, "analyze_audio", return_value=analysis) as analyze,
):
every_beat, _ = timeline.analyze_audio_file("song.wav", fps=24.0)
every_two, _ = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
beat_grid_density="every_2_beats",
)
self.assertEqual(analyze.call_count, 1)
self.assertEqual(every_beat["beat_times"], [0.0, 0.25, 0.5, 0.75])
self.assertEqual(every_two["beat_times"], [0.0, 0.5])
self.assertEqual(every_beat["cache_key"], every_two["cache_key"])
def test_different_crops_reuse_full_source_analysis_and_rebase_events(self):
master = {"waveform": torch.ones(1, 1, 96000), "sample_rate": 24000}
source_analysis = {
"bpm": 60.0,
"base_grid_interval_seconds": 1.0,
"beat_times": [0.25, 1.25, 2.25, 3.25],
"downbeat_times": [0.25, 2.25],
"detected_beat_times": [0.25, 1.25, 2.25, 3.25],
"detected_downbeat_times": [0.25, 2.25],
"detected_beat_confidences": [0.9, 0.8, 0.7, 0.6],
"detected_downbeat_confidences": [0.95, 0.75],
"onset_times": [0.5, 1.5, 2.5, 3.5],
"audio_duration": 4.0,
"waveform_preview": {
"version": 1,
"duration": 4.0,
"scale": 32767,
"peaks": list(range(16)),
},
"drum_times": {
"kick_times": [1.25, 3.25],
"snare_times": [2.25],
"hihat_times": [],
},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)),
mock.patch.object(timeline, "load_audio_file_range", side_effect=audio_range_loader(master)),
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(timeline, "analyze_audio", return_value=source_analysis) as analyze,
):
first, first_audio = timeline.analyze_audio_file(
"song.wav", fps=24.0, trim_start_frame=24, length_frames=48
)
second, second_audio = timeline.analyze_audio_file(
"song.wav", fps=24.0, trim_start_frame=48, length_frames=48
)
self.assertEqual(analyze.call_count, 1)
self.assertEqual(analyze.call_args.args[0]["waveform"].shape[-1], 96000)
self.assertEqual(first_audio["waveform"].shape[-1], 48000)
self.assertEqual(second_audio["waveform"].shape[-1], 48000)
self.assertEqual(first["beat_times"], [0.25, 1.25])
self.assertEqual(second["beat_times"], [0.25, 1.25])
self.assertEqual(first["detected_beat_confidences"], [0.8, 0.7])
self.assertEqual(first["onset_times"], [0.5, 1.5])
self.assertEqual(first["drum_times"]["kick_times"], [0.25])
self.assertEqual(first["source_start"], 1.0)
self.assertEqual(second["source_start"], 2.0)
self.assertEqual(first["source_analysis"]["beat_times"], [0.25, 1.25, 2.25, 3.25])
self.assertFalse(first["analysis_cache_hit"])
self.assertTrue(second["analysis_cache_hit"])
def test_fps_half_time_and_density_project_from_one_source_cache(self):
master = {"waveform": torch.ones(1, 1, 96000), "sample_rate": 24000}
source_analysis = {
"bpm": 120.0,
"base_grid_interval_seconds": 0.5,
"beat_times": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
"downbeat_times": [0.0, 2.0],
"detected_beat_times": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
"detected_downbeat_times": [0.0, 2.0],
"detected_beat_confidences": [0.9] * 8,
"detected_downbeat_confidences": [0.95, 0.85],
"onset_times": [],
"audio_duration": 4.0,
"waveform_preview": None,
"drum_times": {},
}
with tempfile.TemporaryDirectory() as directory:
cache_path = pathlib.Path(directory) / "analysis.json"
with (
mock.patch.object(timeline, "resolve_audio_path", return_value=pathlib.Path("song.wav")),
mock.patch.object(timeline, "load_audio_file", return_value=(pathlib.Path("song.wav"), master)),
mock.patch.object(timeline, "load_audio_file_range", side_effect=audio_range_loader(master)),
mock.patch.object(timeline, "analysis_cache_key", return_value="key"),
mock.patch.object(timeline, "_cache_path", return_value=cache_path),
mock.patch.object(timeline, "analyze_audio", return_value=source_analysis) as analyze,
):
regular, _ = timeline.analyze_audio_file(
"song.wav", fps=24.0, trim_start_frame=12, length_frames=48
)
half_time, _ = timeline.analyze_audio_file(
"song.wav",
fps=48.0,
trim_start_frame=24,
length_frames=96,
half_time=True,
)
every_two, _ = timeline.analyze_audio_file(
"song.wav",
fps=24.0,
trim_start_frame=12,
length_frames=48,
beat_grid_density="every_2_beats",
)
self.assertEqual(analyze.call_count, 1)
self.assertEqual(regular["beat_times"], [0.0, 0.5, 1.0, 1.5])
self.assertEqual(half_time["beat_times"], [0.5, 1.5])
self.assertEqual(every_two["beat_times"], [0.5, 1.5])
self.assertEqual(regular["cache_key"], half_time["cache_key"])
self.assertEqual(regular["cache_key"], every_two["cache_key"])
def test_projection_uses_half_open_boundaries_and_keeps_confidence_pairs(self):
analysis = {
"beat_times": [1.0, 1.5, 2.0],
"downbeat_times": [1.0, 2.0],
"base_beat_times": [1.0, 1.5, 2.0],
"base_downbeat_times": [1.0, 2.0],
"detected_beat_times": [1.0, 1.5, 2.0],
"base_detected_beat_times": [1.0, 1.5, 2.0],
"detected_beat_confidences": [0.9, 0.8, 0.7],
"base_detected_beat_confidences": [0.9, 0.8, 0.7],
"detected_downbeat_times": [1.0, 2.0],
"base_detected_downbeat_times": [1.0, 2.0],
"detected_downbeat_confidences": [0.95, 0.75],
"base_detected_downbeat_confidences": [0.95, 0.75],
"onset_times": [1.0, 2.0],
"drum_times": {"kick_times": [1.0, 2.0]},
"waveform_preview": None,
}
crop = {
"source_duration": 3.0,
"source_start": 1.0,
"trim_start_frame": 24,
"length_frames": 24,
"audio_duration": 1.0,
"sample_rate": 48000,
}
projected = timeline.project_analysis(analysis, crop, fps=24.0)
self.assertEqual(projected["beat_times"], [0.0, 0.5])
self.assertEqual(projected["detected_beat_confidences"], [0.9, 0.8])
self.assertEqual(projected["downbeat_times"], [0.0])
self.assertEqual(projected["onset_times"], [0.0])
self.assertEqual(projected["drum_times"]["kick_times"], [0.0])
def test_beat_detection_mode_has_a_separate_versioned_cache_key(self):
with mock.patch.object(timeline, "audio_file_hash", return_value="audio"):
beats = timeline.analysis_cache_key(pathlib.Path("song.wav"), "mix", True)
auxiliary = timeline.analysis_cache_key(pathlib.Path("song.wav"), "mix", False)
self.assertNotEqual(beats, auxiliary)
if __name__ == "__main__":
unittest.main()