Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions data/io.github.revisto.drum-machine.metainfo.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<li>Volume control for overall mix</li>
<li>Save and load preset patterns</li>
<li>Multiple drum sounds including kick, snare, hi-hat, and more</li>
<li>Audio export feature with support for WAV, FLAC, OGG, and MP3 formats</li>
<li>Metadata support for embedding artist name, song title, and cover art</li>
<li>Keyboard shortcuts for quick access to all functions</li>
</ul>
</description>
Expand Down Expand Up @@ -61,6 +63,22 @@
<email>[email protected]</email>
</developer>
<releases>
<release version="1.5.0" date="2025-09-05">
<description>
<p>Drum Machine 1.5.0 introduces major audio export capabilities! 🎵</p>
<ul>
<li>Audio Export Feature: Export your drum patterns to WAV, FLAC, OGG Vorbis, and MP3 formats</li>
<li>Metadata Support: Embed artist name, song title, and cover art in exported files</li>
<li>Repeat Pattern: Set how many times your beat repeats in the exported audio</li>
<li>Real-time Progress: Track export progress with cancel support</li>
<li>Background Processing: Export runs in background, keeping UI responsive</li>
<li>Toast Notifications: Get notified when export completes with "Open File" action</li>
<li>Bug Fix: Fixed critical issue where multiple drum sounds weren't playing simultaneously</li>
<li>Enhanced UI: Improved layout and user experience throughout</li>
<li>Translation Updates: Updated Spanish, Russian, Georgian, Chinese, Hungarian, Swedish, and more</li>
</ul>
</description>
</release>
<release version="1.4.0" date="2025-08-01">
<description>
<p>Drum Machine 1.4.0 is here with some good updates! 🥁</p>
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -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', ],
)
Expand Down
15 changes: 1 addition & 14 deletions python-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pygame
mido
setuptools
setuptools-scm
Expand Down
84 changes: 72 additions & 12 deletions src/services/sound_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down