"""Tests for rendering and performing. This is where the audio clock and the dance clock have to agree, and where stopping has to actually stop. """ from __future__ import annotations import threading import numpy as np import pytest from aya.band import render_band from aya.score import Score from tests.conftest import FakeRobot, FakeVoice from aya.performance import AnyEvent, Performance, render_song def test_a_rendered_song_carries_its_score_and_stays_inside_range(voice: FakeVoice, song_score: Score) -> None: song = render_song(song_score, voice) assert song.score is song_score assert song.duration_s > song_score.duration_seconds assert float(np.max(np.abs(song.audio))) <= 1.0 def test_the_band_can_be_left_out(voice: FakeVoice, song_score: Score) -> None: with_band = render_song(song_score, voice, with_band=True) without = render_song(song_score, voice, with_band=False) # Both mixes are normalised to the same peak, so comparing loudness proves nothing, # and comparing frequency bands proves nothing either when the voice is a low one. # Correlating each mix against the backing track asks the question directly: is the # band in this recording? band = render_band(song_score, with_band.sample_rate) lead_in = int(with_band.lead_in_s * with_band.sample_rate) length = min(band.size, with_band.audio.size - lead_in, without.audio.size - lead_in) present = float(np.dot(with_band.audio[lead_in : lead_in + length], band[:length])) absent = float(np.dot(without.audio[lead_in : lead_in + length], band[:length])) assert present > absent * 2 def test_rendering_reports_the_stages_it_goes_through(voice: FakeVoice, song_score: Score) -> None: stages: list[str] = [] render_song(song_score, voice, on_progress=lambda stage, fraction: stages.append(stage)) assert "singing" in stages assert "band" in stages assert stages[-1] == "done" def test_playing_a_song_pushes_all_of_its_audio( voice: FakeVoice, song_score: Score, robot: FakeRobot, running: threading.Event ) -> None: song = render_song(song_score, voice, with_band=False) finished = Performance(song).play(robot, running) assert finished is True expected = song.duration_s * robot.media.get_output_audio_samplerate() assert robot.media.total_played >= expected * 0.98 assert robot.media.playing is False def test_playing_a_song_drives_the_robot( voice: FakeVoice, song_score: Score, robot: FakeRobot, running: threading.Event ) -> None: song = render_song(song_score, voice, with_band=False) Performance(song).play(robot, running) assert len(robot.targets) > 10 assert all("head" in target for target in robot.targets) assert all(target["head"].shape == (4, 4) for target in robot.targets) def test_a_stopped_song_stops_and_drops_what_was_queued(voice: FakeVoice, song_score: Score, robot: FakeRobot) -> None: song = render_song(song_score, voice, with_band=False) stopped = threading.Event() stopped.set() finished = Performance(song).play(robot, stopped) assert finished is False assert robot.media.cleared == 1 assert robot.media.playing is False def test_the_dance_follows_musical_time_not_wall_clock(voice: FakeVoice, song_score: Score) -> None: song = render_song(song_score, voice) performance = Performance(song, lead_in_s=0.5) # During the lead-in the dance has not started; a beat later it is at beat one. quiet = performance.pose_at(0.0) first_beat = performance.pose_at(0.5 + song_score.seconds_per_beat) assert quiet is not None and first_beat is not None assert performance.pose_at(0.5).body_yaw_deg == pytest.approx(performance.pose_at(0.5).body_yaw_deg) def test_speaking_an_announcement_plays_audio_and_moves( voice: FakeVoice, song_score: Score, robot: FakeRobot, running: threading.Event ) -> None: song = render_song(song_score, voice, with_band=False) Performance(song).speak(robot, voice, "Here is a test.", running) assert robot.media.total_played > 0 assert robot.targets def test_speaking_nothing_plays_nothing( voice: FakeVoice, song_score: Score, robot: FakeRobot, running: threading.Event ) -> None: song = render_song(song_score, voice, with_band=False) Performance(song).speak(robot, voice, " ", running) assert robot.media.total_played == 0 def test_any_event_is_set_when_any_of_its_events_is() -> None: first, second = threading.Event(), threading.Event() combined = AnyEvent(first, second) assert combined.is_set() is False second.set() assert combined.is_set() is True