From 02c52dc68cdaa7fe42e1a47c1a3d98bbec4d475e Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Tue, 26 Mar 2024 11:19:59 -0400 Subject: [PATCH] audio: Change the Player to use the new StopWatch object Instead of using GStreamer's clock. This lets us call "reset()" when a new track is loaded so we can reset the timer when a track is loaded during gapless playback. Signed-off-by: Anna Schumaker --- emmental/audio/__init__.py | 20 +++++++-------- tests/audio/test_audio.py | 50 ++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/emmental/audio/__init__.py b/emmental/audio/__init__.py index 2107d1b..8fcb734 100644 --- a/emmental/audio/__init__.py +++ b/emmental/audio/__init__.py @@ -5,6 +5,7 @@ from gi.repository import GObject from gi.repository import GLib from gi.repository import Gst from . import filter +from . import stopwatch from .. import path from .. import tmpdir @@ -33,7 +34,7 @@ class Player(GObject.GObject): have_track = GObject.Property(type=bool, default=False) almost_done = GObject.Property(type=bool, default=False) playtime = GObject.Property(type=float) - savedtime = GObject.Property(type=float) + stopwatch = GObject.Property(type=stopwatch.StopWatch) bg_enabled = GObject.Property(type=bool, default=False) bg_volume = GObject.Property(type=float, default=0.5) @@ -41,7 +42,7 @@ class Player(GObject.GObject): def __init__(self): """Initialize the audio Player.""" - super().__init__() + super().__init__(stopwatch=stopwatch.StopWatch()) self._filter = filter.Filter() self._timeout = None @@ -70,12 +71,6 @@ class Player(GObject.GObject): if not self.almost_done: self.emit("about-to-finish") - def __get_current_playtime(self) -> float: - if not self._playbin.clock: - return 0.0 - time = self._playbin.clock.get_time() - self._playbin.base_time - return time / Gst.SECOND - def __msg_async_done(self, bus: Gst.Bus, message: Gst.Message) -> None: self.__update_position() @@ -95,15 +90,18 @@ class Player(GObject.GObject): print("audio: state changed to 'playing'") self.status = "Playing" self.playing = True + self.stopwatch.start() case (_, Gst.State.PAUSED, Gst.State.VOID_PENDING): print("audio: state changed to 'paused'") self.status = "Paused" self.playing = False + self.stopwatch.stop() case (_, Gst.State.READY, Gst.State.VOID_PENDING) | \ (_, Gst.State.NULL, Gst.State.VOID_PENDING): print("audio: state changed to 'stopped'") self.status = "Stopped" self.playing = False + self.stopwatch.stop() self.__update_timeout() @@ -142,9 +140,10 @@ class Player(GObject.GObject): for tag in ["artist", "album-artist", "album", "title"]: self.set_property(tag, "") for tag in ["album-disc-number", "track-number", - "position", "playtime", "savedtime"]: + "position", "playtime"]: self.set_property(tag, 0) + self.stopwatch.reset() self.almost_done = False self.pause_on_load = False self.artwork = artwork @@ -153,7 +152,7 @@ class Player(GObject.GObject): def __update_position(self) -> bool: (res, pos) = self._playbin.query_position(Gst.Format.TIME) self.position = pos / Gst.USECOND if res else 0 - self.playtime = self.__get_current_playtime() + self.savedtime + self.playtime = self.stopwatch.elapsed_time() self.__check_last_second() return GLib.SOURCE_CONTINUE @@ -189,7 +188,6 @@ class Player(GObject.GObject): def seek(self, newpos: float, *args) -> None: """Seek to a different point in the stream.""" - self.savedtime += self.__get_current_playtime() self._playbin.seek_simple(Gst.Format.TIME, SEEK_FLAGS, newpos * Gst.USECOND) diff --git a/tests/audio/test_audio.py b/tests/audio/test_audio.py index 30e154c..3fd0a5a 100644 --- a/tests/audio/test_audio.py +++ b/tests/audio/test_audio.py @@ -81,9 +81,9 @@ class TestAudio(unittest.TestCase): self.player.duration = 10 self.player.position = 8 self.player.playtime = 6 - self.player.savedtime = 4 self.player.almost_done = True self.player.artwork = pathlib.Path("/a/b/c.jpg") + self.player.stopwatch.reset = unittest.mock.Mock() eos = Gst.Message.new_eos(self.player._playbin) self.player._playbin.get_bus().post(eos) @@ -94,9 +94,10 @@ class TestAudio(unittest.TestCase): for prop in ["artist", "album-artist", "album", "title"]: self.assertEqual(self.player.get_property(prop), "") for prop in ["album-disc-number", "track-number", - "position", "duration", "playtime", "savedtime"]: + "position", "duration", "playtime"]: self.assertEqual(self.player.get_property(prop), 0) self.assertIsNone(self.player.artwork) + self.player.stopwatch.reset.assert_called() self.assertEqual(self.player.get_state(), Gst.State.READY) self.assertEqual(self.player.status, "Stopped") @@ -151,21 +152,24 @@ class TestAudio(unittest.TestCase): "audio: state changed to 'paused'\n") self.player.playtime = 6 - self.player.savedtime = 4 + self.player.stopwatch.reset = unittest.mock.Mock() self.player.emit("file-loaded", tests.util.TRACK_OGG) for prop in ["artist", "album-artist", "album", "title"]: self.assertEqual(self.player.get_property(prop), "") - for prop in ["album-disc-number", "track-number", - "playtime", "savedtime"]: + for prop in ["album-disc-number", "track-number", "playtime"]: self.assertEqual(self.player.get_property(prop), 0) + self.player.stopwatch.reset.assert_called() @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_play(self, mock_stdout: io.StringIO): """Test that the play() function works as expected.""" + self.player.stopwatch.start = unittest.mock.Mock() + self.player.play() self.main_loop() self.assertFalse(self.player.playing) self.assertEqual(self.player.status, "Stopped") + self.player.stopwatch.start.assert_not_called() self.player.file = tests.util.TRACK_OGG self.player.play() @@ -175,13 +179,17 @@ class TestAudio(unittest.TestCase): self.assertEqual(self.player.get_state(), Gst.State.PLAYING) self.assertRegex(mock_stdout.getvalue(), "audio: state changed to 'playing'$") + self.player.stopwatch.start.assert_called() @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_pause(self, mock_stdout: io.StringIO): """Test that the pause() function works as expected.""" + self.player.stopwatch.stop = unittest.mock.Mock() + self.player.pause() self.main_loop() self.assertEqual(self.player.status, "Stopped") + self.player.stopwatch.stop.assert_not_called() self.player.playing = True self.player.file = tests.util.TRACK_OGG @@ -192,6 +200,7 @@ class TestAudio(unittest.TestCase): self.assertEqual(self.player.get_state(), Gst.State.PAUSED) self.assertRegex(mock_stdout.getvalue(), "audio: state changed to 'paused'$") + self.player.stopwatch.stop.assert_called() @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_play_pause(self, mock_stdout: io.StringIO): @@ -222,6 +231,7 @@ class TestAudio(unittest.TestCase): @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_stop(self, mock_stdout: io.StringIO): """Test that the stop() function works as expected.""" + self.player.stopwatch.stop = unittest.mock.Mock() self.player.file = tests.util.TRACK_OGG self.player.play() self.main_loop() @@ -234,6 +244,7 @@ class TestAudio(unittest.TestCase): self.assertRegex(mock_stdout.getvalue(), "audio: state changed to 'playing'\n" "audio: state changed to 'stopped'\n$") + self.player.stopwatch.stop.assert_called() @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_tags(self, mock_stdout: io.StringIO): @@ -282,30 +293,15 @@ class TestAudio(unittest.TestCase): @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) def test_playtime(self, mock_stdout: io.StringIO): """Test the play time property.""" + self.assertIsInstance(self.player.stopwatch, + emmental.audio.stopwatch.StopWatch) self.assertEqual(self.player.playtime, 0.0) - self.assertEqual(self.player.savedtime, 0.0) - self.assertEqual(self.player._Player__get_current_playtime(), 0.0) - self.player.file = tests.util.TRACK_OGG - self.player.play() - self.main_loop() - self.assertIsNotNone(self.player._playbin.clock) - - base_time = unittest.mock.PropertyMock(return_value=Gst.SECOND) - type(self.player._playbin).base_time = base_time - get_time = unittest.mock.Mock(return_value=3 * Gst.SECOND) - self.player._playbin.clock.get_time = get_time - - self.assertEqual(self.player._Player__get_current_playtime(), 2.0) - self.player._Player__update_position() - self.assertEqual(self.player.playtime, 2) - - with unittest.mock.patch.object(self.player._playbin, "seek_simple"): - self.player.seek(5) - self.assertEqual(self.player.savedtime, 2) - - self.player._Player__update_position() - self.assertEqual(self.player.playtime, 4) + with unittest.mock.patch.object(self.player.stopwatch, + "elapsed_time") as mock_elapsed: + mock_elapsed.return_value = 2.0 + self.player._Player__update_position() + self.assertEqual(self.player.playtime, 2.0) def test_volume(self): """Test that the volume property works as expected."""