diff --git a/data/io.github.revisto.drum-machine.metainfo.xml.in b/data/io.github.revisto.drum-machine.metainfo.xml.in index aff1671..12b1ce7 100644 --- a/data/io.github.revisto.drum-machine.metainfo.xml.in +++ b/data/io.github.revisto.drum-machine.metainfo.xml.in @@ -19,6 +19,8 @@
Drum Machine 1.5.0 introduces major audio export capabilities! 🎵
+Drum Machine 1.4.0 is here with some good updates! 🥁
diff --git a/meson.build b/meson.build index d3262fd..f73e068 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('drum-machine', - version: '1.4.0', + version: '1.5.0', meson_version: '>= 0.62.0', default_options: [ 'warning_level=2', 'werror=false', ], ) diff --git a/python-dependencies.json b/python-dependencies.json index cd0e4c6..cf93484 100755 --- a/python-dependencies.json +++ b/python-dependencies.json @@ -3,20 +3,7 @@ "buildsystem": "simple", "build-commands": [], "modules": [ - { - "name": "python3-pygame", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pygame\"" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", - "sha256": "56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f" - } - ] - }, + { "name": "python3-mido", "buildsystem": "simple", diff --git a/requirements.txt b/requirements.txt index 0ada4ca..89d0d7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -pygame mido setuptools setuptools-scm diff --git a/src/services/sound_service.py b/src/services/sound_service.py index 9c30cd7..0b637f5 100644 --- a/src/services/sound_service.py +++ b/src/services/sound_service.py @@ -18,33 +18,93 @@ # SPDX-License-Identifier: GPL-3.0-or-later import os -import pygame +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, GLib + from ..interfaces.sound import ISoundService from ..config.constants import DRUM_PARTS class SoundService(ISoundService): def __init__(self, drumkit_dir): - pygame.init() - pygame.mixer.set_num_channels(32) self.drumkit_dir = drumkit_dir + # Map of drum part -> list[Gtk.MediaFile] self.sounds = {} + # Round-robin index for each drum part to allow overlapping playback + self._next_instance_index = {} + # Number of concurrent players per sound to allow overlaps + self._instances_per_sound = 4 + # Volume stored as 0.0 - 1.0 + self.volume = 1.0 def load_sounds(self): + """Preload media players for each drum part using GtkMediaStream.""" + loaded_sounds = {} + indices = {} + + for drum_part in DRUM_PARTS: + file_path = os.path.join(self.drumkit_dir, f"{drum_part}.wav") + # Create a small pool so the same sound can overlap itself + pool = [] + for _ in range(self._instances_per_sound): + media = Gtk.MediaFile.new_for_filename(file_path) + # Ensure current volume is applied + try: + media.set_volume(self.volume) + except Exception: + pass + pool.append(media) + loaded_sounds[drum_part] = pool + indices[drum_part] = 0 + + self.sounds = loaded_sounds + self._next_instance_index = indices + + def _play_sound_on_main(self, sound_name): + pool = self.sounds.get(sound_name) + if not pool: + return False - self.sounds = { - drum_part: pygame.mixer.Sound( - os.path.join(self.drumkit_dir, f"{drum_part}.wav") - ) - for drum_part in DRUM_PARTS - } + idx = self._next_instance_index.get(sound_name, 0) + media = pool[idx] + # Advance round-robin index + self._next_instance_index[sound_name] = (idx + 1) % len(pool) + + try: + # Restart from beginning if supported + if hasattr(media, "seek"): + media.seek(0) + except Exception: + pass + + try: + media.play() + except Exception: + pass + + # Returning False removes this idle source + return False def play_sound(self, sound_name): - self.sounds[sound_name].play() + # Schedule playback on the GTK main loop for thread-safety + GLib.idle_add(self._play_sound_on_main, sound_name) + + def _set_volume_on_main(self): + for pool in self.sounds.values(): + for media in pool: + try: + media.set_volume(self.volume) + except Exception: + pass + return False def set_volume(self, volume): - for sound in self.sounds.values(): - sound.set_volume(volume / 100) + # Convert 0-100 UI value to 0.0-1.0 + self.volume = max(0.0, min(1.0, float(volume) / 100.0)) + # Apply on main loop to avoid threading issues + GLib.idle_add(self._set_volume_on_main) def preview_sound(self, sound_name): if sound_name in self.sounds: