Files
product-vamp/tests/test_playback.py
T
khodowany c07586e06a
ci / checks (push) Failing after 3h14m51s
Fret Range & Cleanup
2026-09-19 22:29:54 -05:00

98 lines
5.5 KiB
Python

from fractions import Fraction
import json
from pathlib import Path
import tempfile
import unittest
import wave
from generator.model import fraction
from generator.playback import PlaybackUnavailable, compile_timeline, generate_tone, validate_assets, write_pcm
from tests.test_musicxml import notes, parse, xml_note, xml_score
class PlaybackTests(unittest.TestCase):
def test_chords_and_trailing_rest(self):
document = parse(xml_score(xml_note(duration=2, kind='half') + xml_note('E', 1, 'quarter', prefix='<chord/>') + '<note><rest/><duration>2</duration><type>half</type></note>'))
result = compile_timeline(document)
self.assertEqual(fraction(result['duration']), 4)
self.assertEqual([fraction(e['duration']) for e in result['events']], [2, 1])
def test_tie_is_one_attack(self):
result = compile_timeline(parse(xml_score(xml_note(duration=2, kind='half', extra='<tie type="start"/>') + xml_note(duration=2, kind='half', extra='<tie type="stop"/>'))))
self.assertEqual(len(result['events']), 1)
self.assertEqual(fraction(result['events'][0]['duration']), 4)
def test_octave_and_microtones(self):
result = compile_timeline(parse(xml_score(xml_note(alter='0.5'), '<transpose><diatonic>0</diatonic><chromatic>0</chromatic><octave-change>-1</octave-change></transpose>')))
self.assertEqual(fraction(result['events'][0]['pitch']), Fraction(97, 2))
def test_grace_default_and_cues(self):
grace = '<note><grace/><pitch><step>D</step><octave>4</octave></pitch><type>eighth</type></note>'
result = compile_timeline(parse(xml_score(grace + xml_note())))
self.assertEqual([fraction(e['duration']) for e in result['events']], [1, 3])
self.assertEqual([fraction(e['onset']) for e in result['events']], [0, 1])
self.assertEqual(compile_timeline(parse(xml_score(xml_note(prefix='<cue/>'))))['events'], [])
def test_tremolo(self):
result = compile_timeline(parse(xml_score(xml_note(extra='<notations><ornaments><tremolo>3</tremolo></ornaments></notations>'))))
self.assertEqual(len(result['events']), 32)
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification><notations><ornaments><tremolo>1</tremolo></ornaments></notations>'
result = compile_timeline(parse(xml_score(xml_note(duration=1, kind='eighth', extra=extra), divisions=3, implicit=True)))
self.assertEqual([fraction(e['duration']) for e in result['events']], [Fraction(1, 6)] * 2)
def test_hidden_pitches_sound_and_unpitched_is_unavailable(self):
hidden = xml_note().replace('<note>', '<note print-object="no">')
self.assertEqual(len(compile_timeline(parse(xml_score(hidden)))['events']), 1)
unpitched = '<note><unpitched><display-step>C</display-step><display-octave>4</display-octave></unpitched><duration>4</duration><type>whole</type></note>'
with self.assertRaises(PlaybackUnavailable):
compile_timeline(parse(xml_score(unpitched)))
def test_guitar_octave_depends_on_score_metadata_not_path(self):
from generator.playback import compile_tree
from generator.score import write_json
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = parse(xml_score(xml_note()))
for part in ('practice', 'acoustic', 'electric', 'vocal'):
source['staves'][0]['arrangement_part_type'] = part
write_json(root / f'{part}.json', source)
compile_tree(root)
for part, pitch in [('practice', 48), ('acoustic', 48), ('electric', 48), ('vocal', 60)]:
playback = json.loads((root / f'{part}.json').read_text())['playback']
self.assertEqual(playback['samples'][0]['id'], generate_tone(root, Fraction(pitch))['id'])
def test_audio_assets_validate_and_reject_corruption(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
generate_fixture(root)
manifest = json.loads((root / 'score.json').read_text())['playback']
validate_assets(manifest, root)
sample = manifest['samples'][0]
path = root / sample['path']
path.write_bytes(path.read_bytes()[:-1])
with self.assertRaises(ValueError):
validate_assets(manifest, root)
generate_fixture(root)
manifest['release']['sha256'] = write_pcm(root / 'audio/release.wav', [0.5] * 2400)
with self.assertRaisesRegex(ValueError, 'envelope'):
validate_assets(manifest, root)
sample['path'] = '../outside.wav'
with self.assertRaises(ValueError):
validate_assets(manifest, root)
def test_generated_tone_is_deterministic_and_loopable(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
a = generate_tone(root, Fraction(60))
b = generate_tone(root, Fraction(60))
self.assertEqual(a, b)
with wave.open(str(root / a['path'])) as audio:
self.assertEqual(audio.getframerate(), 48000)
self.assertEqual(audio.getnframes(), a['loop_end'])
self.assertEqual(audio.getnchannels(), 1)
self.assertGreater(a['loop_end'], a['loop_start'])
def generate_fixture(directory):
from generator.score import write_json
from generator.playback import compile_tree
root = Path(directory)
write_json(root / 'score.json', parse(xml_score(xml_note(duration=2, kind='half') + xml_note('E', 2, 'half', prefix='<chord/>') + '<note><rest/><duration>2</duration><type>half</type></note>')))
compile_tree(root)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--fixture', required=True)
generate_fixture(parser.parse_args().fixture)