├── po ├── meson.build ├── LINGUAS ├── POTFILES.in ├── en_GB.po ├── it.po ├── de.po ├── he.po ├── sv.po └── hu.po ├── data ├── drumkit │ ├── tom.wav │ ├── clap.wav │ ├── crash.wav │ ├── hihat.wav │ ├── kick.wav │ ├── snare.wav │ ├── hihat-2.wav │ ├── kick-2.wav │ ├── kick-3.wav │ ├── snare-2.wav │ └── meson.build ├── patterns │ ├── Slow.mid │ ├── Chill.mid │ ├── Night.mid │ ├── Shoot.mid │ ├── Boom Boom.mid │ ├── Maybe Rock.mid │ └── meson.build ├── screenshots │ ├── drum-machine-dark.png │ └── drum-machine-light.png ├── io.github.revisto.drum-machine.gschema.xml ├── presets │ └── meson.build ├── io.github.revisto.drum-machine.desktop.in ├── icons │ ├── meson.build │ └── hicolor │ │ ├── scalable │ │ ├── actions │ │ │ └── share-symbolic.svg │ │ └── apps │ │ │ └── io.github.revisto.drum-machine.svg │ │ └── symbolic │ │ └── apps │ │ └── io.github.revisto.drum-machine-symbolic.svg ├── meson.build └── io.github.revisto.drum-machine.metainfo.xml.in ├── requirements.txt ├── .flake8 ├── src ├── ui │ └── meson.build ├── models │ ├── meson.build │ └── drum_part.py ├── utils │ ├── meson.build │ ├── name_utils.py │ └── export_progress.py ├── config │ ├── meson.build │ ├── constants.py │ └── export_formats.py ├── interfaces │ ├── meson.build │ ├── player.py │ └── sound.py ├── dialogs │ ├── meson.build │ ├── save_changes_dialog.py │ └── midi_mapping_dialog.py ├── handlers │ ├── meson.build │ └── window_actions.py ├── services │ ├── meson.build │ ├── save_changes_service.py │ ├── sound_service.py │ ├── ui_helper.py │ ├── file_encoder.py │ ├── preset_service.py │ ├── audio_export_service.py │ ├── pattern_service.py │ ├── audio_renderer.py │ └── drum_machine_service.py ├── style-dark.css ├── gtk │ ├── save-changes-dialog.blp │ ├── help-overlay.blp │ └── audio-export-dialog.blp ├── __init__.py ├── drum-machine.gresource.xml ├── main.py ├── drum-machine.in ├── meson.build ├── style.css └── application.py ├── Makefile ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── FUNDING.yml └── workflows │ └── ci.yml ├── meson.build ├── SOUNDS_LICENSING.md ├── drum-machine.doap ├── io.github.revisto.drum-machine.json ├── .gitignore ├── snap └── snapcraft.yaml ├── python-dependencies.json └── README.md /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('drum-machine', preset: 'glib') 2 | -------------------------------------------------------------------------------- /data/drumkit/tom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/tom.wav -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame 2 | mido 3 | setuptools 4 | setuptools-scm 5 | packaging 6 | numpy 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E402, W503 4 | exclude = .flatpak-builder 5 | -------------------------------------------------------------------------------- /data/drumkit/clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/clap.wav -------------------------------------------------------------------------------- /data/drumkit/crash.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/crash.wav -------------------------------------------------------------------------------- /data/drumkit/hihat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/hihat.wav -------------------------------------------------------------------------------- /data/drumkit/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/kick.wav -------------------------------------------------------------------------------- /data/drumkit/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/snare.wav -------------------------------------------------------------------------------- /data/patterns/Slow.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Slow.mid -------------------------------------------------------------------------------- /data/drumkit/hihat-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/hihat-2.wav -------------------------------------------------------------------------------- /data/drumkit/kick-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/kick-2.wav -------------------------------------------------------------------------------- /data/drumkit/kick-3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/kick-3.wav -------------------------------------------------------------------------------- /data/drumkit/snare-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/drumkit/snare-2.wav -------------------------------------------------------------------------------- /data/patterns/Chill.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Chill.mid -------------------------------------------------------------------------------- /data/patterns/Night.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Night.mid -------------------------------------------------------------------------------- /data/patterns/Shoot.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Shoot.mid -------------------------------------------------------------------------------- /data/patterns/Boom Boom.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Boom Boom.mid -------------------------------------------------------------------------------- /data/patterns/Maybe Rock.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/patterns/Maybe Rock.mid -------------------------------------------------------------------------------- /data/screenshots/drum-machine-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/screenshots/drum-machine-dark.png -------------------------------------------------------------------------------- /data/screenshots/drum-machine-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revisto/drum-machine/master/data/screenshots/drum-machine-light.png -------------------------------------------------------------------------------- /src/ui/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'ui') 2 | 3 | ui_sources = [ 4 | 'drum_grid_builder.py', 5 | ] 6 | 7 | install_data(ui_sources, install_dir: modulesubdir) 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: generate-deps 2 | 3 | generate-deps: 4 | flatpak_pip_generator --build-isolation --requirements-file=requirements.txt -o python-dependencies --runtime=org.gnome.Sdk/x86_64/master -------------------------------------------------------------------------------- /src/models/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'models') 2 | 3 | models_sources = [ 4 | 'drum_part.py', 5 | ] 6 | 7 | install_data(models_sources, install_dir: modulesubdir) 8 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | bg 2 | ca 3 | de 4 | el 5 | en_GB 6 | es 7 | eu 8 | fa 9 | fi 10 | fr 11 | he 12 | hu 13 | it 14 | ka 15 | nl 16 | pt_BR 17 | ru 18 | sl 19 | sv 20 | tr 21 | uk 22 | uz 23 | zh_CN 24 | -------------------------------------------------------------------------------- /src/utils/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'utils') 2 | 3 | utils_sources = [ 4 | 'export_progress.py', 5 | 'name_utils.py', 6 | ] 7 | 8 | install_data(utils_sources, install_dir: modulesubdir) 9 | -------------------------------------------------------------------------------- /src/config/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'config') 2 | 3 | config_sources = [ 4 | 'constants.py', 5 | 'export_formats.py', 6 | ] 7 | 8 | install_data(config_sources, install_dir: modulesubdir) 9 | -------------------------------------------------------------------------------- /src/interfaces/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'interfaces') 2 | 3 | interfaces_sources = [ 4 | 'player.py', 5 | 'sound.py', 6 | ] 7 | 8 | install_data(interfaces_sources, install_dir: modulesubdir) -------------------------------------------------------------------------------- /data/io.github.revisto.drum-machine.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/dialogs/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'dialogs') 2 | 3 | services_sources = [ 4 | 'save_changes_dialog.py', 5 | 'audio_export_dialog.py', 6 | 'midi_mapping_dialog.py', 7 | ] 8 | 9 | install_data(services_sources, install_dir: modulesubdir) -------------------------------------------------------------------------------- /src/handlers/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'handlers') 2 | 3 | handlers_sources = [ 4 | 'drag_drop_handler.py', 5 | 'file_dialog_handler.py', 6 | 'window_actions.py', 7 | ] 8 | 9 | install_data(handlers_sources, install_dir: modulesubdir) 10 | -------------------------------------------------------------------------------- /data/presets/meson.build: -------------------------------------------------------------------------------- 1 | pattern_dir = meson.current_source_dir() 2 | 3 | pattern_files = [ 4 | 'Boom Boom.mid', 5 | 'Maybe Rock.mid', 6 | 'Night.mid', 7 | 'Slow.mid', 8 | 'Chill.mid', 9 | 'Shoot.mid', 10 | ] 11 | 12 | install_data(pattern_files, install_dir: get_option('datadir') / 'drum-machine' / 'data' / 'patterns') -------------------------------------------------------------------------------- /data/patterns/meson.build: -------------------------------------------------------------------------------- 1 | pattern_dir = meson.current_source_dir() 2 | 3 | pattern_files = [ 4 | 'Boom Boom.mid', 5 | 'Maybe Rock.mid', 6 | 'Night.mid', 7 | 'Slow.mid', 8 | 'Chill.mid', 9 | 'Shoot.mid', 10 | ] 11 | 12 | install_data(pattern_files, install_dir: get_option('datadir') / 'drum-machine' / 'data' / 'patterns') -------------------------------------------------------------------------------- /data/drumkit/meson.build: -------------------------------------------------------------------------------- 1 | drumkit_dir = meson.current_source_dir() 2 | 3 | drumkit_files = [ 4 | 'kick.wav', 5 | 'kick-2.wav', 6 | 'kick-3.wav', 7 | 'snare.wav', 8 | 'snare-2.wav', 9 | 'hihat.wav', 10 | 'hihat-2.wav', 11 | 'clap.wav', 12 | 'tom.wav', 13 | 'crash.wav', 14 | ] 15 | 16 | install_data(drumkit_files, install_dir: get_option('datadir') / 'drum-machine' / 'data' / 'drumkit') -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and the related issue. Also include relevant motivation and context. Use "Fixes #" if applicable. 4 | 5 | # Type of Change 6 | 7 | - [ ] Bug fix 8 | - [ ] New feature 9 | - [ ] Breaking change 10 | - [ ] Documentation update 11 | 12 | # Additional Notes 13 | 14 | Include any additional information that is important to this pull request. 15 | -------------------------------------------------------------------------------- /src/services/meson.build: -------------------------------------------------------------------------------- 1 | modulesubdir = join_paths(moduledir, 'services') 2 | 3 | services_sources = [ 4 | 'drum_machine_service.py', 5 | 'drum_part_manager.py', 6 | 'sound_service.py', 7 | 'audio_export_service.py', 8 | 'audio_renderer.py', 9 | 'file_encoder.py', 10 | 'ui_helper.py', 11 | 'pattern_service.py', 12 | 'save_changes_service.py', 13 | ] 14 | 15 | install_data(services_sources, install_dir: modulesubdir) -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('drum-machine', 2 | version: '2.0.0', 3 | meson_version: '>= 0.62.0', 4 | default_options: [ 'warning_level=2', 'werror=false', ], 5 | ) 6 | 7 | i18n = import('i18n') 8 | gnome = import('gnome') 9 | 10 | 11 | 12 | subdir('data') 13 | subdir('src') 14 | subdir('po') 15 | 16 | gnome.post_install( 17 | glib_compile_schemas: true, 18 | gtk_update_icon_cache: true, 19 | update_desktop_database: true, 20 | ) 21 | -------------------------------------------------------------------------------- /data/io.github.revisto.drum-machine.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Drum Machine 3 | Exec=drum-machine 4 | Icon=io.github.revisto.drum-machine 5 | Terminal=false 6 | Type=Application 7 | Categories=AudioVideo;Audio;Music; 8 | # Translators: Search terms to find this application. Do NOT translate or localise the semicolons! The list MUST also end with a semicolon! 9 | Keywords=drum;drums;sequencer;rhythm;music;pattern;beats;percussion;loop;groove; 10 | StartupNotify=true 11 | X-Purism-FormFactor=Workstation;Mobile; 12 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | application_id = 'io.github.revisto.drum-machine' 2 | 3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 4 | install_data( 5 | join_paths(scalable_dir, ('@0@.svg').format(application_id)), 6 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) 7 | ) 8 | 9 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 10 | install_data( 11 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), 12 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) 13 | ) 14 | -------------------------------------------------------------------------------- /po/POTFILES.in: -------------------------------------------------------------------------------- 1 | data/io.github.revisto.drum-machine.desktop.in 2 | data/io.github.revisto.drum-machine.metainfo.xml.in 3 | data/io.github.revisto.drum-machine.gschema.xml 4 | src/application.py 5 | src/config/export_formats.py 6 | src/dialogs/audio_export_dialog.py 7 | src/dialogs/midi_mapping_dialog.py 8 | src/handlers/drag_drop_handler.py 9 | src/handlers/file_dialog_handler.py 10 | src/utils/export_progress.py 11 | src/utils/name_utils.py 12 | src/ui/drum_grid_builder.py 13 | src/window.py 14 | src/window.ui 15 | src/gtk/help-overlay.blp 16 | src/gtk/save-changes-dialog.blp 17 | src/gtk/audio-export-dialog.blp 18 | -------------------------------------------------------------------------------- /src/style-dark.css: -------------------------------------------------------------------------------- 1 | .adw-window { 2 | background: #6b369c; 3 | } 4 | 5 | .drum-machine-box { 6 | background-color: #00000027; 7 | } 8 | 9 | .drum-machine-box .drum-toggle.toggle-active { 10 | background-color: #bf79e7; 11 | box-shadow: 0 0 10px #bf79e7; 12 | } 13 | 14 | .drum-machine-box .drum-toggle { 15 | background-color: #ffffff1c; 16 | } 17 | 18 | .drum-machine-box .drum-toggle:hover { 19 | background-color: #ffffff42; 20 | } 21 | 22 | .drum-machine-box .drum-toggle:checked { 23 | background-color: #ffffff5e; 24 | } 25 | 26 | .bottom-wrapper { 27 | background: #6b369c; 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: ["bug","needs-triage"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /src/gtk/save-changes-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SaveChangesDialog: Adw.AlertDialog { 5 | heading: _("Save Changes?"); 6 | body: _("Current pattern contains unsaved changes. Changes which are not saved will be permanently lost."); 7 | close-response: "cancel"; 8 | default-response: "save"; 9 | response::save => $_on_save(); 10 | response::discard => $_on_discard(); 11 | response::cancel => $_on_cancel(); 12 | 13 | responses [ 14 | /* Translators: Cancel the operation */cancel: _("_Cancel"), 15 | /* Translators: Discard all changes */discard: _("_Discard") destructive, 16 | /* Translators: Save current changes */save: _("_Save") suggested, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: ["enhancement","needs-triage"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: Revisto 4 | patreon: Revisto 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['https://www.coffeete.ir/revisto'] 16 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | -------------------------------------------------------------------------------- /src/drum-machine.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | style-dark.css 6 | window.ui 7 | gtk/help-overlay.ui 8 | gtk/save-changes-dialog.ui 9 | gtk/audio-export-dialog.ui 10 | 11 | 12 | 13 | ../data/icons/hicolor/scalable/actions/share-symbolic.svg 14 | 15 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import sys 21 | from .application import DrumMachineApplication 22 | 23 | 24 | def main(version: str) -> int: 25 | app = DrumMachineApplication(version=version) 26 | return app.run(sys.argv) 27 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/share-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/interfaces/player.py: -------------------------------------------------------------------------------- 1 | # interfaces/player.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from abc import ABC, abstractmethod 21 | 22 | 23 | class IPlayer(ABC): 24 | @abstractmethod 25 | def play(self) -> None: 26 | pass 27 | 28 | @abstractmethod 29 | def stop(self) -> None: 30 | pass 31 | 32 | @abstractmethod 33 | def set_bpm(self, bpm: float) -> None: 34 | pass 35 | 36 | @abstractmethod 37 | def set_volume(self, volume: float) -> None: 38 | pass 39 | -------------------------------------------------------------------------------- /SOUNDS_LICENSING.md: -------------------------------------------------------------------------------- 1 | # Sounds Licensing 2 | 3 | | Sound File | Source | License 4 | | :-: | :-: | :-: 5 | | clap.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 6 | | crash.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 7 | | hihat.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 8 | | hihat-2.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 9 | | kick.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 10 | | kick-2.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 11 | | kick-3.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 12 | | snare.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 13 | | snare-2.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 14 | | tom.wav | [99Sounds Drum Samples](https://99sounds.org/drum-samples/) | Royalty-Free 15 | 16 | **License Terms:** 17 | 18 | The drum samples used in this application are from [99Sounds](https://99sounds.org/drum-samples/). All sounds are royalty-free and can be used in free and commercial projects. For more information, please refer to the [99Sounds License](https://99sounds.org/license/). -------------------------------------------------------------------------------- /src/utils/name_utils.py: -------------------------------------------------------------------------------- 1 | # utils/name_utils.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from pathlib import Path 21 | from typing import Union 22 | from gettext import gettext as _ 23 | 24 | 25 | def extract_name_from_path(file_path: Union[str, Path]) -> str: 26 | """Extract display name from file path 27 | 28 | Args: 29 | file_path: Path to the file (str or Path object) 30 | 31 | Returns: 32 | str: Display name extracted from filename, or "Custom Sound" if empty 33 | """ 34 | path = Path(file_path) 35 | name = path.stem.replace("_", " ").replace("-", " ").title() 36 | return name if name.strip() else _("Custom Sound") 37 | -------------------------------------------------------------------------------- /drum-machine.doap: -------------------------------------------------------------------------------- 1 | 2 | 6 | Drum Machine 7 | Create and play drum beats 8 | 9 | Drum Machine is a modern application for creating, playing, and managing drum patterns. Perfect for musicians, producers, and anyone interested in rhythm creation, this application provides a simple interface for drum pattern programming. 10 | 11 | 12 | 2024-11-27 13 | Python 14 | 15 | GTK 4 16 | Libadwaita 17 | 18 | 19 | Revisto 20 | 21 | 22 | 23 | 24 | revisto 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'io.github.revisto.drum-machine.desktop.in', 3 | output: 'io.github.revisto.drum-machine.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: get_option('datadir') / 'applications' 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, args: [desktop_file]) 13 | endif 14 | 15 | appstream_file = i18n.merge_file( 16 | input: 'io.github.revisto.drum-machine.metainfo.xml.in', 17 | output: 'io.github.revisto.drum-machine.metainfo.xml', 18 | po_dir: '../po', 19 | install: true, 20 | install_dir: get_option('datadir') / 'metainfo' 21 | ) 22 | 23 | appstreamcli = find_program('appstreamcli', required: false, disabler: true) 24 | test('Validate appstream file', appstreamcli, 25 | args: ['validate', '--no-net', '--explain', appstream_file]) 26 | 27 | install_data('io.github.revisto.drum-machine.gschema.xml', 28 | install_dir: get_option('datadir') / 'glib-2.0' / 'schemas' 29 | ) 30 | 31 | compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) 32 | test('Validate schema file', 33 | compile_schemas, 34 | args: ['--strict', '--dry-run', meson.current_source_dir()]) 35 | 36 | subdir('icons') 37 | subdir('drumkit') 38 | subdir('patterns') 39 | -------------------------------------------------------------------------------- /src/interfaces/sound.py: -------------------------------------------------------------------------------- 1 | # interfaces/sound.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from abc import ABC, abstractmethod 21 | 22 | 23 | class ISoundService(ABC): 24 | @abstractmethod 25 | def load_sounds(self) -> None: 26 | pass 27 | 28 | @abstractmethod 29 | def play_sound(self, sound_name: str) -> None: 30 | pass 31 | 32 | @abstractmethod 33 | def set_volume(self, volume: float) -> None: 34 | pass 35 | 36 | @abstractmethod 37 | def preview_sound(self, sound_name: str) -> None: 38 | """Play a preview of the sound at preview volume level""" 39 | pass 40 | 41 | @abstractmethod 42 | def stop_all_sounds(self) -> None: 43 | """Stop all currently playing sounds""" 44 | pass 45 | -------------------------------------------------------------------------------- /io.github.revisto.drum-machine.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "io.github.revisto.drum-machine", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "48", 5 | "sdk" : "org.gnome.Sdk", 6 | "command" : "drum-machine", 7 | "finish-args" : [ 8 | "--share=ipc", 9 | "--socket=fallback-x11", 10 | "--device=dri", 11 | "--socket=wayland", 12 | "--socket=pulseaudio" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/man", 18 | "/share/doc", 19 | "/share/gtk-doc", 20 | "/share/man", 21 | "/share/pkgconfig", 22 | "*.la", 23 | "*.a" 24 | ], 25 | "modules" : [ 26 | "python-dependencies.json", 27 | { 28 | "name": "blueprint-compiler", 29 | "buildsystem": "meson", 30 | "cleanup": ["*"], 31 | "sources": [ 32 | { 33 | "type": "git", 34 | "url": "https://gitlab.gnome.org/GNOME/blueprint-compiler", 35 | "tag": "0.18.0" 36 | } 37 | ] 38 | }, 39 | { 40 | "name" : "drum-machine", 41 | "builddir" : true, 42 | "buildsystem" : "meson", 43 | "sources" : [ 44 | { 45 | "type" : "git", 46 | "url" : "https://github.com/revisto/drum-machine.git" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/drum-machine.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # drum-machine.in 4 | # 5 | # Copyright 2024 rev 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | # SPDX-License-Identifier: GPL-3.0-or-later 21 | 22 | import os 23 | import sys 24 | import signal 25 | import locale 26 | import gettext 27 | 28 | VERSION = '@VERSION@' 29 | pkgdatadir = '@pkgdatadir@' 30 | localedir = '@localedir@' 31 | 32 | sys.path.insert(1, pkgdatadir) 33 | signal.signal(signal.SIGINT, signal.SIG_DFL) 34 | locale.bindtextdomain('drum-machine', localedir) 35 | locale.textdomain('drum-machine') 36 | gettext.install('drum-machine', localedir) 37 | 38 | if __name__ == '__main__': 39 | import gi 40 | 41 | from gi.repository import Gio 42 | resource = Gio.Resource.load(os.path.join(pkgdatadir, 'drum-machine.gresource')) 43 | resource._register() 44 | 45 | from drum_machine import main 46 | sys.exit(main.main(VERSION)) 47 | -------------------------------------------------------------------------------- /src/services/save_changes_service.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | from .drum_machine_service import DrumMachineService 3 | from ..dialogs.save_changes_dialog import SaveChangesDialog 4 | 5 | 6 | class SaveChangesService: 7 | """ 8 | Responsible for prompting the user about unsaved changes and 9 | handling the user's decision (save or discard). 10 | """ 11 | 12 | def __init__(self, window, drum_machine_service: DrumMachineService) -> None: 13 | self.window = window 14 | self.drum_machine_service = drum_machine_service 15 | self._unsaved_changes: bool = False 16 | 17 | def mark_unsaved_changes(self, value: bool) -> None: 18 | self._unsaved_changes = value 19 | 20 | def has_unsaved_changes(self) -> bool: 21 | return self._unsaved_changes 22 | 23 | def prompt_save_changes(self, on_save: Callable, on_discard: Callable) -> None: 24 | """ 25 | Open the dialog; if user discards, call on_discard(). 26 | If user saves, call on_save(), then mark changes as saved. 27 | """ 28 | SaveChangesDialog( 29 | window=self.window, 30 | on_save_callback=self._wrap_save_callback(on_save), 31 | on_discard_callback=on_discard, 32 | ) 33 | 34 | def _wrap_save_callback(self, callback: Callable) -> Callable: 35 | """ 36 | Wrap the user-provided callback to reset unsaved changes after saving. 37 | """ 38 | 39 | def wrapper(): 40 | callback() 41 | self.mark_unsaved_changes(False) 42 | 43 | return wrapper 44 | -------------------------------------------------------------------------------- /src/config/constants.py: -------------------------------------------------------------------------------- 1 | # config/constants.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from typing import List, Set, Tuple 21 | 22 | DEFAULT_DRUM_PARTS: List[str] = [ 23 | "kick", 24 | "kick-2", 25 | "kick-3", 26 | "snare", 27 | "snare-2", 28 | "hihat", 29 | "hihat-2", 30 | "clap", 31 | "tom", 32 | "crash", 33 | ] 34 | DEFAULT_PATTERNS: List[str] = [ 35 | "Shoot", 36 | "Maybe Rock", 37 | "Boom Boom", 38 | "Night", 39 | "Slow", 40 | "Chill", 41 | ] 42 | NUM_TOGGLES: int = 16 43 | GROUP_TOGGLE_COUNT: int = 4 44 | 45 | # Audio rendering constants 46 | DEFAULT_FALLBACK_SAMPLE_SIZE: Tuple[int, int] = (1000, 2) 47 | 48 | # Progress bar constants 49 | PULSE_INTERVAL_SECONDS: float = 1.0 50 | 51 | # Audio constants 52 | MIXER_CHANNELS: int = 32 53 | 54 | # DrumPartManager constants 55 | DRUM_PARTS_CONFIG_FILE: str = "drum_parts.json" 56 | 57 | # Supported audio file formats for input/import 58 | SUPPORTED_INPUT_AUDIO_FORMATS: Set[str] = {".wav", ".mp3", ".ogg", ".flac"} 59 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/io.github.revisto.drum-machine-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/dialogs/save_changes_dialog.py: -------------------------------------------------------------------------------- 1 | # dialogs/save_changes_dialog.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from gi.repository import Adw, Gtk 21 | 22 | 23 | @Gtk.Template( 24 | resource_path="/io/github/revisto/drum-machine/gtk/save-changes-dialog.ui" 25 | ) 26 | class SaveChangesDialog(Adw.AlertDialog): 27 | __gtype_name__ = "SaveChangesDialog" 28 | 29 | def __init__(self, window, on_save_callback=None, on_discard_callback=None): 30 | super().__init__() 31 | self._on_save_callback = on_save_callback 32 | self._on_discard_callback = on_discard_callback 33 | self.present(window) 34 | 35 | @Gtk.Template.Callback() 36 | def _on_save(self, _dialog, _response): 37 | if callable(self._on_save_callback): 38 | self._on_save_callback() 39 | self.close() 40 | 41 | @Gtk.Template.Callback() 42 | def _on_discard(self, _dialog, _response): 43 | if callable(self._on_discard_callback): 44 | self._on_discard_callback() 45 | self.close() 46 | 47 | @Gtk.Template.Callback() 48 | def _on_cancel(self, _dialog, _response): 49 | self.close() 50 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() 2 | moduledir = pkgdatadir / 'drum_machine' 3 | gnome = import('gnome') 4 | 5 | blueprint_compiler = find_program('blueprint-compiler', required: true) 6 | 7 | blueprint_files = files( 8 | 'gtk/help-overlay.blp', 9 | 'gtk/audio-export-dialog.blp', 10 | 'gtk/save-changes-dialog.blp', 11 | ) 12 | 13 | compiled_ui_dir_name = 'compiled_ui_files' 14 | 15 | compiled_blueprints = custom_target('compiled_blueprints', 16 | input: blueprint_files, 17 | output: compiled_ui_dir_name, 18 | command: [blueprint_compiler, 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'] 19 | ) 20 | 21 | gnome.compile_resources('drum-machine', 22 | 'drum-machine.gresource.xml', 23 | gresource_bundle: true, 24 | install: true, 25 | install_dir: pkgdatadir, 26 | dependencies: [compiled_blueprints], 27 | source_dir: [meson.current_source_dir(), compiled_blueprints.full_path()], 28 | ) 29 | 30 | python = import('python') 31 | 32 | conf = configuration_data() 33 | conf.set('PYTHON', python.find_installation('python3').full_path()) 34 | conf.set('VERSION', meson.project_version()) 35 | conf.set('localedir', get_option('prefix') / get_option('localedir')) 36 | conf.set('pkgdatadir', pkgdatadir) 37 | 38 | configure_file( 39 | input: 'drum-machine.in', 40 | output: 'drum-machine', 41 | configuration: conf, 42 | install: true, 43 | install_dir: get_option('bindir'), 44 | install_mode: 'r-xr-xr-x' 45 | ) 46 | 47 | drum_machine_sources = [ 48 | '__init__.py', 49 | 'main.py', 50 | 'window.py', 51 | 'application.py', 52 | 'style.css', 53 | 'style-dark.css', 54 | ] 55 | 56 | subdir('config') 57 | subdir('utils') 58 | subdir('interfaces') 59 | subdir('models') 60 | subdir('handlers') 61 | subdir('services') 62 | subdir('dialogs') 63 | subdir('ui') 64 | 65 | install_data(drum_machine_sources, install_dir: moduledir) 66 | -------------------------------------------------------------------------------- /src/models/drum_part.py: -------------------------------------------------------------------------------- 1 | # models/drum_part.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import uuid 21 | from dataclasses import dataclass 22 | 23 | 24 | @dataclass 25 | class DrumPart: 26 | id: str 27 | name: str 28 | file_path: str 29 | is_custom: bool = False 30 | midi_note_id: int = None 31 | 32 | @classmethod 33 | def create_default(cls, name: str, file_path: str, midi_note_id: int): 34 | return cls( 35 | id=f"default_{name}", 36 | name=name.replace("-", " ").title(), 37 | file_path=file_path, 38 | is_custom=False, 39 | midi_note_id=midi_note_id, 40 | ) 41 | 42 | @classmethod 43 | def create_custom(cls, name: str, file_path: str, midi_note_id: int): 44 | return cls( 45 | id=str(uuid.uuid4()), 46 | name=name, 47 | file_path=file_path, 48 | is_custom=True, 49 | midi_note_id=midi_note_id, 50 | ) 51 | 52 | def to_dict(self): 53 | return { 54 | "id": self.id, 55 | "name": self.name, 56 | "file_path": self.file_path, 57 | "is_custom": self.is_custom, 58 | "midi_note_id": self.midi_note_id, 59 | } 60 | 61 | @classmethod 62 | def from_dict(cls, data): 63 | if "midi_note_id" not in data: 64 | data["midi_note_id"] = None 65 | return cls(**data) 66 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .adw-window { 2 | background: #D38CFF; 3 | } 4 | 5 | .drum-machine-box { 6 | padding: 20px; 7 | padding-bottom: 15px; 8 | border-radius: 10px; 9 | background-color: #ffffff27; 10 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | .drum-machine-box .drum-toggle { 14 | background-color: #ffffff65; 15 | } 16 | 17 | .drum-machine-box .drum-toggle.toggle-active { 18 | background-color: #ffffff; 19 | box-shadow: 0 0 10px #ffffff; 20 | } 21 | 22 | .drum-machine-box .drum-toggle:hover { 23 | background-color: #ffffffb0; 24 | } 25 | 26 | .drum-machine-box .drum-toggle:checked { 27 | background-color: #ffffff; 28 | } 29 | 30 | .drum-machine-box.medium button { 31 | min-width: 20px; 32 | min-height: 20px; 33 | padding: 4px; 34 | border-radius: 6px; 35 | } 36 | 37 | .drum-machine-box.compact button { 38 | min-width: 10px; 39 | min-height: 10px; 40 | padding: 0; 41 | border-radius: 6px; 42 | } 43 | 44 | .bottom-wrapper { 45 | padding: 10px; 46 | } 47 | 48 | .drum-machine-box.compact .drum-part-button { 49 | padding-left: 6px; 50 | padding-right: 6px; 51 | } 52 | 53 | .disabled { 54 | opacity: 0.5; 55 | } 56 | 57 | .drag-over-replace { 58 | background-color: @accent_bg_color !important; 59 | box-shadow: 0 0 15px @accent_bg_color !important; 60 | border: 3px solid @accent_bg_color !important; 61 | border-radius: 6px; 62 | } 63 | 64 | 65 | /* New drum placeholder styling */ 66 | .new-drum-placeholder button { 67 | opacity: 0.6; 68 | color: @accent_color; 69 | } 70 | 71 | .new-drum-placeholder.highlight button { 72 | opacity: 1; 73 | box-shadow: 0 0 10px @accent_color; 74 | border: 2px solid @accent_color; 75 | } 76 | 77 | .export-button { 78 | transition: 200ms ease-in-out; 79 | } 80 | 81 | .drag-source-active { 82 | opacity: 0.5; 83 | } 84 | 85 | /* Remove default GTK drop target outline from column */ 86 | box:drop(active) { 87 | outline: none; 88 | box-shadow: none; 89 | border: none; 90 | } 91 | 92 | .insert-above { 93 | box-shadow: 0 -3px 0 0 @accent_color; 94 | } 95 | 96 | .insert-below { 97 | box-shadow: 0 3px 0 0 @accent_color; 98 | } 99 | -------------------------------------------------------------------------------- /src/config/export_formats.py: -------------------------------------------------------------------------------- 1 | # config/export_formats.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | from dataclasses import dataclass 21 | from typing import Dict 22 | from gettext import gettext as _ 23 | 24 | 25 | @dataclass 26 | class ExportFormat: 27 | """Configuration for an audio export format""" 28 | 29 | ext: str 30 | pattern: str 31 | name: str 32 | display: str 33 | supports_metadata: bool 34 | 35 | 36 | class ExportFormatRegistry: 37 | """Registry for managing available export formats""" 38 | 39 | def __init__(self) -> None: 40 | self._formats = { 41 | 0: ExportFormat( 42 | ext=".mp3", 43 | pattern="*.mp3", 44 | name=_("MP3 files"), 45 | display=_("MP3"), 46 | supports_metadata=True, 47 | ), 48 | 1: ExportFormat( 49 | ext=".flac", 50 | pattern="*.flac", 51 | name=_("FLAC files"), 52 | display=_("FLAC (Lossless)"), 53 | supports_metadata=True, 54 | ), 55 | 2: ExportFormat( 56 | ext=".ogg", 57 | pattern="*.ogg", 58 | name=_("Ogg files"), 59 | display=_("Ogg Vorbis"), 60 | supports_metadata=True, 61 | ), 62 | 3: ExportFormat( 63 | ext=".wav", 64 | pattern="*.wav", 65 | name=_("WAV files"), 66 | display=_("WAV (Uncompressed)"), 67 | supports_metadata=False, 68 | ), 69 | } 70 | 71 | def get_format(self, format_id: int) -> ExportFormat: 72 | """Get format configuration by ID""" 73 | return self._formats.get(format_id, self._formats[0]) 74 | 75 | def get_all_formats(self) -> Dict[int, ExportFormat]: 76 | """Get all available formats""" 77 | return self._formats.copy() 78 | 79 | def get_format_by_extension(self, extension: str) -> ExportFormat: 80 | """Get format configuration by file extension""" 81 | for fmt in self._formats.values(): 82 | if fmt.ext == extension: 83 | return fmt 84 | return self._formats[0] 85 | -------------------------------------------------------------------------------- /src/services/sound_service.py: -------------------------------------------------------------------------------- 1 | # services/sound_service.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import pygame 21 | import logging 22 | from typing import Dict 23 | from ..interfaces.sound import ISoundService 24 | from .drum_part_manager import DrumPartManager 25 | from ..config.constants import MIXER_CHANNELS 26 | 27 | 28 | class SoundService(ISoundService): 29 | def __init__(self, user_data_dir: str, bundled_sounds_dir: str = None) -> None: 30 | pygame.init() 31 | pygame.mixer.set_num_channels(MIXER_CHANNELS) 32 | self.user_data_dir = user_data_dir 33 | self.bundled_sounds_dir = bundled_sounds_dir or user_data_dir 34 | self.drum_part_manager = DrumPartManager(user_data_dir, self.bundled_sounds_dir) 35 | self.sounds: Dict[str, pygame.mixer.Sound] = {} 36 | self._current_volume: float = 1.0 37 | 38 | def load_sounds(self) -> None: 39 | self.sounds = {} 40 | for part in self.drum_part_manager.get_all_parts(): 41 | try: 42 | # Skip temporary parts without file paths 43 | if not part.file_path: 44 | continue 45 | 46 | sound = pygame.mixer.Sound(part.file_path) 47 | sound.set_volume(self._current_volume) 48 | self.sounds[part.id] = sound 49 | except Exception as e: 50 | logging.error(f"Error loading sound {part.name}: {e}") 51 | 52 | def reload_sounds(self) -> None: 53 | self.drum_part_manager.reload() 54 | self.load_sounds() 55 | 56 | def reload_specific_sound(self, part_id: str) -> None: 57 | """Reload a specific sound after drum part replacement""" 58 | self.drum_part_manager.reload() 59 | part = self.drum_part_manager.get_part_by_id(part_id) 60 | if part: 61 | sound = pygame.mixer.Sound(part.file_path) 62 | sound.set_volume(self._current_volume) 63 | self.sounds[part_id] = sound 64 | 65 | def play_sound(self, part_id: str) -> None: 66 | if part_id in self.sounds: 67 | self.sounds[part_id].play() 68 | 69 | def set_volume(self, volume: float) -> None: 70 | self._current_volume = volume / 100 71 | for sound in self.sounds.values(): 72 | sound.set_volume(self._current_volume) 73 | 74 | def preview_sound(self, part_id: str) -> None: 75 | if part_id in self.sounds: 76 | self.play_sound(part_id) 77 | 78 | def stop_all_sounds(self) -> None: 79 | pygame.mixer.stop() 80 | -------------------------------------------------------------------------------- /src/gtk/help-overlay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | ShortcutsWindow help_overlay { 4 | modal: true; 5 | 6 | ShortcutsSection { 7 | section-name: "shortcuts"; 8 | max-height: 12; 9 | 10 | ShortcutsGroup { 11 | title: C_("shortcut window", "General"); 12 | 13 | ShortcutsShortcut { 14 | title: C_("shortcut window", "Show Shortcuts"); 15 | action-name: "win.show-help-overlay"; 16 | } 17 | 18 | ShortcutsShortcut { 19 | title: C_("shortcut window", "Quit"); 20 | action-name: "win.quit"; 21 | accelerator: "q"; 22 | } 23 | } 24 | 25 | ShortcutsGroup { 26 | title: C_("shortcut window", "Playback Controls"); 27 | 28 | ShortcutsShortcut { 29 | title: C_("shortcut window", "Play/Pause"); 30 | action-name: "win.play_pause"; 31 | accelerator: "space"; 32 | } 33 | 34 | ShortcutsShortcut { 35 | title: C_("shortcut window", "Clear All"); 36 | action-name: "win.clear_toggles"; 37 | accelerator: "Delete"; 38 | } 39 | } 40 | 41 | ShortcutsGroup { 42 | title: C_("shortcut window", "BPM & Volume Controls"); 43 | 44 | ShortcutsShortcut { 45 | title: C_("shortcut window", "Increase BPM"); 46 | action-name: "win.increase_bpm"; 47 | accelerator: "plus equal"; 48 | } 49 | 50 | ShortcutsShortcut { 51 | title: C_("shortcut window", "Decrease BPM"); 52 | action-name: "win.decrease_bpm"; 53 | accelerator: "minus"; 54 | } 55 | 56 | ShortcutsShortcut { 57 | title: C_("shortcut window", "Increase Volume"); 58 | action-name: "win.increase_volume"; 59 | accelerator: "Up"; 60 | } 61 | 62 | ShortcutsShortcut { 63 | title: C_("shortcut window", "Decrease Volume"); 64 | action-name: "win.decrease_volume"; 65 | accelerator: "Down"; 66 | } 67 | 68 | ShortcutsShortcut { 69 | title: C_("shortcut window", "Mute"); 70 | action-name: "win.mute"; 71 | accelerator: "m"; 72 | } 73 | } 74 | 75 | ShortcutsGroup { 76 | title: C_("shortcut window", "Pattern Management"); 77 | 78 | ShortcutsShortcut { 79 | title: C_("shortcut window", "Load Pattern"); 80 | action-name: "win.load_pattern"; 81 | accelerator: "o"; 82 | } 83 | 84 | ShortcutsShortcut { 85 | title: C_("shortcut window", "Save Pattern"); 86 | action-name: "win.save_pattern"; 87 | accelerator: "s"; 88 | } 89 | 90 | ShortcutsShortcut { 91 | title: C_("shortcut window", "Export Audio"); 92 | action-name: "win.export_audio"; 93 | accelerator: "e"; 94 | } 95 | } 96 | 97 | ShortcutsGroup { 98 | title: C_("shortcut window", "Navigation"); 99 | 100 | ShortcutsShortcut { 101 | title: C_("shortcut window", "Go to Instrument"); 102 | action-name: "win.go_to_instrument"; 103 | accelerator: "i"; 104 | } 105 | 106 | ShortcutsShortcut { 107 | title: C_("shortcut window", "Previous Page"); 108 | action-name: "win.previous_page"; 109 | accelerator: "Page_Up"; 110 | } 111 | 112 | ShortcutsShortcut { 113 | title: C_("shortcut window", "Next Page"); 114 | action-name: "win.next_page"; 115 | accelerator: "Page_Down"; 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | share/python-wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | *.manifest 27 | *.spec 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .nox/ 37 | .coverage 38 | .coverage.* 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | *.cover 43 | *.py,cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | cover/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | db.sqlite3 56 | db.sqlite3-journal 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | .pybuilder/ 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # IPython 76 | profile_default/ 77 | ipython_config.py 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # pipenv 83 | Pipfile.lock 84 | 85 | # poetry 86 | poetry.lock 87 | 88 | # pdm 89 | .pdm.toml 90 | 91 | # PEP 582 92 | __pypackages__/ 93 | 94 | # Celery stuff 95 | celerybeat-schedule 96 | celerybeat.pid 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | # pytype static type analyzer 129 | .pytype/ 130 | 131 | # Cython debug symbols 132 | cython_debug/ 133 | 134 | # PyCharm 135 | .idea/ 136 | 137 | # VS Code 138 | .vscode/ 139 | 140 | # GNOME Builder 141 | .gnome-builder-devenv 142 | 143 | # Meson build system 144 | _build/ 145 | build/ 146 | builddir/ 147 | meson-logs/ 148 | meson-private/ 149 | meson-info/ 150 | compile_commands.json 151 | build.ninja 152 | .ninja_log 153 | .ninja_deps 154 | 155 | # Flatpak 156 | .flatpak-builder/ 157 | 158 | # AppStream 159 | *.appdata.xml 160 | 161 | # Desktop files (generated) 162 | *.desktop 163 | 164 | # GSettings schemas (compiled) 165 | *.gschema.valid 166 | *.gschema.xml.valid 167 | 168 | # Icon cache 169 | icon-theme.cache 170 | 171 | # GTK+ resources (compiled) 172 | *.gresource 173 | 174 | # GLib 175 | *.gir 176 | *.typelib 177 | 178 | # Vala 179 | *.c 180 | *.h 181 | *.vapi 182 | 183 | # GObject Introspection 184 | *.gir 185 | *.typelib 186 | 187 | # System files 188 | .DS_Store 189 | .DS_Store? 190 | ._* 191 | .Spotlight-V100 192 | .Trashes 193 | ehthumbs.db 194 | Thumbs.db 195 | 196 | # Temporary files 197 | *.tmp 198 | *.temp 199 | *.swp 200 | *.swo 201 | *~ 202 | 203 | # Backup files 204 | *.bak 205 | *.backup 206 | *.old 207 | 208 | # Log files 209 | *.log 210 | logs/ 211 | 212 | # Database files 213 | *.db 214 | *.sqlite 215 | *.sqlite3 216 | 217 | # Configuration files with sensitive data 218 | config.ini 219 | settings.json 220 | secrets.json 221 | 222 | # Package files 223 | *.deb 224 | *.rpm 225 | *.tar.gz 226 | *.zip 227 | 228 | # Documentation build 229 | docs/build/ 230 | docs/_build/ 231 | 232 | # Local development files 233 | local/ 234 | dev/ 235 | test-data/ 236 | -------------------------------------------------------------------------------- /src/gtk/audio-export-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $AudioExportDialog: Adw.Dialog { 5 | content-width: 450; 6 | title: _("Export Audio"); 7 | 8 | child: Adw.ToolbarView toolbar_view { 9 | [top] 10 | Adw.HeaderBar header_bar { 11 | show-title: true; 12 | } 13 | 14 | [top] 15 | Adw.Banner warning_banner { 16 | revealed: false; 17 | } 18 | 19 | content: Overlay main_overlay { 20 | [overlay] 21 | ProgressBar progress_bar { 22 | valign: start; 23 | visible: false; 24 | 25 | styles [ 26 | "osd", 27 | ] 28 | } 29 | 30 | ScrolledWindow { 31 | hscrollbar-policy: never; 32 | propagate-natural-height: true; 33 | 34 | Box content_box { 35 | orientation: vertical; 36 | spacing: 24; 37 | margin-top: 24; 38 | margin-bottom: 24; 39 | margin-start: 24; 40 | margin-end: 24; 41 | 42 | Adw.PreferencesGroup export_group { 43 | Adw.ComboRow format_row { 44 | title: _("Audio Format"); 45 | 46 | model: StringList format_list {}; 47 | } 48 | 49 | Adw.SpinRow repeat_row { 50 | title: _("Repeat Count"); 51 | subtitle: _("How many times to repeat the pattern"); 52 | 53 | adjustment: Adjustment { 54 | lower: 1; 55 | upper: 100; 56 | value: 5; 57 | step-increment: 1; 58 | page-increment: 5; 59 | }; 60 | } 61 | } 62 | 63 | Adw.PreferencesGroup metadata_group { 64 | title: _("Metadata"); 65 | 66 | Adw.EntryRow artist_row { 67 | title: _("Artist Name"); 68 | show-apply-button: false; 69 | } 70 | 71 | Adw.EntryRow song_row { 72 | title: _("Song Name"); 73 | show-apply-button: false; 74 | } 75 | 76 | Adw.ActionRow cover_row { 77 | title: _("Cover Art"); 78 | activatable-widget: cover_button; 79 | 80 | Button cover_button { 81 | label: _("Choose File…"); 82 | valign: center; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }; 89 | 90 | [bottom] 91 | Box button_box { 92 | orientation: vertical; 93 | halign: center; 94 | margin-start: 24; 95 | margin-end: 24; 96 | margin-top: 16; 97 | margin-bottom: 32; 98 | spacing: 12; 99 | 100 | Button export_button { 101 | label: _("Export Audio"); 102 | 103 | styles [ 104 | "suggested-action", 105 | "pill", 106 | ] 107 | } 108 | 109 | Button cancel_button { 110 | label: _("Cancel Export"); 111 | visible: false; 112 | 113 | styles [ 114 | "destructive-action", 115 | "pill", 116 | ] 117 | } 118 | 119 | Box status_overlay { 120 | orientation: vertical; 121 | halign: center; 122 | spacing: 6; 123 | visible: false; 124 | 125 | Label status_label { 126 | halign: center; 127 | label: "Exporting…"; 128 | 129 | styles [ 130 | "heading", 131 | ] 132 | } 133 | 134 | Label detail_label { 135 | halign: center; 136 | label: "Processing…"; 137 | 138 | styles [ 139 | "caption", 140 | ] 141 | } 142 | } 143 | } 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Lint with flake8 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.12' 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install flake8 24 | - name: Lint 25 | run: | 26 | # Stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --builtins="_" 28 | # Exit if any flake8 issue is found (errors or warnings) 29 | flake8 . --count --max-complexity=10 --max-line-length=88 --statistics 30 | 31 | translation-check: 32 | name: Check translation template 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Set up Python 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: '3.12' 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install meson ninja 44 | sudo apt-get update 45 | sudo apt-get install -y gettext blueprint-compiler desktop-file-utils libglib2.0-dev 46 | - name: Setup build directory 47 | run: | 48 | meson setup builddir 49 | - name: Check if .pot file needs updating 50 | run: | 51 | # Backup the original .pot file from the checkout 52 | cp po/drum-machine.pot po/drum-machine.pot.orig 53 | 54 | # Regenerate the .pot file 55 | meson compile -C builddir drum-machine-pot 56 | 57 | # Verify the generated .pot file exists 58 | if [ ! -f po/drum-machine.pot ]; then 59 | echo "::error::Failed to generate .pot file. Build may have failed." 60 | exit 1 61 | fi 62 | 63 | # Normalize both files by: 64 | # 1. Replacing timestamp with placeholder 65 | # 2. Removing line number references (e.g., ":123" -> "") 66 | normalize_pot() { 67 | sed -E \ 68 | -e 's/POT-Creation-Date: [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}\+[0-9]{4}/POT-Creation-Date: TIMESTAMP/g' \ 69 | -e '/^#:/ s/:[0-9]+//g' \ 70 | "$1" 71 | } 72 | 73 | normalize_pot po/drum-machine.pot.orig > po/drum-machine.pot.orig.normalized 74 | normalize_pot po/drum-machine.pot > po/drum-machine.pot.normalized 75 | 76 | # Compare normalized files (ignore whitespace differences) 77 | if ! diff -u --ignore-all-space po/drum-machine.pot.orig.normalized po/drum-machine.pot.normalized > /dev/null; then 78 | echo "::error file=po/drum-machine.pot::Translation template (.pot file) is out of date. Please run 'meson compile -C builddir drum-machine-pot' and commit the updated po/drum-machine.pot file." 79 | echo "" 80 | echo "Diff of changes needed:" 81 | diff -u po/drum-machine.pot.orig.normalized po/drum-machine.pot.normalized || true 82 | exit 1 83 | fi 84 | 85 | echo "✓ Translation template is up to date" 86 | 87 | flatpak: 88 | name: Flatpak Builder 89 | runs-on: ubuntu-latest 90 | container: 91 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 92 | options: --privileged 93 | steps: 94 | - uses: actions/checkout@v4 95 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 96 | with: 97 | bundle: "drum-machine-devel.flatpak" 98 | manifest-path: "io.github.revisto.drum-machine.json" 99 | run-tests: "true" 100 | cache-key: flatpak-builder-${{ github.sha }} 101 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: drum-machine 2 | title: Drum Machine 3 | base: core24 4 | license: GPL-3.0-or-later 5 | grade: stable 6 | confinement: strict 7 | compression: lzo 8 | adopt-info: drum-machine 9 | 10 | contact: 11 | - therevisto@gmail.com 12 | issues: 13 | - https://github.com/Revisto/drum-machine/issues 14 | donation: 15 | - https://patreon.com/Revisto 16 | source-code: 17 | - https://github.com/Revisto/drum-machine 18 | website: 19 | - https://apps.gnome.org/DrumMachine/ 20 | 21 | platforms: 22 | amd64: 23 | arm64: 24 | armhf: 25 | 26 | plugs: 27 | ffmpeg-2404: 28 | interface: content 29 | target: ffmpeg-platform 30 | default-provider: ffmpeg-2404 31 | 32 | slots: 33 | drum-machine-mpris: 34 | interface: mpris 35 | name: drum-machine 36 | drum-machine: 37 | interface: dbus 38 | bus: session 39 | name: io.github.revisto.drum-machine 40 | 41 | parts: 42 | python-deps: 43 | plugin: python 44 | source: https://github.com/revisto/drum-machine.git 45 | source-tag: 'v2.0.0' 46 | source-depth: 1 47 | python-packages: 48 | - pygame 49 | - mido 50 | - numpy 51 | organize: 52 | bin: usr/bin 53 | lib/python3.12/site-packages: usr/lib/python3/dist-packages 54 | prime: 55 | - -usr/bin/activate* 56 | - -usr/bin/Activate.ps1 57 | - -usr/bin/python* 58 | - -usr/bin/pip* 59 | - -pyvenv.cfg 60 | - -share 61 | - -include 62 | - -lib 63 | - -lib64 64 | - -usr/lib/*/dist-packages/pip* 65 | - -usr/lib/*/dist-packages/setuptools* 66 | - -usr/lib/*/dist-packages/pkg_resources* 67 | 68 | drum-machine: 69 | after: [python-deps] 70 | plugin: meson 71 | source: https://github.com/revisto/drum-machine.git 72 | source-tag: 'v2.0.0' 73 | source-depth: 1 74 | meson-parameters: 75 | - --prefix=/snap/drum-machine/current/usr 76 | - --buildtype=release 77 | build-environment: 78 | - PYTHONPATH: $CRAFT_STAGE/usr/lib/python3/dist-packages:$PYTHONPATH 79 | build-snaps: 80 | - blueprint-compiler/latest/edge 81 | parse-info: [usr/share/metainfo/io.github.revisto.drum-machine.metainfo.xml] 82 | override-build: | 83 | craftctl default 84 | sed -e '1c#!/usr/bin/env python3' -i ${CRAFT_PART_INSTALL}/snap/drum-machine/current/usr/bin/drum-machine 85 | mkdir -p $CRAFT_PART_INSTALL/meta/gui 86 | cp -r $CRAFT_PART_INSTALL/snap/drum-machine/current/usr/share/icons $CRAFT_PART_INSTALL/meta/gui/ 87 | for i in `find $CRAFT_PART_INSTALL/meta/gui/icons -name "*.svg" -o -name "*.png"`; do 88 | mv $i "`dirname $i`/snap.$CRAFT_PROJECT_NAME.`basename $i`" 89 | done 90 | for i in `find $CRAFT_PART_INSTALL/snap/drum-machine/current/usr/share/icons -name "io.github.revisto.drum-machine*.svg" -o -name "io.github.revisto.drum-machine*.png"`; do 91 | mv $i "`dirname $i`/snap.$CRAFT_PROJECT_NAME.`basename $i`" 92 | done 93 | sed -i 's/Icon=io.github.revisto.drum-machine/Icon=snap.drum-machine.io.github.revisto.drum-machine/' $CRAFT_PART_INSTALL/snap/drum-machine/current/usr/share/applications/io.github.revisto.drum-machine.desktop 94 | organize: 95 | snap/drum-machine/current: . 96 | 97 | gpu-2404: 98 | after: [drum-machine] 99 | plugin: dump 100 | source: https://github.com/canonical/gpu-snap.git 101 | override-prime: | 102 | craftctl default 103 | ${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404 104 | prime: 105 | - -* 106 | 107 | cleanup: 108 | after: 109 | - drum-machine 110 | - gpu-2404 111 | plugin: nil 112 | build-snaps: 113 | - gtk-common-themes 114 | - gnome-46-2404 115 | - core24 116 | - blueprint-compiler 117 | 118 | apps: 119 | drum-machine: 120 | command: usr/bin/drum-machine 121 | desktop: usr/share/applications/io.github.revisto.drum-machine.desktop 122 | extensions: [gnome] 123 | common-id: io.github.revisto.drum-machine 124 | environment: 125 | PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$PYTHONPATH 126 | PATH: $SNAP/ffmpeg-platform/usr/bin:$PATH 127 | LD_LIBRARY_PATH: $SNAP/ffmpeg-platform/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$LD_LIBRARY_PATH 128 | plugs: 129 | - audio-playback 130 | -------------------------------------------------------------------------------- /src/application.py: -------------------------------------------------------------------------------- 1 | # application.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import platform 21 | import logging 22 | from typing import Optional, Callable, List 23 | import gi 24 | from gettext import gettext as _ 25 | 26 | gi.require_version("Gtk", "4.0") 27 | gi.require_version("Adw", "1") 28 | from gi.repository import Adw, Gio, Gtk 29 | from .window import DrumMachineWindow 30 | 31 | 32 | class DrumMachineApplication(Adw.Application): 33 | def __init__(self, version: str) -> None: 34 | super().__init__( 35 | application_id="io.github.revisto.drum-machine", 36 | flags=Gio.ApplicationFlags.DEFAULT_FLAGS, 37 | ) 38 | self.version = version 39 | self.create_action("about", self.on_about_action) 40 | logging.info(f"Drum Machine {version} initialized") 41 | 42 | def do_activate(self) -> None: 43 | win = self.props.active_window 44 | if not win: 45 | logging.info("Creating new window") 46 | try: 47 | win = DrumMachineWindow(application=self) 48 | except Exception as e: 49 | logging.critical(f"Failed to create window: {e}") 50 | raise 51 | win.present() 52 | 53 | def on_about_action(self, *_args) -> None: 54 | debug_info = f"Drum Machine {self.version}\n" 55 | debug_info += f"System: {platform.system()}\n" 56 | if platform.system() == "Linux": 57 | debug_info += f"Dist: {platform.freedesktop_os_release()['PRETTY_NAME']}\n" 58 | debug_info += f"Python {platform.python_version()}\n" 59 | debug_info += ( 60 | f"GTK {Gtk.MAJOR_VERSION}.{Gtk.MINOR_VERSION}.{Gtk.MICRO_VERSION}\n" 61 | ) 62 | debug_info += "PyGObject {}.{}.{}\n".format(*gi.version_info) 63 | debug_info += ( 64 | f"Adwaita {Adw.MAJOR_VERSION}.{Adw.MINOR_VERSION}.{Adw.MICRO_VERSION}" 65 | ) 66 | about = Adw.AboutDialog( 67 | application_name=_("Drum Machine"), 68 | application_icon="io.github.revisto.drum-machine", 69 | developer_name="Revisto", 70 | version=self.version, 71 | developers=["Revisto"], 72 | copyright="© 2024–2025 Revisto", 73 | comments=_( 74 | "Drum Machine is a modern and intuitive application for creating, " 75 | "playing, and managing drum patterns." 76 | ), 77 | debug_info=debug_info, 78 | license_type=Gtk.License.GPL_3_0, 79 | translator_credits=_("translator-credits"), 80 | issue_url="https://github.com/Revisto/drum-machine/issues", 81 | website="https://apps.gnome.org/DrumMachine/", 82 | ) 83 | about.add_acknowledgement_section( 84 | _("Special thanks"), ["Sepehr Rasouli", "Tobias Bernard"] 85 | ) 86 | about.add_legal_section( 87 | _("Sounds"), 88 | _("The drum samples used in this application are from {link}.").format( 89 | link="99Sounds" 90 | ), 91 | Gtk.License.UNKNOWN, 92 | ) 93 | about.add_legal_section("Mido", None, Gtk.License.MIT_X11) 94 | about.add_legal_section("Pygame", None, Gtk.License.LGPL_2_1) 95 | about.present(self.props.active_window) 96 | 97 | def create_action( 98 | self, name: str, callback: Callable, shortcuts: Optional[List[str]] = None 99 | ) -> None: 100 | action = Gio.SimpleAction.new(name, None) 101 | action.connect("activate", callback) 102 | self.add_action(action) 103 | if shortcuts: 104 | self.set_accels_for_action(f"app.{name}", shortcuts) 105 | -------------------------------------------------------------------------------- /src/services/ui_helper.py: -------------------------------------------------------------------------------- 1 | # services/ui_helper.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import logging 21 | from typing import Dict 22 | 23 | 24 | class UIHelper: 25 | def __init__(self, window) -> None: 26 | self.window = window 27 | 28 | @property 29 | def beats_per_page(self) -> int: 30 | """Get the current number of beats per page from the grid builder.""" 31 | return self.window.drum_machine_service.beats_per_page 32 | 33 | def _set_playhead_highlight_for_beat( 34 | self, beat_index: int, highlight_on: bool 35 | ) -> None: 36 | """ 37 | Internal helper to add or remove the 'playhead-active' CSS class 38 | for a vertical column of toggles at a specific beat index. 39 | """ 40 | for part in self.window.sound_service.drum_part_manager.get_all_parts(): 41 | try: 42 | toggle = getattr(self.window, f"{part.id}_toggle_{beat_index}") 43 | if highlight_on: 44 | toggle.get_style_context().add_class("toggle-active") 45 | else: 46 | toggle.get_style_context().remove_class("toggle-active") 47 | except AttributeError: 48 | logging.debug( 49 | f"Toggle not found for playhead highlight: " 50 | f"{part.id}_toggle_{beat_index}" 51 | ) 52 | continue 53 | 54 | def highlight_playhead_at_beat(self, beat_index: int) -> None: 55 | self._set_playhead_highlight_for_beat(beat_index, highlight_on=True) 56 | 57 | def remove_playhead_highlight_at_beat(self, beat_index: int) -> None: 58 | self._set_playhead_highlight_for_beat(beat_index, highlight_on=False) 59 | 60 | def clear_all_playhead_highlights(self) -> None: 61 | """Removes all playhead highlights from the currently visible toggles.""" 62 | # This is inefficient and will be slow with many pages. 63 | # It should ideally track the last highlighted beat and only clear that one. 64 | for i in range(self.beats_per_page * self.window.carousel.get_n_pages()): 65 | self._set_playhead_highlight_for_beat(i, highlight_on=False) 66 | 67 | def deactivate_all_toggles_in_ui(self) -> None: 68 | """Sets the state of all currently rendered toggles to inactive (OFF).""" 69 | total_toggles = self.beats_per_page * self.window.carousel.get_n_pages() 70 | for part in self.window.sound_service.drum_part_manager.get_all_parts(): 71 | for i in range(total_toggles): 72 | try: 73 | toggle = getattr(self.window, f"{part.id}_toggle_{i}") 74 | if toggle.get_active(): 75 | toggle.set_active(False) 76 | except AttributeError: 77 | logging.debug( 78 | f"Toggle not found for deactivation: {part.id}_toggle_{i}" 79 | ) 80 | continue 81 | 82 | def load_pattern_into_ui( 83 | self, drum_parts_state: Dict[str, Dict[int, bool]] 84 | ) -> None: 85 | """ 86 | Updates the UI to reflect a new pattern. 87 | This is fundamentally broken with the dynamic grid and needs a redesign. 88 | The UI should pull data when it's created, not have data pushed to it. 89 | """ 90 | for part_id in drum_parts_state: 91 | for beat_index in drum_parts_state[part_id].keys(): 92 | toggle = getattr(self.window, f"{part_id}_toggle_{beat_index}") 93 | toggle.set_active(True) 94 | 95 | def set_bpm_in_ui(self, bpm_value: float) -> None: 96 | """Updates the BPM spin button with a new value.""" 97 | self.window.bpm_spin_button.set_value(bpm_value) 98 | 99 | def scroll_carousel_to_page(self, page_index: int) -> None: 100 | """Delegates the request to scroll the carousel to the main window.""" 101 | self.window.scroll_carousel_to_page(page_index) 102 | -------------------------------------------------------------------------------- /src/services/file_encoder.py: -------------------------------------------------------------------------------- 1 | # services/file_encoder.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import os 21 | import subprocess 22 | import logging 23 | from ..config.export_formats import ExportFormatRegistry 24 | 25 | 26 | class AudioEncoder: 27 | """Handles encoding audio data to various file formats""" 28 | 29 | def __init__(self, format_registry: ExportFormatRegistry): 30 | self.format_registry = format_registry 31 | 32 | def encode_to_file( 33 | self, audio_data, sample_rate, file_path, metadata=None, export_task=None 34 | ): 35 | """Encode audio data to the specified file format""" 36 | file_ext = os.path.splitext(file_path)[1].lower() 37 | format_info = self.format_registry.get_format_by_extension(file_ext) 38 | 39 | # WAV doesn't support metadata 40 | if not format_info.supports_metadata: 41 | metadata = None 42 | 43 | self._encode_with_ffmpeg( 44 | audio_data, sample_rate, file_path, metadata, export_task 45 | ) 46 | 47 | def _encode_with_ffmpeg( 48 | self, audio_data, sample_rate, file_path, metadata=None, export_task=None 49 | ): 50 | """Use ffmpeg to encode audio data""" 51 | cmd = [ 52 | "ffmpeg", 53 | "-y", # Overwrite output files 54 | "-f", 55 | "f32le", # Input format: 32-bit float little endian 56 | "-ar", 57 | str(sample_rate), # Sample rate 58 | "-ac", 59 | "2", # Stereo 60 | "-i", 61 | "-", # Read from stdin 62 | ] 63 | 64 | # Add cover art if provided 65 | has_cover = self._has_valid_cover_art(metadata) 66 | if has_cover: 67 | cmd.extend(["-i", metadata["cover_art"]]) 68 | 69 | # Map audio stream 70 | cmd.extend(["-map", "0:a"]) 71 | 72 | # Map cover art if present 73 | if has_cover: 74 | cmd.extend(["-map", "1:v", "-disposition:v:0", "attached_pic"]) 75 | 76 | # Add metadata tags 77 | self._add_metadata_to_command(cmd, metadata) 78 | 79 | cmd.append(file_path) 80 | 81 | # Start the subprocess and store reference in export_task 82 | process = subprocess.Popen( 83 | cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE 84 | ) 85 | 86 | if export_task: 87 | export_task.current_process = process 88 | 89 | # Check for cancellation before starting 90 | if export_task and export_task.is_cancelled: 91 | process.terminate() 92 | return 93 | 94 | try: 95 | stdout, stderr = process.communicate(input=audio_data.tobytes()) 96 | if process.returncode != 0: 97 | error_msg = stderr.decode() if stderr else "Unknown error" 98 | logging.error(f"FFmpeg encoding failed: {error_msg}") 99 | raise subprocess.CalledProcessError( 100 | process.returncode, cmd, stdout, stderr 101 | ) 102 | except subprocess.CalledProcessError as e: 103 | logging.error(f"Audio encoding failed for {file_path}: {e}") 104 | raise 105 | finally: 106 | if export_task: 107 | export_task.current_process = None 108 | 109 | def _has_valid_cover_art(self, metadata): 110 | """Check if metadata contains valid cover art""" 111 | return ( 112 | metadata 113 | and metadata.get("cover_art") 114 | and os.path.exists(metadata["cover_art"]) 115 | ) 116 | 117 | def _add_metadata_to_command(self, cmd, metadata): 118 | """Add metadata tags to the ffmpeg command""" 119 | if not metadata: 120 | return 121 | 122 | if metadata.get("title"): 123 | cmd.extend(["-metadata", f'title={metadata["title"]}']) 124 | if metadata.get("artist"): 125 | cmd.extend(["-metadata", f'artist={metadata["artist"]}']) 126 | -------------------------------------------------------------------------------- /python-dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-dependencies", 3 | "buildsystem": "simple", 4 | "build-commands": [], 5 | "modules": [ 6 | { 7 | "name": "python3-pygame", 8 | "buildsystem": "simple", 9 | "build-commands": [ 10 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pygame\"" 11 | ], 12 | "sources": [ 13 | { 14 | "type": "file", 15 | "url": "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", 16 | "sha256": "56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f" 17 | }, 18 | { 19 | "type": "file", 20 | "url": "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", 21 | "sha256": "062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922" 22 | } 23 | ] 24 | }, 25 | { 26 | "name": "python3-mido", 27 | "buildsystem": "simple", 28 | "build-commands": [ 29 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"mido\"" 30 | ], 31 | "sources": [ 32 | { 33 | "type": "file", 34 | "url": "https://files.pythonhosted.org/packages/fd/28/45deb15c11859d2f10702b32e71de9328a9fa494f989626916db39a9617f/mido-1.3.3-py3-none-any.whl", 35 | "sha256": "01033c9b10b049e4436fca2762194ca839b09a4334091dd3c34e7f4ae674fd8a" 36 | }, 37 | { 38 | "type": "file", 39 | "url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", 40 | "sha256": "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "python3-setuptools-scm", 46 | "buildsystem": "simple", 47 | "build-commands": [ 48 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"setuptools-scm\"" 49 | ], 50 | "sources": [ 51 | { 52 | "type": "file", 53 | "url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", 54 | "sha256": "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" 55 | }, 56 | { 57 | "type": "file", 58 | "url": "https://files.pythonhosted.org/packages/f7/14/dd3a6053325e882fe191fb4b42289bbdfabf5f44307c302903a8a3236a0a/setuptools_scm-9.2.0-py3-none-any.whl", 59 | "sha256": "c551ef54e2270727ee17067881c9687ca2aedf179fa5b8f3fab9e8d73bdc421f" 60 | } 61 | ] 62 | }, 63 | { 64 | "name": "python3-packaging", 65 | "buildsystem": "simple", 66 | "build-commands": [ 67 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"packaging\"" 68 | ], 69 | "sources": [ 70 | { 71 | "type": "file", 72 | "url": "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", 73 | "sha256": "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "python3-numpy", 79 | "buildsystem": "simple", 80 | "build-commands": [ 81 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"numpy\"" 82 | ], 83 | "sources": [ 84 | { 85 | "type": "file", 86 | "url": "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", 87 | "sha256": "8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73" 88 | }, 89 | { 90 | "type": "file", 91 | "url": "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", 92 | "sha256": "572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370" 93 | } 94 | ] 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [license-url]: https://github.com/revisto/drum-machine/blob/master/COPYING 2 | [license-image]: https://img.shields.io/github/license/revisto/drum-machine.svg?style=for-the-badge 3 | [flathub-url]: https://flathub.org/apps/io.github.revisto.drum-machine 4 | [flathub-image]: https://img.shields.io/flathub/v/io.github.revisto.drum-machine?logo=flathub&style=for-the-badge 5 | [installs-image]: https://img.shields.io/flathub/downloads/io.github.revisto.drum-machine?style=for-the-badge 6 | [issues-url]: https://github.com/revisto/drum-machine/issues 7 | [issues-image]: https://img.shields.io/github/issues/revisto/drum-machine?style=for-the-badge 8 | [persian-gnome-badge]: https://gnome-fa.github.io/assets/badges/persian-gnome.svg 9 | [persian-gnome-url]: https://gnome_fa.t.me/ 10 | 11 | [circle-url]: https://apps.gnome.org/DrumMachine/ 12 | [circle-image]: https://circle.gnome.org/assets/button/badge.svg 13 | 14 |
15 | 16 | 17 | # Drum Machine 18 | 19 | **Create and play drum beats** 20 | 21 | [![GNOME Circle][circle-image]][circle-url] [![Persian GNOME][persian-gnome-badge]][persian-gnome-url] 22 | 23 | [![License][license-image]][license-url] 24 | [![Flathub][flathub-image]][flathub-url] 25 | [![Issues][issues-image]][issues-url] 26 | [![Installs][installs-image]][flathub-url] 27 | 28 | 29 | 30 |
31 | 32 | ## Description 33 | Drum Machine is a modern and intuitive application for creating, playing, and managing drum patterns. Perfect for musicians, producers, and anyone interested in rhythm creation, this application provides a simple interface for drum pattern programming. 34 | 35 | ## Features 36 | - Intuitive grid-based pattern editor with **infinite pages** 37 | - Navigate through unlimited pattern pages using carousel interface 38 | - Create complex drum sequences across multiple pages 39 | - **Custom samples with MIDI mapping** 40 | - Add your own drum sounds 41 | - Map samples to specific MIDI notes for export 42 | - Adjustable BPM control 43 | - Volume control for overall mix 44 | - Save and load patterns 45 | - Multiple drum sounds including kick, snare, hi-hat, and more 46 | - **Audio & MIDI export** 47 | - Export patterns in WAV, FLAC, Ogg Vorbis, and MP3 formats 48 | - Export as MIDI files with proper note mapping 49 | - Add artist name, song title, and cover art metadata 50 | - Configurable pattern repeat count for longer exports 51 | - Keyboard shortcuts for quick access to all functions 52 | - Modern GTK4 and libadwaita interface 53 | 54 | ## Install 55 | 56 | 57 | Download on Flathub 58 | 59 | 60 | ### Build from source 61 | 62 | You can clone and run from GNOME Builder. 63 | 64 | ## Contribute 65 | We need your help to make Drum Machine better! 66 | There are lots of features that can be added, and we would love to see your contributions. 67 | 68 | If you want to contribute to this project, you can fork the repository and submit a pull request. You can also report a bug or request a feature by opening an issue. 69 | 70 | Your contributions are extremely welcome and appreciated. 71 | 72 | ## Translations 73 | Drum Machine uses [GNOME Damned Lies](https://l10n.gnome.org/) for translation management. If you'd like to contribute translations, please visit the [Drum Machine translation page](https://l10n.gnome.org/module/drum-machine/) on GNOME Damned Lies rather than submitting pull requests with translation files. 74 | 75 | ## Credits 76 | Developed by **[Revisto](https://github.com/revisto)** 77 | 78 | Special thanks to **[Sepehr](https://github.com/sepehr-rs)** for triaging issues and helping maintain the project. 79 | 80 | Special thanks to **[Tobias Bernard](https://tobiasbernard.com)** from the GNOME Circle Committee for helping Drum Machine look the way it does now and for all his valuable contributions. 81 | 82 | Thanks to all contributors who help improve Drum Machine through code, bug reports, and feature requests. 83 | 84 | Thanks to everyone contributing translations on [GNOME Damned Lies](https://l10n.gnome.org/module/drum-machine/), your work makes Drum Machine accessible to users worldwide. 85 | 86 |

87 | 88 | 89 | 90 | 91 | 92 | 93 |

94 | 95 | ## ❤️ Sponsor this project 96 | Drum Machine is free software. If you like it and would like to support and fund it, you may donate through one of the platforms listed in the GitHub Sponsor section. Any amount will be greatly appreciated 🤩. 97 | 98 | ## License 99 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 100 | 101 | ## Code of Conduct 102 | We follow the [GNOME Code of Conduct](https://wiki.gnome.org/Foundation/CodeOfConduct) to ensure a welcoming environment for everyone. Be kind, be respectful, and help us build something awesome and fun together. 103 | -------------------------------------------------------------------------------- /src/services/preset_service.py: -------------------------------------------------------------------------------- 1 | # services/pattern_service.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import mido 21 | import itertools 22 | 23 | 24 | class PatternService: 25 | def __init__(self, window): 26 | self.window = window 27 | 28 | def _get_midi_note_for_part(self, part_id): 29 | """Get MIDI note ID for a drum part""" 30 | drum_part = self.window.sound_service.drum_part_manager.get_part_by_id(part_id) 31 | if drum_part and drum_part.midi_note_id is not None: 32 | return drum_part.midi_note_id 33 | return 0 34 | 35 | def _get_part_id_for_midi_note(self, note): 36 | """Get drum part ID for a MIDI note, creating a temporary part if needed""" 37 | manager = self.window.sound_service.drum_part_manager 38 | drum_part = manager.get_or_create_part_for_midi_note(note) 39 | return drum_part.id 40 | 41 | def save_pattern(self, file_path, drum_parts, bpm): 42 | mid = mido.MidiFile() 43 | track = mido.MidiTrack() 44 | mid.tracks.append(track) 45 | 46 | track.append(mido.MetaMessage("set_tempo", tempo=mido.bpm2tempo(bpm))) 47 | 48 | # 1. Collect all active notes from all pages into a list 49 | events = [] 50 | for part, notes in drum_parts.items(): 51 | for beat_index, is_active in notes.items(): 52 | if is_active: 53 | note = self._get_midi_note_for_part(part) 54 | if note != 0: 55 | # Store the note and its absolute beat index 56 | events.append({"note": note, "beat": beat_index}) 57 | 58 | # 2. Sort events by beat to process them in chronological order 59 | events.sort(key=lambda e: e["beat"]) 60 | 61 | ticks_per_beat = mid.ticks_per_beat 62 | last_time_in_ticks = 0 63 | note_duration_ticks = ticks_per_beat // 4 # 16th note duration 64 | 65 | # 3. Group events by beat to handle chords correctly 66 | for beat, group in itertools.groupby(events, key=lambda e: e["beat"]): 67 | notes_in_chord = [event["note"] for event in group] 68 | 69 | # Calculate time for this beat/chord 70 | absolute_time_in_ticks = int(beat * ticks_per_beat / 4) 71 | delta_time = absolute_time_in_ticks - last_time_in_ticks 72 | 73 | # Add all note_on messages for the chord 74 | # The first note carries the delta_time, subsequent notes have time=0 75 | is_first_note = True 76 | for note in notes_in_chord: 77 | d_time = delta_time if is_first_note else 0 78 | track.append( 79 | mido.Message("note_on", note=note, velocity=100, time=d_time) 80 | ) 81 | is_first_note = False 82 | 83 | # Add all note_off messages for the chord 84 | # The first note_off has the duration, subsequent ones have time=0 85 | is_first_note = True 86 | for note in notes_in_chord: 87 | d_time = note_duration_ticks if is_first_note else 0 88 | track.append( 89 | mido.Message("note_off", note=note, velocity=0, time=d_time) 90 | ) 91 | is_first_note = False 92 | 93 | # Update the time of the last event 94 | last_time_in_ticks = absolute_time_in_ticks + note_duration_ticks 95 | 96 | mid.save(file_path) 97 | 98 | def load_pattern(self, file_path): 99 | mid = mido.MidiFile(file_path) 100 | drum_parts_state = ( 101 | self.window.drum_machine_service.create_empty_drum_parts_state() 102 | ) 103 | bpm = 120 104 | 105 | ticks_per_beat = mid.ticks_per_beat 106 | if ticks_per_beat is None: 107 | ticks_per_beat = 480 # A common default 108 | 109 | for track in mid.tracks: 110 | absolute_time_in_ticks = 0 111 | for msg in track: 112 | # Keep a running total of the absolute time by adding the delta times 113 | absolute_time_in_ticks += msg.time 114 | if msg.type == "set_tempo": 115 | bpm = mido.tempo2bpm(msg.tempo) 116 | elif msg.type == "note_on" and msg.velocity > 0: 117 | part_id = self._get_part_id_for_midi_note(msg.note) 118 | 119 | # Initialize part in state if not already present 120 | if part_id not in drum_parts_state: 121 | drum_parts_state[part_id] = {} 122 | 123 | # Convert absolute time in ticks back to a beat index 124 | # assuming 16th notes 125 | ticks_per_16th_note = ticks_per_beat / 4.0 126 | beat_index = int( 127 | round(absolute_time_in_ticks / ticks_per_16th_note) 128 | ) 129 | drum_parts_state[part_id][beat_index] = True 130 | 131 | return drum_parts_state, bpm 132 | -------------------------------------------------------------------------------- /src/services/audio_export_service.py: -------------------------------------------------------------------------------- 1 | # services/audio_export_service.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import os 21 | import numpy as np 22 | import subprocess 23 | import logging 24 | 25 | from ..utils.export_progress import ExportPhase 26 | from ..config.export_formats import ExportFormatRegistry 27 | from ..services.audio_renderer import AudioRenderer 28 | from ..services.file_encoder import AudioEncoder 29 | 30 | 31 | class SampleLoader: 32 | """Handles loading of drum samples""" 33 | 34 | def __init__(self, sample_rate=44100): 35 | self.sample_rate = sample_rate 36 | self.samples = {} 37 | 38 | def load_samples(self, drum_parts): 39 | """Load all drum samples into memory from drum parts""" 40 | self.samples = {} 41 | for part in drum_parts: 42 | if os.path.exists(part.file_path): 43 | try: 44 | audio_data = self._load_sample(part.file_path) 45 | self.samples[part.id] = audio_data 46 | except Exception as e: 47 | logging.warning(f"Could not load {part.file_path}: {e}") 48 | self.samples[part.id] = np.zeros((1000, 2), dtype="float32") 49 | 50 | def clear_samples(self): 51 | """Clear loaded samples from memory""" 52 | self.samples = {} 53 | 54 | def _load_sample(self, sample_path): 55 | """Load a single audio sample using ffmpeg""" 56 | cmd = [ 57 | "ffmpeg", 58 | "-i", 59 | sample_path, 60 | "-f", 61 | "f32le", 62 | "-ac", 63 | "2", # Convert to stereo 64 | "-ar", 65 | str(self.sample_rate), 66 | "-", 67 | ] 68 | result = subprocess.run(cmd, capture_output=True, check=True) 69 | audio_data = np.frombuffer(result.stdout, dtype=np.float32) 70 | return audio_data.reshape(-1, 2) 71 | 72 | def get_samples(self): 73 | """Get the loaded samples dictionary""" 74 | return self.samples 75 | 76 | 77 | class AudioExportService: 78 | """Handles audio export functionality with progress tracking""" 79 | 80 | def __init__(self, window): 81 | self.window = window 82 | self.sample_rate = 44100 83 | 84 | # Initialize components (samples loaded lazily during export) 85 | self.sample_loader = SampleLoader(self.sample_rate) 86 | self.audio_renderer = AudioRenderer({}, self.sample_rate) 87 | self.format_registry = ExportFormatRegistry() 88 | self.audio_encoder = AudioEncoder(self.format_registry) 89 | 90 | def export_audio( 91 | self, 92 | drum_parts_state, 93 | bpm, 94 | file_path, 95 | progress_callback, 96 | repeat_count=1, 97 | metadata=None, 98 | export_task=None, 99 | ): 100 | """ 101 | Export drum pattern to audio file 102 | 103 | Args: 104 | drum_parts_state: Current drum pattern state 105 | bpm: Beats per minute 106 | file_path: Output file path 107 | progress_callback: Callback function for progress updates 108 | repeat_count: Number of times to repeat the pattern 109 | metadata: Dict with artist, title, and cover_art keys 110 | """ 111 | try: 112 | progress_callback(ExportPhase.PREPARING) 113 | 114 | # Load samples fresh from current drum parts (ensures latest files) 115 | drum_parts = self.window.sound_service.drum_part_manager.get_all_parts() 116 | self.sample_loader.load_samples(drum_parts) 117 | # Update the renderer with the loaded samples 118 | self.audio_renderer.update_samples(self.sample_loader.get_samples()) 119 | 120 | self._validate_pattern(drum_parts_state) 121 | 122 | progress_callback(ExportPhase.RENDERING) 123 | total_beats = self.window.drum_machine_service.total_beats 124 | audio_buffer = self.audio_renderer.render_pattern( 125 | drum_parts_state, bpm, total_beats, repeat_count 126 | ) 127 | 128 | progress_callback(ExportPhase.SAVING) 129 | self.audio_encoder.encode_to_file( 130 | audio_buffer.buffer, self.sample_rate, file_path, metadata, export_task 131 | ) 132 | 133 | # Clear samples from memory after export 134 | self.sample_loader.clear_samples() 135 | self.audio_renderer.clear_samples() 136 | 137 | logging.info(f"Audio exported successfully to {file_path}") 138 | return True 139 | 140 | except ValueError as e: 141 | logging.warning(f"Export validation failed: {e}") 142 | # Clear samples even on error 143 | self.sample_loader.clear_samples() 144 | self.audio_renderer.clear_samples() 145 | raise 146 | except Exception as e: 147 | logging.error(f"Export failed: {e}") 148 | # Clear samples even on error 149 | self.sample_loader.clear_samples() 150 | self.audio_renderer.clear_samples() 151 | raise 152 | 153 | def _validate_pattern(self, drum_parts_state): 154 | """Validate that the pattern has active beats""" 155 | has_beats = any( 156 | any(part_state.values()) for part_state in drum_parts_state.values() 157 | ) 158 | if not has_beats: 159 | raise ValueError("No active beats in pattern") 160 | -------------------------------------------------------------------------------- /data/io.github.revisto.drum-machine.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.revisto.drum-machine 4 | CC0-1.0 5 | GPL-3.0-or-later 6 | Drum Machine 7 | Create and play drum beats 8 | 9 |

10 | Drum Machine is a modern and intuitive application for creating, playing, and 11 | managing drum patterns. Perfect for musicians, producers, and anyone interested 12 | in rhythm creation, this application provides a simple interface for drum 13 | pattern programming. Have fun! 14 |

15 |

Features:

16 |
    17 |
  • Intuitive grid-based pattern editor with infinite pages
  • 18 |
  • Custom samples: Add your own drum sounds with MIDI note mapping
  • 19 |
  • Adjustable BPM control
  • 20 |
  • Volume control for overall mix
  • 21 |
  • Save and load patterns
  • 22 |
  • Multiple drum sounds including kick, snare, hi-hat, and more
  • 23 |
  • Audio and MIDI export with support for WAV, FLAC, Ogg, MP3, and MIDI formats
  • 24 |
  • Metadata support for embedding artist name, song title, and cover art
  • 25 |
  • Keyboard shortcuts for quick access to all functions
  • 26 |
27 |
28 | 29 | #eac8ff 30 | #9d25ff 31 | 32 | 33 | 34 | Drum Pattern Loaded in Light Mode 35 | https://github.com/revisto/drum-machine/raw/master/data/screenshots/drum-machine-light.png 36 | 37 | 38 | Drum Pattern Loaded in Dark Mode 39 | https://github.com/revisto/drum-machine/raw/master/data/screenshots/drum-machine-dark.png 40 | 41 | 42 | drum-machine 43 | io.github.revisto.drum-machine.desktop 44 | https://github.com/revisto/drum-machine 45 | https://github.com/revisto/drum-machine/issues 46 | https://github.com/revisto/drum-machine 47 | mailto:therevisto@gmail.com 48 | 49 | Revisto 50 | therevisto@gmail.com 51 | 52 | 53 | 54 | 55 |

Drum Machine 2.0.0 is a major release with custom samples! 🎶

56 |
    57 |
  • Custom Samples: Add your own drum sounds and map them to specific MIDI notes for export
  • 58 |
  • MIDI Export: Export your patterns as MIDI files with proper note mapping
  • 59 |
  • Pattern Management: Presets have been refactored to a more flexible pattern system
  • 60 |
  • Bug Fixes: Sounds now stop immediately when pausing, disabled export when pattern is empty
  • 61 |
  • New Translations: Added Catalan, Greek, and Uzbek languages
  • 62 |
  • Translation Updates: Updated Slovenian, Basque, Ukrainian, Georgian, Chinese, Persian, and many more
  • 63 |
64 |
65 |
66 | 67 | 68 |

Drum Machine 1.5.0 introduces major audio export capabilities! 🎵

69 |
    70 |
  • Audio Export Feature: Export your drum patterns to WAV, FLAC, Ogg Vorbis, and MP3 formats
  • 71 |
  • Metadata Support: Embed artist name, song title, and cover art in exported files
  • 72 |
  • Repeat Pattern: Set how many times your beat repeats in the exported audio
  • 73 |
  • Bug Fix: Fixed issue where multiple drum sounds weren't playing simultaneously
  • 74 |
  • Translation Updates: Updated Spanish, Russian, Georgian, Chinese, Hungarian, Swedish, and more
  • 75 |
76 |
77 |
78 | 79 | 80 |

Drum Machine 1.4.0 is here with some good updates! 🥁

81 |
    82 |
  • More pages for longer, more complex beats, because sometimes you need more than 16 steps!
  • 83 |
  • Mobile-friendly improvements, now your drum machine works better on phones (with showing half of the grid)
  • 84 |
  • Goes global! Now translated into 17 languages including Chinese, Russian, Arabic, Hebrew, and many more
  • 85 |
86 |
87 |
88 | 89 | 90 |

This release of Drum Machine 1.3.1 brings significant UI and functionality improvements:

91 |
    92 |
  • Refactored headerbar and background styling for a flat, integrated visual experience.
  • 93 |
  • Enhanced accessibility with improved tooltips and descriptive labels for BPM and volume controls.
  • 94 |
  • Redesigned app icon and updated text styling for clearer user interface elements.
  • 95 |
  • Optimized layout breakpoints to improve responsiveness and reduce early scrolling.
  • 96 |
  • Fixed padding, border-radius, and vertical alignment issues across multiple controls.
  • 97 |
  • Updated file dialogs to use the Nautilus file chooser for a more modern approach.
  • 98 |
  • Improved keyboard navigation focus and updated tooltips, including dedicated play and pause hints.
  • 99 |
  • Transitioned to the Nautilus file chooser for a more contemporary interface.
  • 100 |
  • Now accessible on narrow mobile screens with adaptive UI elements.
  • 101 |
  • Refined app icon, and adjusted brand colors for better contrast.
  • 102 |
103 |
104 |
105 |
106 | 107 | keyboard 108 | pointing 109 | touch 110 | 111 | 112 | 360 113 | offline-only 114 | 115 | 116 |
-------------------------------------------------------------------------------- /src/services/pattern_service.py: -------------------------------------------------------------------------------- 1 | # services/pattern_service.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import mido 21 | import itertools 22 | import logging 23 | from typing import Dict, Tuple 24 | 25 | 26 | class PatternService: 27 | def __init__(self, window) -> None: 28 | self.window = window 29 | 30 | def _get_midi_note_for_part(self, part_id: str) -> int: 31 | """Get MIDI note ID for a drum part""" 32 | drum_part = self.window.sound_service.drum_part_manager.get_part_by_id(part_id) 33 | if drum_part and drum_part.midi_note_id is not None: 34 | return drum_part.midi_note_id 35 | return 0 36 | 37 | def _get_part_id_for_midi_note(self, note: int) -> str: 38 | """Get drum part ID for a MIDI note, creating a temporary part if needed""" 39 | manager = self.window.sound_service.drum_part_manager 40 | drum_part = manager.get_or_create_part_for_midi_note(note) 41 | return drum_part.id 42 | 43 | def save_pattern( 44 | self, file_path: str, drum_parts: Dict[str, Dict[int, bool]], bpm: float 45 | ) -> None: 46 | try: 47 | mid = mido.MidiFile() 48 | track = mido.MidiTrack() 49 | mid.tracks.append(track) 50 | 51 | track.append(mido.MetaMessage("set_tempo", tempo=mido.bpm2tempo(bpm))) 52 | 53 | # 1. Collect all active notes from all pages into a list 54 | events = [] 55 | for part, notes in drum_parts.items(): 56 | for beat_index, is_active in notes.items(): 57 | if is_active: 58 | note = self._get_midi_note_for_part(part) 59 | if note != 0: 60 | # Store the note and its absolute beat index 61 | events.append({"note": note, "beat": beat_index}) 62 | 63 | # 2. Sort events by beat to process them in chronological order 64 | events.sort(key=lambda e: e["beat"]) 65 | 66 | ticks_per_beat = mid.ticks_per_beat 67 | last_time_in_ticks = 0 68 | note_duration_ticks = ticks_per_beat // 4 # 16th note duration 69 | 70 | # 3. Group events by beat to handle chords correctly 71 | for beat, group in itertools.groupby(events, key=lambda e: e["beat"]): 72 | notes_in_chord = [event["note"] for event in group] 73 | 74 | # Calculate time for this beat/chord 75 | absolute_time_in_ticks = int(beat * ticks_per_beat / 4) 76 | delta_time = absolute_time_in_ticks - last_time_in_ticks 77 | 78 | # Add all note_on messages for the chord 79 | # The first note carries the delta_time, subsequent notes have time=0 80 | is_first_note = True 81 | for note in notes_in_chord: 82 | d_time = delta_time if is_first_note else 0 83 | track.append( 84 | mido.Message("note_on", note=note, velocity=100, time=d_time) 85 | ) 86 | is_first_note = False 87 | 88 | # Add all note_off messages for the chord 89 | # The first note_off has the duration, subsequent ones have time=0 90 | is_first_note = True 91 | for note in notes_in_chord: 92 | d_time = note_duration_ticks if is_first_note else 0 93 | track.append( 94 | mido.Message("note_off", note=note, velocity=0, time=d_time) 95 | ) 96 | is_first_note = False 97 | 98 | # Update the time of the last event 99 | last_time_in_ticks = absolute_time_in_ticks + note_duration_ticks 100 | 101 | mid.save(file_path) 102 | logging.info(f"Pattern saved successfully to {file_path}") 103 | except Exception as e: 104 | logging.error(f"Failed to save pattern to {file_path}: {e}") 105 | raise 106 | 107 | def load_pattern(self, file_path: str) -> Tuple[Dict[str, Dict[int, bool]], float]: 108 | try: 109 | mid = mido.MidiFile(file_path) 110 | drum_parts_state = ( 111 | self.window.drum_machine_service.create_empty_drum_parts_state() 112 | ) 113 | bpm = 120 114 | 115 | ticks_per_beat = mid.ticks_per_beat 116 | if ticks_per_beat is None: 117 | ticks_per_beat = 480 # A common default 118 | 119 | for track in mid.tracks: 120 | absolute_time_in_ticks = 0 121 | for msg in track: 122 | # Keep a running total of the absolute time 123 | # by adding the delta times 124 | absolute_time_in_ticks += msg.time 125 | if msg.type == "set_tempo": 126 | bpm = mido.tempo2bpm(msg.tempo) 127 | elif msg.type == "note_on" and msg.velocity > 0: 128 | part_id = self._get_part_id_for_midi_note(msg.note) 129 | 130 | # Initialize part in state if not already present 131 | if part_id not in drum_parts_state: 132 | drum_parts_state[part_id] = {} 133 | 134 | # Convert absolute time in ticks back to a beat index 135 | # assuming 16th notes 136 | ticks_per_16th_note = ticks_per_beat / 4.0 137 | beat_index = int( 138 | round(absolute_time_in_ticks / ticks_per_16th_note) 139 | ) 140 | drum_parts_state[part_id][beat_index] = True 141 | 142 | logging.info(f"Pattern loaded successfully from {file_path}") 143 | return drum_parts_state, bpm 144 | except Exception as e: 145 | logging.error(f"Failed to load pattern from {file_path}: {e}") 146 | raise 147 | -------------------------------------------------------------------------------- /src/utils/export_progress.py: -------------------------------------------------------------------------------- 1 | # utils/export_progress.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import threading 21 | import time 22 | import logging 23 | from enum import Enum 24 | from typing import Optional 25 | from gi.repository import GLib, Gtk, Adw 26 | from gettext import gettext as _ 27 | from ..config.constants import PULSE_INTERVAL_SECONDS 28 | 29 | 30 | class ExportPhase(Enum): 31 | PREPARING = "preparing" 32 | RENDERING = "rendering" 33 | SAVING = "saving" 34 | 35 | 36 | class ExportProgressHandler: 37 | """Handles export progress updates and UI thread coordination""" 38 | 39 | def __init__( 40 | self, 41 | progress_bar: Gtk.ProgressBar, 42 | status_overlay: Adw.ToastOverlay, 43 | status_label: Gtk.Label, 44 | detail_label: Gtk.Label, 45 | ) -> None: 46 | self.progress_bar = progress_bar 47 | self.status_overlay = status_overlay 48 | self.status_label = status_label 49 | self.detail_label = detail_label 50 | 51 | self.pulse_thread: Optional[threading.Thread] = None 52 | self.pulse_stop_event: Optional[threading.Event] = None 53 | self.is_active: bool = False 54 | 55 | def start_progress_tracking(self) -> None: 56 | """Start showing progress UI and pulse updates""" 57 | self.is_active = True 58 | self.progress_bar.set_visible(True) 59 | self.status_overlay.set_visible(True) 60 | self._start_pulse_thread() 61 | 62 | def stop_progress_tracking(self): 63 | """Stop progress tracking and hide UI""" 64 | self.is_active = False 65 | self.progress_bar.set_visible(False) 66 | self.status_overlay.set_visible(False) 67 | self._stop_pulse_thread() 68 | 69 | def update_phase(self, phase: ExportPhase): 70 | """Update the progress UI for the current export phase""" 71 | 72 | def update_ui(): 73 | self.progress_bar.pulse() 74 | 75 | if phase == ExportPhase.PREPARING: 76 | self.status_label.set_label(_("Preparing…")) 77 | self.detail_label.set_label(_("Initializing…")) 78 | elif phase == ExportPhase.RENDERING: 79 | self.status_label.set_label(_("Rendering audio…")) 80 | self.detail_label.set_label(_("Processing beats…")) 81 | elif phase == ExportPhase.SAVING: 82 | self.status_label.set_label(_("Saving file…")) 83 | self.detail_label.set_label(_("Writing to disk…")) 84 | else: 85 | self.status_label.set_label(_("Exporting…")) 86 | self.detail_label.set_label(_("Processing…")) 87 | 88 | GLib.idle_add(update_ui) 89 | 90 | def _start_pulse_thread(self): 91 | """Start background thread for progress bar pulsing""" 92 | if self.pulse_thread and self.pulse_thread.is_alive(): 93 | return 94 | 95 | self.pulse_stop_event = threading.Event() 96 | 97 | def pulse_worker(): 98 | while not self.pulse_stop_event.is_set() and self.is_active: 99 | GLib.idle_add(self.progress_bar.pulse) 100 | time.sleep(PULSE_INTERVAL_SECONDS) 101 | 102 | self.pulse_thread = threading.Thread(target=pulse_worker, daemon=True) 103 | self.pulse_thread.start() 104 | 105 | def _stop_pulse_thread(self): 106 | """Stop the pulse thread""" 107 | if self.pulse_stop_event: 108 | self.pulse_stop_event.set() 109 | if self.pulse_thread and self.pulse_thread.is_alive(): 110 | self.pulse_thread.join(timeout=1.1) 111 | 112 | 113 | class ExportTask: 114 | """Manages the background export operation""" 115 | 116 | def __init__(self, audio_export_service, progress_handler: ExportProgressHandler): 117 | self.audio_export_service = audio_export_service 118 | self.progress_handler = progress_handler 119 | self.export_thread = None 120 | self.is_cancelled = False 121 | self.current_process = None 122 | 123 | def start_export( 124 | self, 125 | drum_parts_state, 126 | bpm, 127 | filename, 128 | repeat_count, 129 | metadata, 130 | completion_callback, 131 | ): 132 | """Start the export process in a background thread""" 133 | if self.export_thread and self.export_thread.is_alive(): 134 | return False 135 | 136 | self.is_cancelled = False 137 | self.progress_handler.start_progress_tracking() 138 | 139 | self.export_thread = threading.Thread( 140 | target=self._export_worker, 141 | args=( 142 | drum_parts_state, 143 | bpm, 144 | filename, 145 | repeat_count, 146 | metadata, 147 | completion_callback, 148 | ), 149 | daemon=True, 150 | ) 151 | self.export_thread.start() 152 | return True 153 | 154 | def cancel_export(self): 155 | """Cancel the ongoing export""" 156 | self.is_cancelled = True 157 | 158 | # Kill running process immediately 159 | if self.current_process and self.current_process.poll() is None: 160 | try: 161 | self.current_process.kill() 162 | except Exception: 163 | pass 164 | 165 | self.progress_handler.stop_progress_tracking() 166 | 167 | def _export_worker( 168 | self, 169 | drum_parts_state, 170 | bpm, 171 | filename, 172 | repeat_count, 173 | metadata, 174 | completion_callback, 175 | ): 176 | """Background worker for the export process""" 177 | try: 178 | if self.is_cancelled: 179 | return 180 | 181 | success = self.audio_export_service.export_audio( 182 | drum_parts_state, 183 | bpm, 184 | filename, 185 | progress_callback=self.progress_handler.update_phase, 186 | repeat_count=repeat_count, 187 | metadata=metadata, 188 | export_task=self, 189 | ) 190 | 191 | if not self.is_cancelled: 192 | GLib.idle_add(completion_callback, success, filename) 193 | except Exception as e: 194 | logging.error(f"Export error: {e}") 195 | if not self.is_cancelled: 196 | GLib.idle_add(completion_callback, False, filename) 197 | finally: 198 | self.progress_handler.stop_progress_tracking() 199 | -------------------------------------------------------------------------------- /src/services/audio_renderer.py: -------------------------------------------------------------------------------- 1 | # services/audio_renderer.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import logging 21 | import numpy as np 22 | from ..config.constants import ( 23 | GROUP_TOGGLE_COUNT, 24 | DEFAULT_FALLBACK_SAMPLE_SIZE, 25 | ) 26 | 27 | 28 | class AudioBuffer: 29 | """Manages audio buffer operations""" 30 | 31 | def __init__(self, sample_rate: int = 44100): 32 | self.sample_rate = sample_rate 33 | self.buffer = None 34 | 35 | def create_buffer(self, duration_seconds: float): 36 | """Create output buffer with calculated duration""" 37 | try: 38 | total_samples = int(duration_seconds * self.sample_rate) 39 | self.buffer = np.zeros((total_samples, 2), dtype="float32") 40 | return self.buffer 41 | except (MemoryError, ValueError) as e: 42 | logging.error( 43 | f"Failed to create audio buffer (duration={duration_seconds}s): {e}" 44 | ) 45 | raise 46 | 47 | def add_sample(self, sample_data: np.ndarray, start_sample: int): 48 | """Add a sample to the buffer at the specified position""" 49 | if self.buffer is None: 50 | logging.warning("Attempted to add sample to uninitialized buffer") 51 | return 52 | 53 | try: 54 | end_sample = min(start_sample + len(sample_data), len(self.buffer)) 55 | self.buffer[start_sample:end_sample] += sample_data[ 56 | : end_sample - start_sample 57 | ] 58 | except (ValueError, TypeError) as e: 59 | logging.error(f"Failed to add sample at position {start_sample}: {e}") 60 | raise 61 | 62 | def normalize(self): 63 | """Normalize the audio buffer""" 64 | if self.buffer is None: 65 | logging.warning("Attempted to normalize uninitialized buffer") 66 | return 67 | 68 | try: 69 | max_amplitude = np.max(np.abs(self.buffer)) 70 | if max_amplitude > 0: 71 | self.buffer[:] = self.buffer / max_amplitude * 0.95 72 | except (ValueError, RuntimeError) as e: 73 | logging.error(f"Failed to normalize audio buffer: {e}") 74 | raise 75 | 76 | 77 | class AudioRenderer: 78 | """Handles audio rendering operations""" 79 | 80 | def __init__(self, samples, sample_rate: int = 44100): 81 | self.samples = samples 82 | self.sample_rate = sample_rate 83 | 84 | def update_samples(self, samples): 85 | """Update the samples dictionary""" 86 | self.samples = samples 87 | 88 | def clear_samples(self): 89 | """Clear all samples from memory""" 90 | self.samples = {} 91 | 92 | def calculate_pattern_duration( 93 | self, drum_parts_state, bpm: int, repeat_count: int, total_beats: int 94 | ) -> float: 95 | """Calculate total export duration including repeats""" 96 | subdivisions_per_second = (bpm / 60) * GROUP_TOGGLE_COUNT 97 | pattern_duration_seconds = total_beats / subdivisions_per_second 98 | 99 | latest_sample_end_time = self._find_latest_sample_end_time( 100 | drum_parts_state, subdivisions_per_second 101 | ) 102 | 103 | extra_time_to_add = ( 104 | max(latest_sample_end_time, pattern_duration_seconds) 105 | - pattern_duration_seconds 106 | ) 107 | return (pattern_duration_seconds * repeat_count) + extra_time_to_add 108 | 109 | def render_pattern( 110 | self, drum_parts_state, bpm: int, total_beats: int, repeat_count: int 111 | ) -> AudioBuffer: 112 | """Render drum pattern into an audio buffer""" 113 | duration = self.calculate_pattern_duration( 114 | drum_parts_state, bpm, repeat_count, total_beats 115 | ) 116 | 117 | audio_buffer = AudioBuffer(self.sample_rate) 118 | audio_buffer.create_buffer(duration) 119 | 120 | subdivisions_per_second = (bpm / 60) * GROUP_TOGGLE_COUNT 121 | samples_per_subdivision = int(self.sample_rate / subdivisions_per_second) 122 | pattern_duration_seconds = total_beats / subdivisions_per_second 123 | 124 | for repeat in range(repeat_count): 125 | repeat_offset = repeat * int(pattern_duration_seconds * self.sample_rate) 126 | self._render_repeat( 127 | drum_parts_state, 128 | audio_buffer, 129 | repeat_offset, 130 | samples_per_subdivision, 131 | total_beats, 132 | ) 133 | 134 | audio_buffer.normalize() 135 | return audio_buffer 136 | 137 | def _find_latest_sample_end_time( 138 | self, drum_parts_state, subdivisions_per_second: float 139 | ) -> float: 140 | """Find the latest time any sample will finish playing""" 141 | latest_sample_end_time = 0 142 | 143 | for part_id, part_state in drum_parts_state.items(): 144 | if not part_state: 145 | continue 146 | 147 | active_subdivisions = [sub for sub, active in part_state.items() if active] 148 | if not active_subdivisions: 149 | continue 150 | 151 | last_subdivision = max(active_subdivisions) 152 | trigger_time = last_subdivision / subdivisions_per_second 153 | sample_length_seconds = ( 154 | len(self.samples.get(part_id, [])) / self.sample_rate 155 | ) 156 | end_time = trigger_time + sample_length_seconds 157 | latest_sample_end_time = max(latest_sample_end_time, end_time) 158 | 159 | return latest_sample_end_time 160 | 161 | def _render_repeat( 162 | self, 163 | drum_parts_state, 164 | audio_buffer: AudioBuffer, 165 | repeat_offset: int, 166 | samples_per_subdivision: int, 167 | total_beats: int, 168 | ): 169 | """Render a single repeat of the pattern""" 170 | for subdivision in range(total_beats): 171 | start_sample = repeat_offset + (subdivision * samples_per_subdivision) 172 | self._add_subdivision_samples( 173 | drum_parts_state, audio_buffer, subdivision, start_sample 174 | ) 175 | 176 | def _add_subdivision_samples( 177 | self, 178 | drum_parts_state, 179 | audio_buffer: AudioBuffer, 180 | subdivision: int, 181 | start_sample: int, 182 | ): 183 | """Add samples for all active drum parts at this subdivision""" 184 | for part_id, part_state in drum_parts_state.items(): 185 | if part_state.get(subdivision, False): 186 | sample_data = self.samples.get( 187 | part_id, np.zeros(DEFAULT_FALLBACK_SAMPLE_SIZE) 188 | ) 189 | audio_buffer.add_sample(sample_data, start_sample) 190 | -------------------------------------------------------------------------------- /po/en_GB.po: -------------------------------------------------------------------------------- 1 | # British English translation for drum-machine. 2 | # Copyright (C) 2025 drum-machine's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the drum-machine package. 4 | # Andi Chandler , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: drum-machine master\n" 9 | "Report-Msgid-Bugs-To: https://github.com/revisto/drum-machine/issues\n" 10 | "POT-Creation-Date: 2025-03-28 13:12+0000\n" 11 | "PO-Revision-Date: 2025-03-24 14:07+0000\n" 12 | "Last-Translator: Andi Chandler \n" 13 | "Language-Team: British English \n" 14 | "Language: en_GB\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.5\n" 20 | 21 | #: data/io.github.revisto.drum-machine.desktop.in:3 22 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:58 23 | msgid "Drum Machine" 24 | msgstr "Drum Machine" 25 | 26 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 27 | msgid "Create and play drum beats" 28 | msgstr "Create and play drum beats" 29 | 30 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 31 | msgid "" 32 | "Drum Machine is a modern and intuitive application for creating, playing, " 33 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 34 | "interested in rhythm creation, this application provides a simple interface " 35 | "for drum pattern programming. Have fun!" 36 | msgstr "" 37 | "Drum Machine is a modern and intuitive application for creating, playing, " 38 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 39 | "interested in rhythm creation, this application provides a simple interface " 40 | "for drum pattern programming. Have fun!" 41 | 42 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 43 | msgid "Features:" 44 | msgstr "Features:" 45 | 46 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 47 | msgid "Intuitive grid-based pattern editor" 48 | msgstr "Intuitive grid-based pattern editor" 49 | 50 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 51 | msgid "Adjustable BPM control" 52 | msgstr "Adjustable BPM control" 53 | 54 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 55 | msgid "Volume control for overall mix" 56 | msgstr "Volume control for overall mix" 57 | 58 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 59 | msgid "Save and load preset patterns" 60 | msgstr "Save and load preset patterns" 61 | 62 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 63 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 64 | msgstr "Multiple drum sounds including kick, snare, hi-hat, and more" 65 | 66 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 67 | msgid "Keyboard shortcuts for quick access to all functions" 68 | msgstr "Keyboard shortcuts for quick access to all functions" 69 | 70 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 71 | msgid "Drum Pattern Loaded in Light Mode" 72 | msgstr "Drum Pattern Loaded in Light Mode" 73 | 74 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 75 | msgid "Drum Pattern Loaded in Dark Mode" 76 | msgstr "Drum Pattern Loaded in Dark Mode" 77 | 78 | #. Update tooltip and accessibility with current BPM 79 | #: src/window.py:278 80 | msgid "{} Beats per Minute (BPM)" 81 | msgstr "{} Beats per Minute (BPM)" 82 | 83 | #. Update button tooltip to show current volume level 84 | #: src/window.py:287 85 | msgid "{:.0f}% Volume" 86 | msgstr "{:.0f}% Volume" 87 | 88 | #: src/window.py:298 src/window.ui:158 89 | msgid "Play" 90 | msgstr "Play" 91 | 92 | #: src/window.py:302 93 | msgid "Pause" 94 | msgstr "Pause" 95 | 96 | #: src/window.py:317 97 | msgid "Default Presets" 98 | msgstr "Default Presets" 99 | 100 | #: src/window.py:344 101 | msgid "MIDI files" 102 | msgstr "MIDI files" 103 | 104 | #: src/window.py:350 105 | msgid "Open MIDI File" 106 | msgstr "Open MIDI File" 107 | 108 | #: src/window.ui:41 109 | msgid "Open" 110 | msgstr "Open" 111 | 112 | #: src/window.ui:43 src/window.ui:46 113 | msgid "Open Preset" 114 | msgstr "Open Preset" 115 | 116 | #: src/window.ui:47 117 | msgid "Open Saved Drum Pattern Preset" 118 | msgstr "Open Saved Drum Pattern Preset" 119 | 120 | #: src/window.ui:65 src/window.ui:67 121 | msgid "Main Menu" 122 | msgstr "Main Menu" 123 | 124 | #: src/window.ui:68 125 | msgid "Access Keyboard Shortcuts and Application Information" 126 | msgstr "Access Keyboard Shortcuts and Application Information" 127 | 128 | #: src/window.ui:75 src/window.ui:77 129 | msgid "Save Drum Pattern" 130 | msgstr "Save Drum Pattern" 131 | 132 | #: src/window.ui:78 133 | msgid "Save Current Drum Pattern as a Preset File" 134 | msgstr "Save Current Drum Pattern as a Preset File" 135 | 136 | #: src/window.ui:122 137 | msgid "BPM" 138 | msgstr "BPM" 139 | 140 | #: src/window.ui:127 src/window.ui:131 141 | msgid "Adjust Tempo In Beats per Minute (BPM)" 142 | msgstr "Adjust Tempo In Beats per Minute (BPM)" 143 | 144 | #: src/window.ui:130 145 | msgid "Tempo" 146 | msgstr "Tempo" 147 | 148 | #: src/window.ui:172 149 | msgid "Adjust Volume" 150 | msgstr "Adjust Volume" 151 | 152 | #: src/window.ui:195 153 | msgid "Reset" 154 | msgstr "Reset" 155 | 156 | #: src/window.ui:196 157 | msgid "Reset the Drum Sequence" 158 | msgstr "Reset the Drum Sequence" 159 | 160 | #: src/window.ui:209 161 | msgid "_Keyboard Shortcuts" 162 | msgstr "_Keyboard Shortcuts" 163 | 164 | #: src/window.ui:213 165 | msgid "_About Drum Machine" 166 | msgstr "_About Drum Machine" 167 | 168 | #: src/gtk/help-overlay.ui:11 169 | msgctxt "shortcut window" 170 | msgid "General" 171 | msgstr "General" 172 | 173 | #: src/gtk/help-overlay.ui:14 174 | msgctxt "shortcut window" 175 | msgid "Show Shortcuts" 176 | msgstr "Show Shortcuts" 177 | 178 | #: src/gtk/help-overlay.ui:20 179 | msgctxt "shortcut window" 180 | msgid "Quit" 181 | msgstr "Quit" 182 | 183 | #: src/gtk/help-overlay.ui:29 184 | msgctxt "shortcut window" 185 | msgid "Playback Controls" 186 | msgstr "Playback Controls" 187 | 188 | #: src/gtk/help-overlay.ui:32 189 | msgctxt "shortcut window" 190 | msgid "Play/Pause" 191 | msgstr "Play/Pause" 192 | 193 | #: src/gtk/help-overlay.ui:39 194 | msgctxt "shortcut window" 195 | msgid "Clear All" 196 | msgstr "Clear All" 197 | 198 | #: src/gtk/help-overlay.ui:48 199 | msgctxt "shortcut window" 200 | msgid "BPM & Volume Controls" 201 | msgstr "BPM & Volume Controls" 202 | 203 | #: src/gtk/help-overlay.ui:51 204 | msgctxt "shortcut window" 205 | msgid "Increase BPM" 206 | msgstr "Increase BPM" 207 | 208 | #: src/gtk/help-overlay.ui:58 209 | msgctxt "shortcut window" 210 | msgid "Decrease BPM" 211 | msgstr "Decrease BPM" 212 | 213 | #: src/gtk/help-overlay.ui:65 214 | msgctxt "shortcut window" 215 | msgid "Increase Volume" 216 | msgstr "Increase Volume" 217 | 218 | #: src/gtk/help-overlay.ui:72 219 | msgctxt "shortcut window" 220 | msgid "Decrease Volume" 221 | msgstr "Decrease Volume" 222 | 223 | #: src/gtk/help-overlay.ui:81 224 | msgctxt "shortcut window" 225 | msgid "Preset Management" 226 | msgstr "Preset Management" 227 | 228 | #: src/gtk/help-overlay.ui:84 229 | msgctxt "shortcut window" 230 | msgid "Load Preset" 231 | msgstr "Load Preset" 232 | 233 | #: src/gtk/help-overlay.ui:91 234 | msgctxt "shortcut window" 235 | msgid "Save Preset" 236 | msgstr "Save Preset" 237 | 238 | #: src/gtk/save_changes_dialog.ui:6 239 | msgid "Save Changes?" 240 | msgstr "Save Changes?" 241 | 242 | #: src/gtk/save_changes_dialog.ui:7 243 | msgid "" 244 | "Current preset contains unsaved changes. Changes which are not saved will be " 245 | "permanently lost." 246 | msgstr "" 247 | "Current preset contains unsaved changes. Changes which are not saved will be " 248 | "permanently lost." 249 | 250 | #. Cancel the operation 251 | #: src/gtk/save_changes_dialog.ui:14 252 | msgid "_Cancel" 253 | msgstr "_Cancel" 254 | 255 | #. Discard all changes 256 | #: src/gtk/save_changes_dialog.ui:15 257 | msgid "_Discard" 258 | msgstr "_Discard" 259 | 260 | #. Save current changes 261 | #: src/gtk/save_changes_dialog.ui:16 262 | msgid "_Save" 263 | msgstr "_Save" 264 | -------------------------------------------------------------------------------- /po/it.po: -------------------------------------------------------------------------------- 1 | # Italian translation for drum-machine. 2 | # Copyright (C) 2025 drum-machine's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the drum-machine package. 4 | # catoblepa , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: drum-machine master\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-03-24 13:12+0000\n" 11 | "PO-Revision-Date: 2025-03-26 23:36+0100\n" 12 | "Last-Translator: catoblepa \n" 13 | "Language-Team: Italian\n" 14 | "Language: it\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Gtranslator 47.1\n" 20 | 21 | #: data/io.github.revisto.drum-machine.desktop.in:3 22 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:58 23 | msgid "Drum Machine" 24 | msgstr "Drum Machine" 25 | 26 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 27 | msgid "Create and play drum beats" 28 | msgstr "Crea e suona ritmi di batteria" 29 | 30 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 31 | msgid "" 32 | "Drum Machine is a modern and intuitive application for creating, playing, " 33 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 34 | "interested in rhythm creation, this application provides a simple interface " 35 | "for drum pattern programming. Have fun!" 36 | msgstr "" 37 | "Drum Machine è un'applicazione moderna e intuitiva per creare, suonare e " 38 | "gestire pattern di batteria. Perfetta per musicisti, produttori e chiunque " 39 | "sia interessato alla creazione di ritmi, questa applicazione offre " 40 | "un'interfaccia semplice per programmare pattern di batteria. Buon " 41 | "divertimento!" 42 | 43 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 44 | msgid "Features:" 45 | msgstr "Funzionalità:" 46 | 47 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 48 | msgid "Intuitive grid-based pattern editor" 49 | msgstr "Editor di pattern intuitivo basato su griglia" 50 | 51 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 52 | msgid "Adjustable BPM control" 53 | msgstr "Regolazione dei battiti al minuto" 54 | 55 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 56 | msgid "Volume control for overall mix" 57 | msgstr "Controllo del volume per il mix generale" 58 | 59 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 60 | msgid "Save and load preset patterns" 61 | msgstr "Salva e carica pattern preimpostati" 62 | 63 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 64 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 65 | msgstr "" 66 | "Suoni di batteria multipli, inclusi cassa, rullante, charleston e altro" 67 | 68 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 69 | msgid "Keyboard shortcuts for quick access to all functions" 70 | msgstr "Scorciatoie da tastiera per un accesso rapido a tutte le funzioni" 71 | 72 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 73 | msgid "Drum Pattern Loaded in Light Mode" 74 | msgstr "Pattern di batteria caricato in modalità chiara" 75 | 76 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 77 | msgid "Drum Pattern Loaded in Dark Mode" 78 | msgstr "Pattern di batteria caricato in modalità scura" 79 | 80 | #. Update tooltip and accessibility with current BPM 81 | #: src/window.py:278 82 | msgid "{} Beats per Minute (BPM)" 83 | msgstr "{} battiti al minuto (BPM)" 84 | 85 | #. Update button tooltip to show current volume level 86 | #: src/window.py:287 87 | msgid "{:.0f}% Volume" 88 | msgstr "Volume al {:.0f}%" 89 | 90 | #: src/window.py:298 src/window.ui:158 91 | msgid "Play" 92 | msgstr "Riproduci" 93 | 94 | #: src/window.py:302 95 | msgid "Pause" 96 | msgstr "Pausa" 97 | 98 | #: src/window.py:317 99 | msgid "Default Presets" 100 | msgstr "Preimpostazioni predefinite" 101 | 102 | #: src/window.py:344 103 | msgid "MIDI files" 104 | msgstr "File MIDI" 105 | 106 | #: src/window.py:350 107 | msgid "Open MIDI File" 108 | msgstr "Apri file MIDI" 109 | 110 | #: src/window.ui:41 111 | msgid "Open" 112 | msgstr "Apri" 113 | 114 | #: src/window.ui:43 src/window.ui:46 115 | msgid "Open Preset" 116 | msgstr "Apri preimpostazione" 117 | 118 | #: src/window.ui:47 119 | msgid "Open Saved Drum Pattern Preset" 120 | msgstr "Apri preimpostazione di pattern di batteria salvata" 121 | 122 | #: src/window.ui:65 src/window.ui:67 123 | msgid "Main Menu" 124 | msgstr "Menù principale" 125 | 126 | #: src/window.ui:68 127 | msgid "Access Keyboard Shortcuts and Application Information" 128 | msgstr "" 129 | "Accedi alle scorciatoie da tastiera e alle informazioni sull'applicazione" 130 | 131 | #: src/window.ui:75 src/window.ui:77 132 | msgid "Save Drum Pattern" 133 | msgstr "Salva pattern di batteria" 134 | 135 | #: src/window.ui:78 136 | msgid "Save Current Drum Pattern as a Preset File" 137 | msgstr "Salva il pattern di batteria attuale come file di preimpostazione" 138 | 139 | #: src/window.ui:122 140 | msgid "BPM" 141 | msgstr "BPM" 142 | 143 | #: src/window.ui:127 src/window.ui:131 144 | msgid "Adjust Tempo In Beats per Minute (BPM)" 145 | msgstr "Regola il tempo in battiti al minuto (BPM)" 146 | 147 | #: src/window.ui:130 148 | msgid "Tempo" 149 | msgstr "Tempo" 150 | 151 | #: src/window.ui:172 152 | msgid "Adjust Volume" 153 | msgstr "Regola il volume" 154 | 155 | #: src/window.ui:195 156 | msgid "Reset" 157 | msgstr "Reimposta" 158 | 159 | #: src/window.ui:196 160 | msgid "Reset the Drum Sequence" 161 | msgstr "Reimposta la sequenza di batteria" 162 | 163 | #: src/window.ui:209 164 | msgid "_Keyboard Shortcuts" 165 | msgstr "Scorciatoie da _tastiera" 166 | 167 | #: src/window.ui:213 168 | msgid "_About Drum Machine" 169 | msgstr "_Informazioni su Drum Machine" 170 | 171 | #: src/gtk/help-overlay.ui:11 172 | msgctxt "shortcut window" 173 | msgid "General" 174 | msgstr "Generali" 175 | 176 | #: src/gtk/help-overlay.ui:14 177 | msgctxt "shortcut window" 178 | msgid "Show Shortcuts" 179 | msgstr "Mostra scorciatoie" 180 | 181 | #: src/gtk/help-overlay.ui:20 182 | msgctxt "shortcut window" 183 | msgid "Quit" 184 | msgstr "Esce" 185 | 186 | #: src/gtk/help-overlay.ui:29 187 | msgctxt "shortcut window" 188 | msgid "Playback Controls" 189 | msgstr "Controlli di riproduzione" 190 | 191 | #: src/gtk/help-overlay.ui:32 192 | msgctxt "shortcut window" 193 | msgid "Play/Pause" 194 | msgstr "Riproduci/Pausa" 195 | 196 | #: src/gtk/help-overlay.ui:39 197 | msgctxt "shortcut window" 198 | msgid "Clear All" 199 | msgstr "Cancella tutto" 200 | 201 | #: src/gtk/help-overlay.ui:48 202 | msgctxt "shortcut window" 203 | msgid "BPM & Volume Controls" 204 | msgstr "Controlli BPM e volume" 205 | 206 | #: src/gtk/help-overlay.ui:51 207 | msgctxt "shortcut window" 208 | msgid "Increase BPM" 209 | msgstr "Aumenta i BPM" 210 | 211 | #: src/gtk/help-overlay.ui:58 212 | msgctxt "shortcut window" 213 | msgid "Decrease BPM" 214 | msgstr "Diminuisce i BPM" 215 | 216 | #: src/gtk/help-overlay.ui:65 217 | msgctxt "shortcut window" 218 | msgid "Increase Volume" 219 | msgstr "Aumenta il volume" 220 | 221 | #: src/gtk/help-overlay.ui:72 222 | msgctxt "shortcut window" 223 | msgid "Decrease Volume" 224 | msgstr "Diminuisce il volume" 225 | 226 | #: src/gtk/help-overlay.ui:81 227 | msgctxt "shortcut window" 228 | msgid "Preset Management" 229 | msgstr "Gestione preimpostazioni" 230 | 231 | #: src/gtk/help-overlay.ui:84 232 | msgctxt "shortcut window" 233 | msgid "Load Preset" 234 | msgstr "Carica preimpostazioni" 235 | 236 | #: src/gtk/help-overlay.ui:91 237 | msgctxt "shortcut window" 238 | msgid "Save Preset" 239 | msgstr "Salva preimpostazioni" 240 | 241 | #: src/gtk/save_changes_dialog.ui:6 242 | msgid "Save Changes?" 243 | msgstr "Salvare le modifiche?" 244 | 245 | #: src/gtk/save_changes_dialog.ui:7 246 | msgid "" 247 | "Current preset contains unsaved changes. Changes which are not saved will be " 248 | "permanently lost." 249 | msgstr "" 250 | "La preimpostazione corrente contiene modifiche non salvate. Le modifiche non " 251 | "salvate saranno perse definitivamente." 252 | 253 | #. Cancel the operation 254 | #: src/gtk/save_changes_dialog.ui:14 255 | msgid "_Cancel" 256 | msgstr "_Annulla" 257 | 258 | #. Discard all changes 259 | #: src/gtk/save_changes_dialog.ui:15 260 | msgid "_Discard" 261 | msgstr "S_carta" 262 | 263 | #. Save current changes 264 | #: src/gtk/save_changes_dialog.ui:16 265 | msgid "_Save" 266 | msgstr "_Salva" 267 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # German translation for drum-machine. 2 | # Copyright (C) 2025 drum-machine's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the drum-machine package. 4 | # Manuel Rottschäfer , 2025. 5 | # Philipp Kiemle , 2025. 6 | # Jürgen Benvenuti , 2025. 7 | # 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: drum-machine master\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2025-03-26 13:12+0000\n" 13 | "PO-Revision-Date: 2025-03-27 18:59+0100\n" 14 | "Last-Translator: Jürgen Benvenuti \n" 15 | "Language-Team: German \n" 16 | "Language: de\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | "X-Generator: Poedit 3.4.4\n" 22 | 23 | #: data/io.github.revisto.drum-machine.desktop.in:3 24 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:58 25 | msgid "Drum Machine" 26 | msgstr "Drum Machine" 27 | 28 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 29 | msgid "Create and play drum beats" 30 | msgstr "Erstellen und Abspielen von Schlagzeugrhythmen" 31 | 32 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 33 | msgid "" 34 | "Drum Machine is a modern and intuitive application for creating, playing, " 35 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 36 | "interested in rhythm creation, this application provides a simple interface " 37 | "for drum pattern programming. Have fun!" 38 | msgstr "" 39 | "Drum Machine ist eine moderne und intuitive Anwendung, die das Erstellen, " 40 | "Abspielen und Verwalten von Schlagzeugrhythmen ermöglicht. Sie eignet sich " 41 | "optimal für Musiker, Produzenten und alle, die sich für die Erstellung von " 42 | "Schlagzeugrhythmen interessieren. Drum Machine zeichnet sich durch eine " 43 | "simple Benutzeroberfläche aus. Viel Spaß!" 44 | 45 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 46 | msgid "Features:" 47 | msgstr "Funktionen:" 48 | 49 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 50 | msgid "Intuitive grid-based pattern editor" 51 | msgstr "Intuitiver, gitterbasierter Rhythmus-Editor" 52 | 53 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 54 | msgid "Adjustable BPM control" 55 | msgstr "Anpassbare BPM-Kontrolle" 56 | 57 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 58 | msgid "Volume control for overall mix" 59 | msgstr "Lautstärkeregelung für den allgemeinen Mix" 60 | 61 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 62 | msgid "Save and load preset patterns" 63 | msgstr "Speichern und Laden von Vorgaberhythmen" 64 | 65 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 66 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 67 | msgstr "Verschiedene Drum-Klänge einschließlich Kick, Snare, Hi-Hat und mehr" 68 | 69 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 70 | msgid "Keyboard shortcuts for quick access to all functions" 71 | msgstr "Tastenkürzel für schnellen Zugriff auf alle Funktionen" 72 | 73 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 74 | msgid "Drum Pattern Loaded in Light Mode" 75 | msgstr "Schlagzeugrhythmus im hellen Thema geladen" 76 | 77 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 78 | msgid "Drum Pattern Loaded in Dark Mode" 79 | msgstr "Schlagzeugrhythmus im dunklen Thema geladen" 80 | 81 | #. Update tooltip and accessibility with current BPM 82 | #: src/window.py:278 83 | msgid "{} Beats per Minute (BPM)" 84 | msgstr "{} Taktschläge pro Minute (BPM)" 85 | 86 | #. Update button tooltip to show current volume level 87 | #: src/window.py:287 88 | msgid "{:.0f}% Volume" 89 | msgstr "{:.0f}% Lautstärke" 90 | 91 | #: src/window.py:298 src/window.ui:158 92 | msgid "Play" 93 | msgstr "Abspielen" 94 | 95 | #: src/window.py:302 96 | msgid "Pause" 97 | msgstr "Pausieren" 98 | 99 | #: src/window.py:317 100 | msgid "Default Presets" 101 | msgstr "Standard-Vorgaben" 102 | 103 | #: src/window.py:344 104 | msgid "MIDI files" 105 | msgstr "MIDI-Dateien" 106 | 107 | #: src/window.py:350 108 | msgid "Open MIDI File" 109 | msgstr "MIDI-Datei öffnen" 110 | 111 | #: src/window.ui:41 112 | msgid "Open" 113 | msgstr "Öffnen" 114 | 115 | #: src/window.ui:43 src/window.ui:46 116 | msgid "Open Preset" 117 | msgstr "Vorgabe öffnen" 118 | 119 | #: src/window.ui:47 120 | msgid "Open Saved Drum Pattern Preset" 121 | msgstr "Gespeicherte Schlagzeugrhythmus-Vorgabe öffnen" 122 | 123 | #: src/window.ui:65 src/window.ui:67 124 | msgid "Main Menu" 125 | msgstr "Hauptmenü" 126 | 127 | #: src/window.ui:68 128 | msgid "Access Keyboard Shortcuts and Application Information" 129 | msgstr "Tastenkürzel und Anwendungsinformationen einsehen" 130 | 131 | #: src/window.ui:75 src/window.ui:77 132 | msgid "Save Drum Pattern" 133 | msgstr "Schlagzeugrhythmus speichern" 134 | 135 | #: src/window.ui:78 136 | msgid "Save Current Drum Pattern as a Preset File" 137 | msgstr "Aktuellen Schlagzeugrhythmus als Vorgabe speichern" 138 | 139 | #: src/window.ui:122 140 | msgid "BPM" 141 | msgstr "BPM" 142 | 143 | #: src/window.ui:127 src/window.ui:131 144 | msgid "Adjust Tempo In Beats per Minute (BPM)" 145 | msgstr "Taktschläge pro Minute anpassen (BPM)" 146 | 147 | #: src/window.ui:130 148 | msgid "Tempo" 149 | msgstr "Geschwindigkeit" 150 | 151 | #: src/window.ui:172 152 | msgid "Adjust Volume" 153 | msgstr "Lautstärke anpassen" 154 | 155 | #: src/window.ui:195 156 | msgid "Reset" 157 | msgstr "Zurücksetzen" 158 | 159 | #: src/window.ui:196 160 | msgid "Reset the Drum Sequence" 161 | msgstr "Den Schlagzeugrhythmus zurücksetzen" 162 | 163 | #: src/window.ui:209 164 | msgid "_Keyboard Shortcuts" 165 | msgstr "_Tastenkürzel" 166 | 167 | #: src/window.ui:213 168 | msgid "_About Drum Machine" 169 | msgstr "_Info zu Drum Machine" 170 | 171 | #: src/gtk/help-overlay.ui:11 172 | msgctxt "shortcut window" 173 | msgid "General" 174 | msgstr "Allgemein" 175 | 176 | #: src/gtk/help-overlay.ui:14 177 | msgctxt "shortcut window" 178 | msgid "Show Shortcuts" 179 | msgstr "Tastenkürzel anzeigen" 180 | 181 | #: src/gtk/help-overlay.ui:20 182 | msgctxt "shortcut window" 183 | msgid "Quit" 184 | msgstr "Beenden" 185 | 186 | #: src/gtk/help-overlay.ui:29 187 | msgctxt "shortcut window" 188 | msgid "Playback Controls" 189 | msgstr "Wiedergabekontrolle" 190 | 191 | #: src/gtk/help-overlay.ui:32 192 | msgctxt "shortcut window" 193 | msgid "Play/Pause" 194 | msgstr "Abspielen/Pausieren" 195 | 196 | #: src/gtk/help-overlay.ui:39 197 | msgctxt "shortcut window" 198 | msgid "Clear All" 199 | msgstr "Alles leeren" 200 | 201 | #: src/gtk/help-overlay.ui:48 202 | msgctxt "shortcut window" 203 | msgid "BPM & Volume Controls" 204 | msgstr "BPM & Lautstärkeregelung" 205 | 206 | #: src/gtk/help-overlay.ui:51 207 | msgctxt "shortcut window" 208 | msgid "Increase BPM" 209 | msgstr "BPM erhöhen" 210 | 211 | #: src/gtk/help-overlay.ui:58 212 | msgctxt "shortcut window" 213 | msgid "Decrease BPM" 214 | msgstr "BPM verringern" 215 | 216 | #: src/gtk/help-overlay.ui:65 217 | msgctxt "shortcut window" 218 | msgid "Increase Volume" 219 | msgstr "Lautstärke erhöhen" 220 | 221 | #: src/gtk/help-overlay.ui:72 222 | msgctxt "shortcut window" 223 | msgid "Decrease Volume" 224 | msgstr "Lautstärke verringern" 225 | 226 | #: src/gtk/help-overlay.ui:81 227 | msgctxt "shortcut window" 228 | msgid "Preset Management" 229 | msgstr "Vorgaberhythmenverwaltung" 230 | 231 | #: src/gtk/help-overlay.ui:84 232 | msgctxt "shortcut window" 233 | msgid "Load Preset" 234 | msgstr "Vorgaberhythmus laden" 235 | 236 | #: src/gtk/help-overlay.ui:91 237 | msgctxt "shortcut window" 238 | msgid "Save Preset" 239 | msgstr "Vorgaberhythmus speichern" 240 | 241 | #: src/gtk/save_changes_dialog.ui:6 242 | msgid "Save Changes?" 243 | msgstr "Änderungen speichern?" 244 | 245 | #: src/gtk/save_changes_dialog.ui:7 246 | msgid "" 247 | "Current preset contains unsaved changes. Changes which are not saved will be " 248 | "permanently lost." 249 | msgstr "" 250 | "Der aktuelle Vorgaberhythmus enthält ungespeicherte Änderungen, die " 251 | "unwiederbringlich verloren gehen, sofern sie nicht gespeichert werden." 252 | 253 | #. Cancel the operation 254 | #: src/gtk/save_changes_dialog.ui:14 255 | msgid "_Cancel" 256 | msgstr "_Abbrechen" 257 | 258 | #. Discard all changes 259 | #: src/gtk/save_changes_dialog.ui:15 260 | msgid "_Discard" 261 | msgstr "_Verwerfen" 262 | 263 | #. Save current changes 264 | #: src/gtk/save_changes_dialog.ui:16 265 | msgid "_Save" 266 | msgstr "_Speichern" 267 | -------------------------------------------------------------------------------- /src/dialogs/midi_mapping_dialog.py: -------------------------------------------------------------------------------- 1 | # dialogs/midi_mapping_dialog.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # SPDX-License-Identifier: GPL-3.0-or-later 11 | 12 | import logging 13 | import gi 14 | 15 | gi.require_version("Gtk", "4.0") 16 | gi.require_version("Adw", "1") 17 | from gi.repository import Gtk, Adw 18 | from gettext import gettext as _ 19 | 20 | # Common GM Percussion Map (General MIDI Level 1) 21 | GM_PERCUSSION_MAP = { 22 | 35: _("Acoustic Bass Drum"), 23 | 36: _("Bass Drum 1"), 24 | 37: _("Side Stick"), 25 | 38: _("Acoustic Snare"), 26 | 39: _("Hand Clap"), 27 | 40: _("Electric Snare"), 28 | 41: _("Low Floor Tom"), 29 | 42: _("Closed Hi Hat"), 30 | 43: _("High Floor Tom"), 31 | 44: _("Pedal Hi Hat"), 32 | 45: _("Low Tom"), 33 | 46: _("Open Hi Hat"), 34 | 47: _("Low-Mid Tom"), 35 | 48: _("Hi-Mid Tom"), 36 | 49: _("Crash Cymbal 1"), 37 | 50: _("High Tom"), 38 | 51: _("Ride Cymbal 1"), 39 | 52: _("Chinese Cymbal"), 40 | 53: _("Ride Bell"), 41 | 54: _("Tambourine"), 42 | 55: _("Splash Cymbal"), 43 | 56: _("Cowbell"), 44 | 57: _("Crash Cymbal 2"), 45 | 58: _("Vibraslap"), 46 | 59: _("Ride Cymbal 2"), 47 | 60: _("Hi Bongo"), 48 | 61: _("Low Bongo"), 49 | 62: _("Mute Hi Conga"), 50 | 63: _("Open Hi Conga"), 51 | 64: _("Low Conga"), 52 | 65: _("High Timbale"), 53 | 66: _("Low Timbale"), 54 | 67: _("High Agogo"), 55 | 68: _("Low Agogo"), 56 | 69: _("Cabasa"), 57 | 70: _("Maracas"), 58 | 71: _("Short Whistle"), 59 | 72: _("Long Whistle"), 60 | 73: _("Short Guiro"), 61 | 74: _("Long Guiro"), 62 | 75: _("Claves"), 63 | 76: _("Hi Wood Block"), 64 | 77: _("Low Wood Block"), 65 | 78: _("Mute Cuica"), 66 | 79: _("Open Cuica"), 67 | 80: _("Mute Triangle"), 68 | 81: _("Open Triangle"), 69 | } 70 | 71 | 72 | class MidiMappingDialog(Adw.Dialog): 73 | def __init__(self, parent, drum_part, on_save_callback): 74 | super().__init__() 75 | self.drum_part = drum_part 76 | self.on_save_callback = on_save_callback 77 | 78 | self.set_title(_("MIDI Mapping")) 79 | self.set_content_width(450) 80 | 81 | # Toolbar View to hold HeaderBar + Content 82 | toolbar_view = Adw.ToolbarView() 83 | self.set_child(toolbar_view) 84 | 85 | # Header Bar 86 | header_bar = Adw.HeaderBar() 87 | header_bar.set_show_title(True) 88 | 89 | toolbar_view.add_top_bar(header_bar) 90 | 91 | # Content Area - Match export dialog structure with margins 92 | scrolled = Gtk.ScrolledWindow() 93 | scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 94 | scrolled.set_propagate_natural_height(True) 95 | 96 | content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24) 97 | content_box.set_margin_start(24) 98 | content_box.set_margin_end(24) 99 | 100 | scrolled.set_child(content_box) 101 | toolbar_view.set_content(scrolled) 102 | 103 | # Use PreferencesPage for nice grouping 104 | page = Adw.PreferencesPage() 105 | content_box.append(page) 106 | 107 | # Group 1: Note Assignment 108 | note_group = Adw.PreferencesGroup(title=_("Note Assignment")) 109 | note_group.set_description( 110 | _( 111 | "Assign a MIDI note for '{}'. " 112 | "This ensures correct playback when exporting." 113 | ).format(drum_part.name) 114 | ) 115 | page.add(note_group) 116 | 117 | # Action Row for Note 118 | self.note_row = Adw.ActionRow(title=_("MIDI Note")) 119 | self.note_row.set_subtitle(_("The note number to trigger")) 120 | 121 | # Spin Button for Note Number 122 | adjustment = Gtk.Adjustment( 123 | value=drum_part.midi_note_id or 36, 124 | lower=0, 125 | upper=127, 126 | step_increment=1, 127 | page_increment=12, 128 | ) 129 | self.spin_button = Gtk.SpinButton(adjustment=adjustment) 130 | self.spin_button.set_valign(Gtk.Align.CENTER) 131 | self.spin_button.connect("value-changed", self._on_value_changed) 132 | 133 | self.note_row.add_suffix(self.spin_button) 134 | note_group.add(self.note_row) 135 | 136 | # Group 2: Standard Instruments 137 | preset_group = Adw.PreferencesGroup(title=_("Standard Instruments")) 138 | preset_group.set_description( 139 | _("Select a General MIDI instrument to automatically set the note.") 140 | ) 141 | page.add(preset_group) 142 | 143 | preset_row = Adw.ActionRow(title=_("Instrument Preset")) 144 | 145 | # Dropdown for presets 146 | model = Gtk.StringList() 147 | self.note_map = [] # List of (note, string_item) 148 | 149 | # Sort by note number 150 | sorted_map = sorted(GM_PERCUSSION_MAP.items()) 151 | 152 | current_note = int(adjustment.get_value()) 153 | selected_idx = -1 154 | 155 | idx = 0 156 | for note, name in sorted_map: 157 | display_str = f"{note} - {name}" 158 | model.append(display_str) 159 | self.note_map.append(note) 160 | if note == current_note: 161 | selected_idx = idx 162 | idx += 1 163 | 164 | self.dropdown = Gtk.DropDown(model=model) 165 | self.dropdown.set_enable_search(True) 166 | self.dropdown.set_valign(Gtk.Align.CENTER) 167 | 168 | self.dropdown.connect("notify::selected", self._on_preset_selected) 169 | 170 | if selected_idx != -1: 171 | self.dropdown.set_selected(selected_idx) 172 | else: 173 | self.dropdown.set_selected(Gtk.INVALID_LIST_POSITION) 174 | 175 | preset_row.add_suffix(self.dropdown) 176 | preset_group.add(preset_row) 177 | 178 | # Update subtitle of note row to match initial state 179 | self._update_gm_subtitle(int(adjustment.get_value())) 180 | 181 | # Bottom Bar for actions - single pill button (matching export dialog style) 182 | button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) 183 | button_box.set_halign(Gtk.Align.CENTER) 184 | button_box.set_margin_bottom(32) 185 | 186 | save_btn = Gtk.Button(label=_("Save")) 187 | save_btn.add_css_class("pill") 188 | save_btn.add_css_class("suggested-action") 189 | save_btn.connect("clicked", self._on_save_clicked) 190 | button_box.append(save_btn) 191 | 192 | toolbar_view.add_bottom_bar(button_box) 193 | 194 | def _on_value_changed(self, button): 195 | val = int(button.get_value()) 196 | self._update_gm_subtitle(val) 197 | 198 | # Update dropdown if matches 199 | found = False 200 | try: 201 | if val in self.note_map: 202 | idx = self.note_map.index(val) 203 | if self.dropdown.get_selected() != idx: 204 | self.dropdown.set_selected(idx) 205 | found = True 206 | except ValueError as e: 207 | # ValueError is expected if val is not in self.note_map; 208 | # This is normal behavior for custom notes not in GM percussion map 209 | logging.debug(f"MIDI note {val} not in GM percussion map: {e}") 210 | pass 211 | 212 | if not found: 213 | self.dropdown.set_selected(Gtk.INVALID_LIST_POSITION) 214 | 215 | def _on_preset_selected(self, dropdown, pspec): 216 | selected_idx = dropdown.get_selected() 217 | if selected_idx != Gtk.INVALID_LIST_POSITION and selected_idx < len( 218 | self.note_map 219 | ): 220 | note = self.note_map[selected_idx] 221 | if int(self.spin_button.get_value()) != note: 222 | self.spin_button.set_value(note) 223 | 224 | def _update_gm_subtitle(self, note): 225 | name = GM_PERCUSSION_MAP.get(note) 226 | if name: 227 | self.note_row.set_subtitle(name) 228 | else: 229 | self.note_row.set_subtitle(_("Custom Note")) 230 | 231 | def _on_save_clicked(self, button): 232 | note = int(self.spin_button.get_value()) 233 | self.on_save_callback(self.drum_part.id, note) 234 | self.close() 235 | -------------------------------------------------------------------------------- /src/handlers/window_actions.py: -------------------------------------------------------------------------------- 1 | # handlers/window_actions.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import logging 21 | import gi 22 | from typing import Optional, List, Callable 23 | 24 | gi.require_version("Gtk", "4.0") 25 | gi.require_version("Gio", "2.0") 26 | gi.require_version("Adw", "1") 27 | from gi.repository import Gio 28 | 29 | 30 | class WindowActionHandler: 31 | """Handles window-level actions and keyboard shortcuts""" 32 | 33 | def __init__(self, window) -> None: 34 | self.window = window 35 | 36 | def setup_actions(self) -> None: 37 | """Setup all window actions and keyboard shortcuts""" 38 | actions = [ 39 | ("open_menu", self.on_open_menu_action, ["F10"]), 40 | ("show-help-overlay", self.on_show_help_overlay, ["question"]), 41 | ("play_pause", self.handle_play_pause_action, ["space"]), 42 | ("clear_toggles", self.handle_clear_action, ["Delete"]), 43 | ("increase_bpm", self.increase_bpm_action, ["plus", "equal"]), 44 | ("decrease_bpm", self.decrease_bpm_action, ["minus"]), 45 | ("increase_volume", self.increase_volume_action, ["Up"]), 46 | ("decrease_volume", self.decrease_volume_action, ["Down"]), 47 | ("load_pattern", self.on_open_file_action, ["o"]), 48 | ("save_pattern", self.on_save_pattern_action, ["s"]), 49 | ("export_audio", self.on_export_audio_action, ["e"]), 50 | ("add_samples", self.on_add_samples_action, ["a"]), 51 | ("quit", self.on_quit_action, ["q"]), 52 | ("close_window", self.on_quit_action, ["w"]), 53 | ("go_to_instrument", self.handle_go_to_instrument_action, ["i"]), 54 | ("previous_page", self.handle_previous_page_action, ["Page_Up"]), 55 | ("next_page", self.handle_next_page_action, ["Page_Down"]), 56 | ("mute", self.handle_mute, ["m"]), 57 | ] 58 | 59 | for action_name, callback, shortcuts in actions: 60 | self._create_action(action_name, callback, shortcuts) 61 | 62 | def _create_action( 63 | self, name: str, callback: Callable, shortcuts: Optional[List[str]] = None 64 | ) -> None: 65 | """Create and register an action with optional keyboard shortcuts""" 66 | action = Gio.SimpleAction.new(name, None) 67 | action.connect("activate", callback) 68 | self.window.add_action(action) 69 | if shortcuts: 70 | self.window.application.set_accels_for_action(f"win.{name}", shortcuts) 71 | 72 | # Action handlers 73 | def on_open_menu_action( 74 | self, action: Gio.SimpleAction, param: Optional[object] 75 | ) -> None: 76 | self.window.menu_button.activate() 77 | 78 | def on_show_help_overlay( 79 | self, action: Gio.SimpleAction, param: Optional[object] 80 | ) -> None: 81 | self.window.get_help_overlay().present() 82 | 83 | def handle_play_pause_action( 84 | self, action: Gio.SimpleAction, param: Optional[object] 85 | ) -> None: 86 | self.window.handle_play_pause(self.window.play_pause_button) 87 | 88 | def handle_clear_action( 89 | self, action: Gio.SimpleAction, param: Optional[object] 90 | ) -> None: 91 | self.window.handle_clear(self.window.clear_button) 92 | 93 | def increase_bpm_action( 94 | self, action: Gio.SimpleAction, param: Optional[object] 95 | ) -> None: 96 | current_bpm = self.window.bpm_spin_button.get_value() 97 | self.window.bpm_spin_button.set_value(current_bpm + 1) 98 | 99 | def decrease_bpm_action( 100 | self, action: Gio.SimpleAction, param: Optional[object] 101 | ) -> None: 102 | current_bpm = self.window.bpm_spin_button.get_value() 103 | self.window.bpm_spin_button.set_value(current_bpm - 1) 104 | 105 | def increase_volume_action( 106 | self, action: Gio.SimpleAction, param: Optional[object] 107 | ) -> None: 108 | current_volume = self.window.volume_button.get_value() 109 | self.window.volume_button.set_value(min(current_volume + 5, 100)) 110 | 111 | def decrease_volume_action( 112 | self, action: Gio.SimpleAction, param: Optional[object] 113 | ) -> None: 114 | current_volume = self.window.volume_button.get_value() 115 | self.window.volume_button.set_value(max(current_volume - 5, 0)) 116 | 117 | def on_open_file_action( 118 | self, action: Gio.SimpleAction, param: Optional[object] 119 | ) -> None: 120 | self.window._on_open_file_clicked(self.window.file_pattern_button) 121 | 122 | def on_save_pattern_action( 123 | self, action: Gio.SimpleAction, param: Optional[object] 124 | ) -> None: 125 | self.window._on_save_pattern_clicked() 126 | 127 | def on_export_audio_action( 128 | self, action: Gio.SimpleAction, param: Optional[object] 129 | ) -> None: 130 | self.window._on_export_audio_clicked(self.window.export_audio_button) 131 | 132 | def on_quit_action( 133 | self, action: Optional[Gio.SimpleAction], param: Optional[object] 134 | ) -> None: 135 | if self.window.save_changes_service.has_unsaved_changes(): 136 | self.window.save_changes_service.prompt_save_changes( 137 | on_save=self.window._save_and_close, 138 | on_discard=self.window.cleanup_and_destroy, 139 | ) 140 | else: 141 | self.window.cleanup_and_destroy() 142 | 143 | def handle_go_to_instrument_action( 144 | self, action: Gio.SimpleAction, param: Optional[object] 145 | ) -> None: 146 | """Go to the currently focused instrument button.""" 147 | if hasattr(self.window, "carousel"): 148 | # Find which drum part is currently focused 149 | focused_widget = self.window.get_focus() 150 | if focused_widget: 151 | widget_name = focused_widget.get_name() 152 | if widget_name and "_toggle_" in widget_name: 153 | drum_part = widget_name.split("_toggle_")[0] 154 | try: 155 | instrument_button = getattr( 156 | self.window, f"{drum_part}_instrument_button" 157 | ) 158 | instrument_button.grab_focus() 159 | except AttributeError as e: 160 | logging.debug( 161 | f"Could not find instrument button for {drum_part}: {e}" 162 | ) 163 | pass 164 | 165 | def handle_previous_page_action( 166 | self, action: Gio.SimpleAction, param: Optional[object] 167 | ) -> None: 168 | """Go to the previous page.""" 169 | if hasattr(self.window, "carousel"): 170 | carousel = self.window.carousel 171 | current_page = carousel.get_position() 172 | if current_page > 0: 173 | carousel.scroll_to(carousel.get_nth_page(current_page - 1), True) 174 | 175 | def handle_next_page_action( 176 | self, action: Gio.SimpleAction, param: Optional[object] 177 | ) -> None: 178 | """Go to the next page.""" 179 | if hasattr(self.window, "carousel"): 180 | carousel = self.window.carousel 181 | current_page = carousel.get_position() 182 | n_pages = carousel.get_n_pages() 183 | if current_page < n_pages - 1: 184 | carousel.scroll_to(carousel.get_nth_page(current_page + 1), True) 185 | 186 | def handle_mute(self, action: Gio.SimpleAction, param: Optional[object]) -> None: 187 | current_volume = self.window.volume_button.get_value() 188 | last_volume = self.window.drum_machine_service.last_volume 189 | if current_volume == 0: 190 | self.window.volume_button.set_value(last_volume) 191 | else: 192 | self.window.volume_button.set_value(0) 193 | 194 | def on_add_samples_action( 195 | self, action: Gio.SimpleAction, param: Optional[object] 196 | ) -> None: 197 | """Open file dialog to select multiple audio samples""" 198 | self.window.file_dialog_handler.handle_add_samples() 199 | -------------------------------------------------------------------------------- /src/services/drum_machine_service.py: -------------------------------------------------------------------------------- 1 | # services/drum_machine_service.py 2 | # 3 | # Copyright 2025 revisto 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | import threading 21 | import time 22 | import logging 23 | from typing import Dict, Optional 24 | from gi.repository import GLib 25 | from ..interfaces.player import IPlayer 26 | from ..config.constants import NUM_TOGGLES, GROUP_TOGGLE_COUNT 27 | from .pattern_service import PatternService 28 | from .ui_helper import UIHelper 29 | 30 | 31 | class DrumMachineService(IPlayer): 32 | def __init__(self, window, sound_service, ui_helper: UIHelper) -> None: 33 | self.window = window 34 | self.sound_service = sound_service 35 | self.ui_helper = ui_helper 36 | self.playing: bool = False 37 | self.bpm: float = 120 38 | self.last_volume: float = 100 39 | self.play_thread: Optional[threading.Thread] = None 40 | self.stop_event: threading.Event = threading.Event() 41 | self.drum_parts_state: Dict[str, Dict[int, bool]] = ( 42 | self.create_empty_drum_parts_state() 43 | ) 44 | self.pattern_service = PatternService(window) 45 | self.total_beats: int = NUM_TOGGLES 46 | self.beats_per_page: int = NUM_TOGGLES 47 | self.active_pages: int = 1 48 | self.playing_beat: int = -1 49 | 50 | def create_empty_drum_parts_state(self) -> Dict[str, Dict[int, bool]]: 51 | # Get drum parts from sound service 52 | drum_parts = self.sound_service.drum_part_manager.get_all_parts() 53 | drum_parts_state = {part.id: dict() for part in drum_parts} 54 | return drum_parts_state 55 | 56 | def play(self) -> None: 57 | self.playing = True 58 | self.stop_event.clear() 59 | self.play_thread = threading.Thread(target=self._play_drum_sequence) 60 | self.play_thread.start() 61 | 62 | def stop(self) -> None: 63 | self.playing = False 64 | self.stop_event.set() 65 | self.sound_service.stop_all_sounds() 66 | self.ui_helper.clear_all_playhead_highlights() 67 | self.playing_beat = -1 68 | if self.play_thread: 69 | self.play_thread.join() 70 | self.play_thread = None 71 | 72 | def update_total_beats(self) -> None: 73 | """ 74 | Calculates the total number of beats and active pages 75 | based on the highest active toggle. 76 | """ 77 | max_beat = 0 78 | for part_state in self.drum_parts_state.values(): 79 | if part_state: # Check if the instrument has any active toggles 80 | max_beat = max(max_beat, *part_state.keys()) 81 | 82 | # If the pattern is completely empty, default to one page. 83 | if max_beat == 0 and not any(self.drum_parts_state.values()): 84 | num_pages = 1 85 | else: 86 | # Calculate pages needed for the highest beat. 87 | num_pages = (max_beat // self.beats_per_page) + 1 88 | 89 | self.active_pages = num_pages 90 | self.total_beats = self.active_pages * self.beats_per_page 91 | 92 | def set_bpm(self, bpm: float) -> None: 93 | self.bpm = bpm 94 | 95 | def set_volume(self, volume: float) -> None: 96 | self.sound_service.set_volume(volume) 97 | if volume != 0: 98 | self.last_volume = volume 99 | 100 | def clear_all_toggles(self) -> None: 101 | self.drum_parts_state = self.create_empty_drum_parts_state() 102 | self.ui_helper.deactivate_all_toggles_in_ui() 103 | 104 | def save_pattern(self, file_path: str) -> None: 105 | self.pattern_service.save_pattern(file_path, self.drum_parts_state, self.bpm) 106 | 107 | def load_pattern(self, file_path: str) -> None: 108 | self.ui_helper.deactivate_all_toggles_in_ui() 109 | self.drum_parts_state, self.bpm = self.pattern_service.load_pattern(file_path) 110 | 111 | # Refresh UI to show new temporary parts 112 | self.window.drum_grid_builder.rebuild_drum_parts_column() 113 | self.window.drum_grid_builder.rebuild_carousel() 114 | 115 | self.ui_helper.set_bpm_in_ui(self.bpm) 116 | 117 | def _play_drum_sequence(self) -> None: 118 | current_beat = 0 119 | while self.playing and not self.stop_event.is_set(): 120 | # Check if the loop should end or wrap around 121 | if current_beat >= self.total_beats: 122 | current_beat = 0 # Loop back to the beginning 123 | 124 | if self.stop_event.is_set(): 125 | break 126 | 127 | # Highlight the current beat (this will also de-highlight the previous one) 128 | self.ui_helper.highlight_playhead_at_beat(current_beat) 129 | self.playing_beat = current_beat 130 | 131 | if current_beat % self.beats_per_page == 0 or current_beat == 0: 132 | target_page = current_beat // self.beats_per_page 133 | GLib.idle_add(self.ui_helper.scroll_carousel_to_page, target_page) 134 | 135 | # Play sounds for the current beat 136 | drum_parts = self.sound_service.drum_part_manager.get_all_parts() 137 | for part in drum_parts: 138 | if self.drum_parts_state[part.id].get(current_beat, False): 139 | self.sound_service.play_sound(part.id) 140 | 141 | # Wait for the next beat 142 | delay_per_step = 60 / self.bpm / GROUP_TOGGLE_COUNT 143 | time.sleep(delay_per_step) 144 | 145 | GLib.idle_add( 146 | self.ui_helper.remove_playhead_highlight_at_beat, current_beat 147 | ) 148 | 149 | # Advance the playhead 150 | current_beat += 1 151 | 152 | def preview_drum_part(self, part_id: str) -> None: 153 | """Preview a drum part sound""" 154 | drum_part_manager = self.sound_service.drum_part_manager 155 | if drum_part_manager.get_part_by_id(part_id): 156 | self.sound_service.preview_sound(part_id) 157 | 158 | def add_drum_part_state(self, part_id: str) -> None: 159 | """Add a new drum part to the state""" 160 | self.drum_parts_state[part_id] = {} 161 | 162 | def add_new_drum_part(self, file_path: str, name: str) -> Optional[object]: 163 | """Add a new drum part from an audio file""" 164 | new_part = self.sound_service.drum_part_manager.add_custom_part(name, file_path) 165 | if new_part: 166 | # Reload sounds 167 | self.sound_service.reload_sounds() 168 | # Add to drum machine state 169 | self.add_drum_part_state(new_part.id) 170 | # Update UI 171 | self.window.drum_grid_builder.add_drum_part(new_part) 172 | return new_part 173 | return None 174 | 175 | def replace_drum_part( 176 | self, drum_id: str, file_path: str, name: str 177 | ) -> Optional[object]: 178 | """Replace an existing drum part with a new audio file""" 179 | result = self.sound_service.drum_part_manager.replace_part( 180 | drum_id, file_path, name 181 | ) 182 | if result: 183 | # Reload the specific sound for this drum part 184 | self.sound_service.reload_specific_sound(drum_id) 185 | # Update UI button label 186 | self.window.drum_grid_builder.update_drum_button(drum_id) 187 | # Update total beats in case pattern changed 188 | self.update_total_beats() 189 | return result 190 | return None 191 | 192 | def remove_drum_part(self, drum_id: str) -> bool: 193 | """Remove a drum part from the service""" 194 | result = self.sound_service.drum_part_manager.remove_part(drum_id) 195 | if result: 196 | # Remove from drum machine state 197 | self.drum_parts_state.pop(drum_id, None) 198 | # Rebuild the UI to reflect the removal 199 | self.window.drum_grid_builder.rebuild_drum_parts_column() 200 | self.window.drum_grid_builder.rebuild_carousel() 201 | # Update total beats in case pattern changed 202 | self.update_total_beats() 203 | logging.info(f"Removed drum part: {drum_id}") 204 | return True 205 | else: 206 | logging.error(f"Failed to remove drum part: {drum_id}") 207 | return False 208 | -------------------------------------------------------------------------------- /po/he.po: -------------------------------------------------------------------------------- 1 | # Hebrew translation for drum-machine. 2 | # Copyright (C) 2025 drum-machine's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the drum-machine package. 4 | # Yaron Shahrabani , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: drum-machine master\n" 9 | "Report-Msgid-Bugs-To: https://github.com/revisto/drum-machine/issues\n" 10 | "POT-Creation-Date: 2025-08-15 03:26+0000\n" 11 | "PO-Revision-Date: 2025-08-15 14:39+0300\n" 12 | "Last-Translator: Yaron Shahrabani \n" 13 | "Language-Team: Hebrew \n" 14 | "Language: he\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.6\n" 20 | 21 | #: data/io.github.revisto.drum-machine.desktop.in:2 22 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:67 23 | msgid "Drum Machine" 24 | msgstr "מכונת תופים" 25 | 26 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 27 | msgid "Create and play drum beats" 28 | msgstr "יצירת ונגינה של צלילי תופים" 29 | 30 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 31 | msgid "" 32 | "Drum Machine is a modern and intuitive application for creating, playing, " 33 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 34 | "interested in rhythm creation, this application provides a simple interface " 35 | "for drum pattern programming. Have fun!" 36 | msgstr "" 37 | "מכונת תופים הוא יישום ברור וחדיש ליצירת, נגינת וניהול תבניות נגינה על תופים. " 38 | "מושלם למוזיקאים, מפיקים וכל מי שמתעניין ביצירת מקצבים, היישום הזה מספק מנשק " 39 | "פשוט לתכנות תבניות תיפוף. שיהיה בכיף!" 40 | 41 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 42 | msgid "Features:" 43 | msgstr "יכולות:" 44 | 45 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 46 | msgid "Intuitive grid-based pattern editor" 47 | msgstr "עורך תבניות מבוסס רשת ברור" 48 | 49 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 50 | msgid "Adjustable BPM control" 51 | msgstr "בקרת BPM מתכווננת" 52 | 53 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 54 | msgid "Volume control for overall mix" 55 | msgstr "בקרת עוצמת שמע לערבול כולל" 56 | 57 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 58 | msgid "Save and load preset patterns" 59 | msgstr "שמירת וטעינת תבניות שנערכו מראש" 60 | 61 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 62 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 63 | msgstr "מגוון צלילי תיפוף כגון קיק, סנר ,הייהט ועוד" 64 | 65 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 66 | msgid "Keyboard shortcuts for quick access to all functions" 67 | msgstr "קיצורי מקלדת לגישה מהירה לכל הפונקציות" 68 | 69 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 70 | msgid "Drum Pattern Loaded in Light Mode" 71 | msgstr "תבנית תיפוף טעונה במצב בהיר" 72 | 73 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 74 | msgid "Drum Pattern Loaded in Dark Mode" 75 | msgstr "תבנית תיפוף טעונה במצב כהה" 76 | 77 | #: src/application.py:69 78 | msgid "translator-credits" 79 | msgstr "ירון שהרבני " 80 | 81 | #. Update tooltip and accessibility with current BPM 82 | #: src/window.py:190 83 | msgid "{} Beats per Minute (BPM)" 84 | msgstr "{} פעימות בדקה (BPM)" 85 | 86 | #. Update button tooltip to show current volume level 87 | #: src/window.py:199 88 | msgid "{:.0f}% Volume" 89 | msgstr "{:.0f}% עוצמת שמע" 90 | 91 | #: src/window.py:214 src/window.ui:167 92 | msgid "Play" 93 | msgstr "נגינה" 94 | 95 | #: src/window.py:218 96 | msgid "Pause" 97 | msgstr "השהיה" 98 | 99 | #: src/window.ui:50 100 | msgid "Open" 101 | msgstr "פתיחה" 102 | 103 | #: src/window.ui:52 104 | msgid "Open Preset" 105 | msgstr "פתיחת ערכה" 106 | 107 | #: src/window.ui:55 108 | msgctxt "accessibility" 109 | msgid "Open Preset" 110 | msgstr "פתיחת ערכה" 111 | 112 | #: src/window.ui:56 113 | msgid "Open Saved Drum Pattern Preset" 114 | msgstr "פתיחת ערכת תבנית תיפוף שמורה" 115 | 116 | #: src/window.ui:74 117 | msgid "Main Menu" 118 | msgstr "תפריט ראשי" 119 | 120 | #: src/window.ui:76 121 | msgctxt "accessibility" 122 | msgid "Main Menu" 123 | msgstr "תפריט ראשי" 124 | 125 | #: src/window.ui:77 126 | msgid "Access Keyboard Shortcuts and Application Information" 127 | msgstr "גישה לקיצורי מקלדת ולפרטים על היישום" 128 | 129 | #: src/window.ui:84 130 | msgid "Save Drum Pattern" 131 | msgstr "שמירת תבנית תיפוף" 132 | 133 | #: src/window.ui:86 134 | msgctxt "accessibility" 135 | msgid "Save Drum Pattern" 136 | msgstr "שמירת תבנית תיפוף" 137 | 138 | #: src/window.ui:87 139 | msgid "Save Current Drum Pattern as a Preset File" 140 | msgstr "שמירת תבנית התיפוף הנוכחית כקובץ ערכה" 141 | 142 | #: src/window.ui:131 143 | msgid "BPM" 144 | msgstr "BPM" 145 | 146 | #: src/window.ui:136 src/window.ui:140 147 | msgid "Adjust Tempo In Beats per Minute (BPM)" 148 | msgstr "כיוון הקצב בפעימות לדקה (BPM)" 149 | 150 | #: src/window.ui:139 151 | msgid "Tempo" 152 | msgstr "קצב" 153 | 154 | #: src/window.ui:181 155 | msgid "Adjust Volume" 156 | msgstr "כיוון עוצמת שמע" 157 | 158 | #: src/window.ui:204 159 | msgid "Reset" 160 | msgstr "איפוס" 161 | 162 | #: src/window.ui:205 163 | msgid "Reset the Drum Sequence" 164 | msgstr "איפוס רצף התיפוף" 165 | 166 | #: src/window.ui:218 167 | msgid "_Keyboard Shortcuts" 168 | msgstr "_קיצורי מקלדת" 169 | 170 | #: src/window.ui:222 171 | msgid "_About Drum Machine" 172 | msgstr "על מכונת _תופים" 173 | 174 | #: src/gtk/help-overlay.ui:11 175 | msgctxt "shortcut window" 176 | msgid "General" 177 | msgstr "כללי" 178 | 179 | #: src/gtk/help-overlay.ui:14 180 | msgctxt "shortcut window" 181 | msgid "Show Shortcuts" 182 | msgstr "הצגת קיצורי מקלדת" 183 | 184 | #: src/gtk/help-overlay.ui:20 185 | msgctxt "shortcut window" 186 | msgid "Quit" 187 | msgstr "יציאה" 188 | 189 | #: src/gtk/help-overlay.ui:29 190 | msgctxt "shortcut window" 191 | msgid "Playback Controls" 192 | msgstr "פקדי נגינה" 193 | 194 | #: src/gtk/help-overlay.ui:32 195 | msgctxt "shortcut window" 196 | msgid "Play/Pause" 197 | msgstr "נגינה / השהיה" 198 | 199 | #: src/gtk/help-overlay.ui:39 200 | msgctxt "shortcut window" 201 | msgid "Clear All" 202 | msgstr "לרוקן הכול" 203 | 204 | #: src/gtk/help-overlay.ui:48 205 | msgctxt "shortcut window" 206 | msgid "BPM & Volume Controls" 207 | msgstr "BPM ובקרת עוצמת שמע" 208 | 209 | #: src/gtk/help-overlay.ui:51 210 | msgctxt "shortcut window" 211 | msgid "Increase BPM" 212 | msgstr "האצת פעימות לדקה" 213 | 214 | #: src/gtk/help-overlay.ui:58 215 | msgctxt "shortcut window" 216 | msgid "Decrease BPM" 217 | msgstr "האטת פעימות לדקה" 218 | 219 | #: src/gtk/help-overlay.ui:65 220 | msgctxt "shortcut window" 221 | msgid "Increase Volume" 222 | msgstr "הגברת עוצמת שמע" 223 | 224 | #: src/gtk/help-overlay.ui:72 225 | msgctxt "shortcut window" 226 | msgid "Decrease Volume" 227 | msgstr "הנמכת עוצמת שמע" 228 | 229 | #: src/gtk/help-overlay.ui:79 230 | msgctxt "shortcut window" 231 | msgid "Mute" 232 | msgstr "השתקה" 233 | 234 | #: src/gtk/help-overlay.ui:88 235 | msgctxt "shortcut window" 236 | msgid "Preset Management" 237 | msgstr "ניהול ערכות" 238 | 239 | #: src/gtk/help-overlay.ui:91 240 | msgctxt "shortcut window" 241 | msgid "Load Preset" 242 | msgstr "טעינת ערכה" 243 | 244 | #: src/gtk/help-overlay.ui:98 245 | msgctxt "shortcut window" 246 | msgid "Save Preset" 247 | msgstr "שמירת ערכה" 248 | 249 | #: src/gtk/help-overlay.ui:107 250 | msgctxt "shortcut window" 251 | msgid "Navigation" 252 | msgstr "ניווט" 253 | 254 | #: src/gtk/help-overlay.ui:110 255 | msgctxt "shortcut window" 256 | msgid "Go to Instrument" 257 | msgstr "מעבר לכלי" 258 | 259 | #: src/gtk/help-overlay.ui:117 260 | msgctxt "shortcut window" 261 | msgid "Previous Page" 262 | msgstr "העמוד הקודם" 263 | 264 | #: src/gtk/help-overlay.ui:124 265 | msgctxt "shortcut window" 266 | msgid "Next Page" 267 | msgstr "העמוד הבא" 268 | 269 | #: src/gtk/save_changes_dialog.ui:6 270 | msgid "Save Changes?" 271 | msgstr "לשמור את השינויים?" 272 | 273 | #: src/gtk/save_changes_dialog.ui:7 274 | msgid "" 275 | "Current preset contains unsaved changes. Changes which are not saved will be " 276 | "permanently lost." 277 | msgstr "" 278 | "הערכה הנוכחית מכילה שינויים שלא נשמרו. השינויים שלא יישמרו יאבדו לצמיתות." 279 | 280 | #. Cancel the operation 281 | #: src/gtk/save_changes_dialog.ui:14 282 | msgid "_Cancel" 283 | msgstr "_ביטול" 284 | 285 | #. Discard all changes 286 | #: src/gtk/save_changes_dialog.ui:15 287 | msgid "_Discard" 288 | msgstr "הת_עלמות" 289 | 290 | #. Save current changes 291 | #: src/gtk/save_changes_dialog.ui:16 292 | msgid "_Save" 293 | msgstr "_שמירה" 294 | 295 | #: src/handlers/file_dialog_handler.py:48 296 | msgid "Default Presets" 297 | msgstr "ערכות ברירת מחדל" 298 | 299 | #: src/handlers/file_dialog_handler.py:76 300 | #: src/handlers/file_dialog_handler.py:142 301 | msgid "MIDI files" 302 | msgstr "קובצי MIDI" 303 | 304 | #: src/handlers/file_dialog_handler.py:82 305 | msgid "Open MIDI File" 306 | msgstr "פתיחת קובץ MIDI" 307 | 308 | #: src/handlers/file_dialog_handler.py:148 309 | msgid "Save Sequence" 310 | msgstr "שמירת הרצף" 311 | -------------------------------------------------------------------------------- /po/sv.po: -------------------------------------------------------------------------------- 1 | # Swedish translation for drum-machine. 2 | # Copyright © 2025 drum-machine's COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the drum-machine package. 4 | # Anders Jonsson , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: drum-machine master\n" 9 | "Report-Msgid-Bugs-To: https://github.com/revisto/drum-machine/issues\n" 10 | "POT-Creation-Date: 2025-08-12 15:25+0000\n" 11 | "PO-Revision-Date: 2025-08-26 01:20+0200\n" 12 | "Last-Translator: Anders Jonsson \n" 13 | "Language-Team: Swedish \n" 14 | "Language: sv\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.7\n" 20 | 21 | #: data/io.github.revisto.drum-machine.desktop.in:2 22 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:67 23 | msgid "Drum Machine" 24 | msgstr "Drum Machine" 25 | 26 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 27 | msgid "Create and play drum beats" 28 | msgstr "Skapa och spela trumslag" 29 | 30 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 31 | msgid "" 32 | "Drum Machine is a modern and intuitive application for creating, playing, " 33 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 34 | "interested in rhythm creation, this application provides a simple interface " 35 | "for drum pattern programming. Have fun!" 36 | msgstr "" 37 | "Drum Machine är ett modernt och intuitivt program för att skapa, spela och " 38 | "hantera trumslingor. Perfekt för musiker, producenter och alla som är " 39 | "intresserade av att skapa rytmer. Detta program tillhandahåller ett enkelt " 40 | "gränssnitt för trumslingeprogrammering. Ha så kul!" 41 | 42 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 43 | msgid "Features:" 44 | msgstr "Funktioner:" 45 | 46 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 47 | msgid "Intuitive grid-based pattern editor" 48 | msgstr "Intuitiv rutnätsbaserad slingredigerare" 49 | 50 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 51 | msgid "Adjustable BPM control" 52 | msgstr "Justerbar BPM-kontroll" 53 | 54 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 55 | msgid "Volume control for overall mix" 56 | msgstr "Volymkontroll för generell mix" 57 | 58 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 59 | msgid "Save and load preset patterns" 60 | msgstr "Spara och läs in förinställda slingor" 61 | 62 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 63 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 64 | msgstr "Flera trumljud inkluderande bastrumma, virveltrumma, hi-hat med mera" 65 | 66 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 67 | msgid "Keyboard shortcuts for quick access to all functions" 68 | msgstr "Tangentbordsgenvägar för snabb åtkomst till alla funktioner" 69 | 70 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 71 | msgid "Drum Pattern Loaded in Light Mode" 72 | msgstr "Trumslinga inläst i ljust läge" 73 | 74 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 75 | msgid "Drum Pattern Loaded in Dark Mode" 76 | msgstr "Trumslinga inläst i mörkt läge" 77 | 78 | #: src/application.py:69 79 | msgid "translator-credits" 80 | msgstr "Anders Jonsson " 81 | 82 | #. Update tooltip and accessibility with current BPM 83 | #: src/window.py:190 84 | msgid "{} Beats per Minute (BPM)" 85 | msgstr "{} slag per minut (BPM)" 86 | 87 | #. Update button tooltip to show current volume level 88 | #: src/window.py:199 89 | msgid "{:.0f}% Volume" 90 | msgstr "{:.0f}% volym" 91 | 92 | #: src/window.py:214 src/window.ui:167 93 | msgid "Play" 94 | msgstr "Spela" 95 | 96 | #: src/window.py:218 97 | msgid "Pause" 98 | msgstr "Pausa" 99 | 100 | #: src/window.ui:50 101 | msgid "Open" 102 | msgstr "Öppna" 103 | 104 | #: src/window.ui:52 105 | msgid "Open Preset" 106 | msgstr "Öppna förinställning" 107 | 108 | #: src/window.ui:55 109 | msgctxt "accessibility" 110 | msgid "Open Preset" 111 | msgstr "Öppna förinställning" 112 | 113 | #: src/window.ui:56 114 | msgid "Open Saved Drum Pattern Preset" 115 | msgstr "Öppna sparad trumslingeförinställning" 116 | 117 | #: src/window.ui:74 118 | msgid "Main Menu" 119 | msgstr "Huvudmeny" 120 | 121 | #: src/window.ui:76 122 | msgctxt "accessibility" 123 | msgid "Main Menu" 124 | msgstr "Huvudmeny" 125 | 126 | #: src/window.ui:77 127 | msgid "Access Keyboard Shortcuts and Application Information" 128 | msgstr "Kom åt tangentbordsgenvägar och programinformation" 129 | 130 | #: src/window.ui:84 131 | msgid "Save Drum Pattern" 132 | msgstr "Spara trumslinga" 133 | 134 | #: src/window.ui:86 135 | msgctxt "accessibility" 136 | msgid "Save Drum Pattern" 137 | msgstr "Spara trumslinga" 138 | 139 | #: src/window.ui:87 140 | msgid "Save Current Drum Pattern as a Preset File" 141 | msgstr "Spara aktuell trumslinga som en förinställningsfil" 142 | 143 | #: src/window.ui:131 144 | msgid "BPM" 145 | msgstr "BPM" 146 | 147 | #: src/window.ui:136 src/window.ui:140 148 | msgid "Adjust Tempo In Beats per Minute (BPM)" 149 | msgstr "Justera tempo i slag per minut (BPM)" 150 | 151 | #: src/window.ui:139 152 | msgid "Tempo" 153 | msgstr "Tempo" 154 | 155 | #: src/window.ui:181 156 | msgid "Adjust Volume" 157 | msgstr "Justera volym" 158 | 159 | #: src/window.ui:204 160 | msgid "Reset" 161 | msgstr "Återställ" 162 | 163 | #: src/window.ui:205 164 | msgid "Reset the Drum Sequence" 165 | msgstr "Återställ trumsekvensen" 166 | 167 | #: src/window.ui:218 168 | msgid "_Keyboard Shortcuts" 169 | msgstr "_Tangentbordsgenvägar" 170 | 171 | #: src/window.ui:222 172 | msgid "_About Drum Machine" 173 | msgstr "_Om Drum Machine" 174 | 175 | #: src/gtk/help-overlay.ui:11 176 | msgctxt "shortcut window" 177 | msgid "General" 178 | msgstr "Allmänt" 179 | 180 | #: src/gtk/help-overlay.ui:14 181 | msgctxt "shortcut window" 182 | msgid "Show Shortcuts" 183 | msgstr "Visa kortkommandon" 184 | 185 | #: src/gtk/help-overlay.ui:20 186 | msgctxt "shortcut window" 187 | msgid "Quit" 188 | msgstr "Avsluta" 189 | 190 | #: src/gtk/help-overlay.ui:29 191 | msgctxt "shortcut window" 192 | msgid "Playback Controls" 193 | msgstr "Uppspelningskontroller" 194 | 195 | #: src/gtk/help-overlay.ui:32 196 | msgctxt "shortcut window" 197 | msgid "Play/Pause" 198 | msgstr "Spela/pausa" 199 | 200 | #: src/gtk/help-overlay.ui:39 201 | msgctxt "shortcut window" 202 | msgid "Clear All" 203 | msgstr "Töm alla" 204 | 205 | #: src/gtk/help-overlay.ui:48 206 | msgctxt "shortcut window" 207 | msgid "BPM & Volume Controls" 208 | msgstr "BPM- och volymkontroller" 209 | 210 | #: src/gtk/help-overlay.ui:51 211 | msgctxt "shortcut window" 212 | msgid "Increase BPM" 213 | msgstr "Öka slag/minut" 214 | 215 | #: src/gtk/help-overlay.ui:58 216 | msgctxt "shortcut window" 217 | msgid "Decrease BPM" 218 | msgstr "Minska slag/minut" 219 | 220 | #: src/gtk/help-overlay.ui:65 221 | msgctxt "shortcut window" 222 | msgid "Increase Volume" 223 | msgstr "Öka volymen" 224 | 225 | #: src/gtk/help-overlay.ui:72 226 | msgctxt "shortcut window" 227 | msgid "Decrease Volume" 228 | msgstr "Sänk volymen" 229 | 230 | #: src/gtk/help-overlay.ui:79 231 | msgctxt "shortcut window" 232 | msgid "Mute" 233 | msgstr "Tysta" 234 | 235 | #: src/gtk/help-overlay.ui:88 236 | msgctxt "shortcut window" 237 | msgid "Preset Management" 238 | msgstr "Förinställningshantering" 239 | 240 | #: src/gtk/help-overlay.ui:91 241 | msgctxt "shortcut window" 242 | msgid "Load Preset" 243 | msgstr "Läs in förinställning" 244 | 245 | #: src/gtk/help-overlay.ui:98 246 | msgctxt "shortcut window" 247 | msgid "Save Preset" 248 | msgstr "Spara förinställning" 249 | 250 | #: src/gtk/help-overlay.ui:107 251 | msgctxt "shortcut window" 252 | msgid "Navigation" 253 | msgstr "Navigering" 254 | 255 | #: src/gtk/help-overlay.ui:110 256 | msgctxt "shortcut window" 257 | msgid "Go to Instrument" 258 | msgstr "Gå till instrument" 259 | 260 | #: src/gtk/help-overlay.ui:117 261 | msgctxt "shortcut window" 262 | msgid "Previous Page" 263 | msgstr "Föregående sida" 264 | 265 | #: src/gtk/help-overlay.ui:124 266 | msgctxt "shortcut window" 267 | msgid "Next Page" 268 | msgstr "Nästa sida" 269 | 270 | #: src/gtk/save_changes_dialog.ui:6 271 | msgid "Save Changes?" 272 | msgstr "Spara ändringar?" 273 | 274 | #: src/gtk/save_changes_dialog.ui:7 275 | msgid "" 276 | "Current preset contains unsaved changes. Changes which are not saved will be " 277 | "permanently lost." 278 | msgstr "" 279 | "Aktuell förinställning innehåller osparade ändringar. Ändringar som inte " 280 | "sparas kommer gå permanent förlorade." 281 | 282 | #. Cancel the operation 283 | #: src/gtk/save_changes_dialog.ui:14 284 | msgid "_Cancel" 285 | msgstr "A_vbryt" 286 | 287 | #. Discard all changes 288 | #: src/gtk/save_changes_dialog.ui:15 289 | msgid "_Discard" 290 | msgstr "_Förkasta" 291 | 292 | #. Save current changes 293 | #: src/gtk/save_changes_dialog.ui:16 294 | msgid "_Save" 295 | msgstr "_Spara" 296 | 297 | #: src/handlers/file_dialog_handler.py:48 298 | msgid "Default Presets" 299 | msgstr "Standardförinställningar" 300 | 301 | #: src/handlers/file_dialog_handler.py:76 302 | #: src/handlers/file_dialog_handler.py:142 303 | msgid "MIDI files" 304 | msgstr "MIDI-filer" 305 | 306 | #: src/handlers/file_dialog_handler.py:82 307 | msgid "Open MIDI File" 308 | msgstr "Öppna MIDI-fil" 309 | 310 | #: src/handlers/file_dialog_handler.py:148 311 | msgid "Save Sequence" 312 | msgstr "Spara sekvens" 313 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/io.github.revisto.drum-machine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /po/hu.po: -------------------------------------------------------------------------------- 1 | # Hungarian translation for drum-machine. 2 | # Copyright (C) 2025 Free Software Foundation, Inc. 3 | # This file is distributed under the same license as the drum-machine package. 4 | # 5 | # Balázs Meskó , 2025. 6 | # Balázs Úr , 2025. 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: drum-machine master\n" 10 | "Report-Msgid-Bugs-To: https://github.com/revisto/drum-machine/issues\n" 11 | "POT-Creation-Date: 2025-09-01 15:26+0000\n" 12 | "PO-Revision-Date: 2025-09-01 17:35+0200\n" 13 | "Last-Translator: Balázs Úr \n" 14 | "Language-Team: Hungarian \n" 15 | "Language: hu\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | "X-Generator: Lokalize 24.12.3\n" 21 | 22 | #: data/io.github.revisto.drum-machine.desktop.in:2 23 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:6 src/window.ui:67 24 | msgid "Drum Machine" 25 | msgstr "Dobgép" 26 | 27 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:7 28 | msgid "Create and play drum beats" 29 | msgstr "Dobütések létrehozása és lejátszása" 30 | 31 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:9 32 | msgid "" 33 | "Drum Machine is a modern and intuitive application for creating, playing, " 34 | "and managing drum patterns. Perfect for musicians, producers, and anyone " 35 | "interested in rhythm creation, this application provides a simple interface " 36 | "for drum pattern programming. Have fun!" 37 | msgstr "" 38 | "A Dobgép egy modern és intuitív alkalmazás dobminták létrehozásához, lejátszás" 39 | "ához és kezeléséhez. Ez az alkalmazás tökéletes zenészek, producerek és minden" 40 | "ki számára, aki érdeklődik a ritmusalkotás iránt, mivel egyszerű felületet biz" 41 | "tosít a dobminták programozásához. Jó szórakozást!" 42 | 43 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:15 44 | msgid "Features:" 45 | msgstr "Jellemzők:" 46 | 47 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:17 48 | msgid "Intuitive grid-based pattern editor" 49 | msgstr "Intuitív rácsalapú mintaszerkesztő" 50 | 51 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:18 52 | msgid "Adjustable BPM control" 53 | msgstr "Állítható BPM-vezérlés" 54 | 55 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:19 56 | msgid "Volume control for overall mix" 57 | msgstr "Hangerőszabályzó a teljes keveréshez" 58 | 59 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:20 60 | msgid "Save and load preset patterns" 61 | msgstr "Minta-előbeállítások mentése és betöltése" 62 | 63 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:21 64 | msgid "Multiple drum sounds including kick, snare, hi-hat, and more" 65 | msgstr "Többféle dobhang, köztük lábdob, pergődob, lábcin és egyebek" 66 | 67 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:22 68 | msgid "Keyboard shortcuts for quick access to all functions" 69 | msgstr "Gyorsbillentyűk az összes funkció gyors eléréséhez" 70 | 71 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:31 72 | msgid "Drum Pattern Loaded in Light Mode" 73 | msgstr "Dobminta világos módban betöltve" 74 | 75 | #: data/io.github.revisto.drum-machine.metainfo.xml.in:35 76 | msgid "Drum Pattern Loaded in Dark Mode" 77 | msgstr "Dobminta sötét módban betöltve" 78 | 79 | #: src/application.py:69 80 | msgid "translator-credits" 81 | msgstr "" 82 | "Meskó Balázs , 2025.\n" 83 | "Úr Balázs , 2025." 84 | 85 | #. Update tooltip and accessibility with current BPM 86 | #: src/window.py:190 87 | msgid "{} Beats per Minute (BPM)" 88 | msgstr "{} ütés percenként (BPM)" 89 | 90 | #. Update button tooltip to show current volume level 91 | #: src/window.py:199 92 | msgid "{:.0f}% Volume" 93 | msgstr "{:.0f}%-os hangerő" 94 | 95 | #: src/window.py:214 src/window.ui:167 96 | msgid "Play" 97 | msgstr "Lejátszás" 98 | 99 | #: src/window.py:218 100 | msgid "Pause" 101 | msgstr "Szüneteltetés" 102 | 103 | #: src/window.ui:50 104 | msgid "Open" 105 | msgstr "Megnyitás" 106 | 107 | #: src/window.ui:52 108 | msgid "Open Preset" 109 | msgstr "Előbeállítás megnyitása" 110 | 111 | #: src/window.ui:55 112 | msgctxt "accessibility" 113 | msgid "Open Preset" 114 | msgstr "Előbeállítás megnyitása" 115 | 116 | #: src/window.ui:56 117 | msgid "Open Saved Drum Pattern Preset" 118 | msgstr "Mentett dobminta-előbeállítás megnyitása" 119 | 120 | #: src/window.ui:74 121 | msgid "Main Menu" 122 | msgstr "Főmenü" 123 | 124 | #: src/window.ui:76 125 | msgctxt "accessibility" 126 | msgid "Main Menu" 127 | msgstr "Főmenü" 128 | 129 | #: src/window.ui:77 130 | msgid "Access Keyboard Shortcuts and Application Information" 131 | msgstr "Gyorsbillentyűk és alkalmazásinformációk elérése" 132 | 133 | #: src/window.ui:84 134 | msgid "Save Drum Pattern" 135 | msgstr "Dobminta mentése" 136 | 137 | #: src/window.ui:86 138 | msgctxt "accessibility" 139 | msgid "Save Drum Pattern" 140 | msgstr "Dobminta mentése" 141 | 142 | #: src/window.ui:87 143 | msgid "Save Current Drum Pattern as a Preset File" 144 | msgstr "A jelenlegi dobminta mentése előbeállítás-fájlként" 145 | 146 | #: src/window.ui:131 147 | msgid "BPM" 148 | msgstr "BPM" 149 | 150 | #: src/window.ui:136 src/window.ui:140 151 | msgid "Adjust Tempo In Beats per Minute (BPM)" 152 | msgstr "A tempó módosítása percenkénti ütésszámban (BPM)" 153 | 154 | #: src/window.ui:139 155 | msgid "Tempo" 156 | msgstr "Tempó" 157 | 158 | #: src/window.ui:181 159 | msgid "Adjust Volume" 160 | msgstr "Hangerő módosítása" 161 | 162 | #: src/window.ui:204 163 | msgid "Reset" 164 | msgstr "Visszaállítás" 165 | 166 | #: src/window.ui:205 167 | msgid "Reset the Drum Sequence" 168 | msgstr "A dobsorozat visszaállítása" 169 | 170 | #: src/window.ui:218 171 | msgid "_Keyboard Shortcuts" 172 | msgstr "_Gyorsbillentyűk" 173 | 174 | #: src/window.ui:222 175 | msgid "_About Drum Machine" 176 | msgstr "A Dobgép _névjegye" 177 | 178 | #: src/gtk/help-overlay.ui:11 179 | msgctxt "shortcut window" 180 | msgid "General" 181 | msgstr "Általános" 182 | 183 | #: src/gtk/help-overlay.ui:14 184 | msgctxt "shortcut window" 185 | msgid "Show Shortcuts" 186 | msgstr "Gyorsbillentyűk megjelenítése" 187 | 188 | #: src/gtk/help-overlay.ui:20 189 | msgctxt "shortcut window" 190 | msgid "Quit" 191 | msgstr "Kilépés" 192 | 193 | #: src/gtk/help-overlay.ui:29 194 | msgctxt "shortcut window" 195 | msgid "Playback Controls" 196 | msgstr "Lejátszásvezérlők" 197 | 198 | #: src/gtk/help-overlay.ui:32 199 | msgctxt "shortcut window" 200 | msgid "Play/Pause" 201 | msgstr "Lejátszás és szüneteltetés" 202 | 203 | #: src/gtk/help-overlay.ui:39 204 | msgctxt "shortcut window" 205 | msgid "Clear All" 206 | msgstr "Összes törlése" 207 | 208 | #: src/gtk/help-overlay.ui:48 209 | msgctxt "shortcut window" 210 | msgid "BPM & Volume Controls" 211 | msgstr "BPM- és hangerőszabályzás" 212 | 213 | #: src/gtk/help-overlay.ui:51 214 | msgctxt "shortcut window" 215 | msgid "Increase BPM" 216 | msgstr "BPM növelése" 217 | 218 | #: src/gtk/help-overlay.ui:58 219 | msgctxt "shortcut window" 220 | msgid "Decrease BPM" 221 | msgstr "BPM csökkentése" 222 | 223 | #: src/gtk/help-overlay.ui:65 224 | msgctxt "shortcut window" 225 | msgid "Increase Volume" 226 | msgstr "Hangerő növelése" 227 | 228 | #: src/gtk/help-overlay.ui:72 229 | msgctxt "shortcut window" 230 | msgid "Decrease Volume" 231 | msgstr "Hangerő csökkentése" 232 | 233 | #: src/gtk/help-overlay.ui:79 234 | msgctxt "shortcut window" 235 | msgid "Mute" 236 | msgstr "Némítás" 237 | 238 | #: src/gtk/help-overlay.ui:88 239 | msgctxt "shortcut window" 240 | msgid "Preset Management" 241 | msgstr "Előbeállítás-kezelés" 242 | 243 | #: src/gtk/help-overlay.ui:91 244 | msgctxt "shortcut window" 245 | msgid "Load Preset" 246 | msgstr "Előbeállítás betöltése" 247 | 248 | #: src/gtk/help-overlay.ui:98 249 | msgctxt "shortcut window" 250 | msgid "Save Preset" 251 | msgstr "Előbeállítás mentése" 252 | 253 | #: src/gtk/help-overlay.ui:107 254 | msgctxt "shortcut window" 255 | msgid "Navigation" 256 | msgstr "Navigáció" 257 | 258 | #: src/gtk/help-overlay.ui:110 259 | msgctxt "shortcut window" 260 | msgid "Go to Instrument" 261 | msgstr "Ugrás a hangszerhez" 262 | 263 | #: src/gtk/help-overlay.ui:117 264 | msgctxt "shortcut window" 265 | msgid "Previous Page" 266 | msgstr "Előző oldal" 267 | 268 | #: src/gtk/help-overlay.ui:124 269 | msgctxt "shortcut window" 270 | msgid "Next Page" 271 | msgstr "Következő oldal" 272 | 273 | #: src/gtk/save_changes_dialog.ui:6 274 | msgid "Save Changes?" 275 | msgstr "Menti a változtatásokat?" 276 | 277 | #: src/gtk/save_changes_dialog.ui:7 278 | msgid "" 279 | "Current preset contains unsaved changes. Changes which are not saved will be " 280 | "permanently lost." 281 | msgstr "" 282 | "A jelenlegi előbeállítás mentetlen változtatásokat tartalmaz. A mentetlen " 283 | "változtatások végleg el fognak veszni." 284 | 285 | #. Cancel the operation 286 | #: src/gtk/save_changes_dialog.ui:14 287 | msgid "_Cancel" 288 | msgstr "_Mégse" 289 | 290 | #. Discard all changes 291 | #: src/gtk/save_changes_dialog.ui:15 292 | msgid "_Discard" 293 | msgstr "_Elvetés" 294 | 295 | #. Save current changes 296 | #: src/gtk/save_changes_dialog.ui:16 297 | msgid "_Save" 298 | msgstr "Me_ntés" 299 | 300 | #: src/handlers/file_dialog_handler.py:48 301 | msgid "Default Presets" 302 | msgstr "Alapértelmezett előbeállítások" 303 | 304 | #: src/handlers/file_dialog_handler.py:76 305 | #: src/handlers/file_dialog_handler.py:142 306 | msgid "MIDI files" 307 | msgstr "MIDI-fájlok" 308 | 309 | #: src/handlers/file_dialog_handler.py:82 310 | msgid "Open MIDI File" 311 | msgstr "MIDI-fájl megnyitása" 312 | 313 | #: src/handlers/file_dialog_handler.py:148 314 | msgid "Save Sequence" 315 | msgstr "Sorozat mentése" 316 | --------------------------------------------------------------------------------