diff --git a/AGENTS.md b/AGENTS.md index 9584813..db3d1db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,16 +77,17 @@ This repository contains a Python synthesizer and its accompanying tests. - `step_sequencer.py` - Step sequencer with scale and rhythm controls - `record.py` - Audio recording functionality (WAV export) -### Testing & Examples +### Testing & Benchmarking - `tests/` - Comprehensive test suite for all modules -- `examples/` - Example scripts demonstrating various features - `synth_performance_benchmark.py` - Performance benchmarking tool ### Key Features - Real-time polyphonic synthesis with multiple waveforms -- Multiple input modes: QWERTY keyboard + external MIDI controllers +- Three input methods: + - QWERTY keyboard (default) + - External MIDI controller input (real-time hardware) + - MIDI file playback (pre-recorded files with tempo control) - Live audio visualization (waveform, spectrum, ADSR curves) -- MIDI file playback with tempo control - Comprehensive effects chain (filter, drive, delay, chorus, reverb) - Modulation system with LFO and envelope control - Pattern sequencing and arpeggiator diff --git a/README.md b/README.md index aa08dfd..c81b991 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,13 @@ A minimalist real-time synthesizer built in Python using the keyboard as a piano ## Installation -Install dependencies using `uv`: +First, install the required system package: + +```bash +sudo apt install portaudio19-dev +``` + +Then install Python dependencies using `uv`: ```bash uv sync @@ -48,6 +54,31 @@ uv run python main.py Then press keys on the keyboard to play or use the GUI controls. Press ESC to quit. +## Command line options + +```bash +uv run python main.py [OPTIONS] +``` + +- `--midi FILE` - Load MIDI file on startup +- `--play` - Auto-play the loaded MIDI file (requires `--midi`) +- `--patch NAME` - Load a saved patch preset + +Example: +```bash +uv run python main.py --midi song.mid --play --patch "Bass" +``` + +## Input methods + +The synth supports three distinct ways to create sound: + +- **QWERTY Keyboard**: Play notes using computer keys A-K, W, E, T, Y, U, O, P, etc. (default) +- **MIDI Controller**: Connect external MIDI keyboard/controller hardware (enable in MIDI Input tab) +- **MIDI File Playback**: Play pre-recorded .mid files like a music player (use MIDI Player tab or `--midi` flag) + +Note: MIDI controller input and MIDI file playback are separate features that can be used independently. + ## Testing Run the test suite to verify everything is working correctly: @@ -69,6 +100,7 @@ uv run pytest * Additional waveform types (FM synthesis, wavetables) * More filter types (comb, formant filters) +* MIDI input enhancements (sustain pedal, pitch bend, MIDI learn, etc.) ## Demo video https://github.com/user-attachments/assets/6c35c888-a61f-4219-97a1-2ab8da18e066 diff --git a/docs/keyboard_to_midi_plan.md b/docs/keyboard_to_midi_plan.md deleted file mode 100644 index fbba5ae..0000000 --- a/docs/keyboard_to_midi_plan.md +++ /dev/null @@ -1,71 +0,0 @@ -# Keyboard-to-MIDI Translator Plan - -## Overview -- Build a dedicated module that listens to QWERTY keyboard events and emits MIDI-style note messages so downstream components can treat it like any other MIDI instrument. -- Decouple GUI and controller code from the current ad-hoc keyboard model to simplify future input modes (real MIDI controllers, automation, etc.). - -## Design Goals -- Keep keyboard-to-note mapping explicit and easily swappable. -- Emit canonical MIDI `note_on`/`note_off` events with velocity and timestamp metadata. -- Maintain compatibility with mono/poly modes, arpeggiator, transpose, and global config locks. -- Provide a clean integration layer so the rest of the synth only handles MIDI events. - -## Proposed Architecture -- `qwerty_synth/keyboard_midi.py` will host the new functionality. -- Core pieces: - - `KeyboardMidiTranslator`: wraps `pynput.keyboard.Listener`, handles key state, octave/semitone shifts, and maps QWERTY keys to MIDI note numbers. - - `MidiEvent`: lightweight dataclass bundling `type` (`note_on`/`note_off`), `note`, `velocity`, `channel`, and `timestamp`. - - `MidiEventDispatcher`: pluggable callback (default: controller integration) invoked sequentially from translator threads while guarding shared state with `config.notes_lock`. -- Translator responsibilities: - - Maintain pressed key set to avoid duplicate `note_on` spam and to support mono note priority logic before handing notes to the controller. - - Emit transpose or mode-change commands by publishing control callbacks instead of directly mutating `config` (e.g., dedicated dispatcher hook or new `controller.apply_transpose_delta`). - - Allow velocity overrides (initially constant) and expose hooks for future features (e.g., velocity from key press duration). -- Event flow: QWERTY key → translator produces MIDI event → dispatcher maps to `controller.handle_midi_message` → controller converts to oscillator lifecycle operations in `synth`. - -## Integration Strategy -- Controller layer: - - Add `handle_midi_message(message: MidiEvent)` to centralize `note_on`/`note_off` handling, reuse existing helper `play_midi_note` for note-on logic, and add a symmetric release path that operates via oscillator keys rather than raw characters. - - Consolidate mono-mode tracking inside controller (possibly using a queue or `MonoVoiceManager`) so the translator no longer manipulates `config.active_notes` directly. -- GUI / application bootstrap: - - Replace imports of `qwerty_synth.input` with the new translator. - - Update GUI toggles (octave buttons, arpeggiator, mono mode) to call controller helpers or translator setters rather than mutating module globals. - - Ensure controller provides functions the GUI can call when the user changes transpose so translator state stays in sync. -- Configuration adjustments: - - Move `mono_pressed_keys` and any keyboard-specific artifacts into translator-owned state. - - Keep `octave_offset`, `semitone_offset`, and locks in `config`, but expose controller helpers that the translator uses to read/write the values under lock. - -## Legacy Keyboard Model Removal -- ✅ Removed `qwerty_synth/input.py` after migrating all call sites to the translator/controller stack. -- ✅ Replaced the old input tests with controller/translator coverage and smoke tests. -- ✅ Routed exit behaviour through dispatcher events and eliminated legacy globals. -- Ongoing: keep an eye out for stale references during future refactors. - -## Testing & Validation -- Unit tests for translator: - - Key-to-MIDI mapping integrity, including octave/semitone shifts and boundary checks. - - Mono/poly behavior via injected mock dispatcher capturing emitted events. - - Arpeggiator enablement path ensures appropriate events (possibly beat-tracked) are pushed. -- Integration tests: - - Controller `handle_midi_message` generates/release oscillators correctly. - - GUI smoke test ensuring translator bootstrap occurs without raising and exits cleanly when requested. -- Manual verification checklist: - - Run synth and confirm QWERTY input still plays notes. - - Confirm mono glide and octave switches function. - - Validate arpeggiator receives held notes. - -## Open Assumptions -- `pynput` remains our keyboard listener and is acceptable for the new module. -- We will keep using `mido` message semantics (note numbers 0–127, velocity 0–127 scaled internally) without introducing a third-party virtual MIDI device. -- Mono voice priority can stay "last pressed" for now; no need for configurable priority schemes during this refactor. -- Escape-to-exit behavior can be mediated through the dispatcher without additional UI prompts. -- Tests can be refactored in place without large fixture overhauls (current mocking strategy stays workable). - -## Current Status -- ✅ Implemented `qwerty_synth/keyboard_midi.py` with `KeyboardMidiTranslator`, `MidiEvent`, transpose controls, system-exit signaling, duplicate key suppression, and safe config access. -- ✅ Added translator-focused unit tests (`tests/test_keyboard_midi.py`) and reshaped controller tests (`tests/test_input.py`) around the new API boundary. -- ✅ Integrated the translator with the controller and GUI bootstrap: `controller.handle_midi_message` now manages keyboard-driven mono/poly voice allocation and transpose events, while `gui_qt` instantiates the translator and routes system-exit requests through Qt-safe callbacks. -- ✅ Refactored GUI octave/semitone/mono controls to go through controller helpers and introduced `tests/test_keyboard_integration.py` smoke coverage for translator→controller flows. - -## Next Implementation Steps -- **Next Step — Harden integration with broader regression coverage:** - Add headless GUI smoke tests or extended translator/controller scenarios (including arpeggiator + mono interactions) and tighten teardown hooks so repeated sessions leave no dangling listeners. diff --git a/docs/midi_controller_input_plan.md b/docs/midi_controller_input_plan.md deleted file mode 100644 index bce8cf1..0000000 --- a/docs/midi_controller_input_plan.md +++ /dev/null @@ -1,70 +0,0 @@ -# MIDI Controller Input Integration Plan - -## Goals -- Accept real-time input from external MIDI controllers in addition to the existing QWERTY translator. -- Reuse the controller’s `handle_midi_message` pathway so mono/poly rules, arpeggiator hooks, and transpose logic stay unified. -- Provide a GUI workflow for selecting/monitoring MIDI ports and shutting them down cleanly. -- Ship with sensible defaults and documentation so setup is straightforward across platforms. - -## Proposed Architecture -- Add `qwerty_synth/midi_input.py` implementing a `MidiPortTranslator` class similar in spirit to `KeyboardMidiTranslator`. - - Uses `mido`’s real-time backend (`mido.open_input`) running on a background thread. - - Normalises incoming `note_on`/`note_off` messages (velocity 0 treated as off) into the shared `MidiEvent` dataclass. - - Optionally captures channel/CC data for future expansion (sustain pedal, modulation wheel). -- Update the dispatcher wiring so both translators feed the same controller callback, possibly tagging events with a `source` field if we need to differentiate. -- GUI updates (`gui_qt`): - - Add a “MIDI Input” panel with a drop-down of available ports, connect/disconnect buttons, and status indicators. - - Persist last-used port in configuration (optional) to auto-reconnect on startup. - - Expose basic logging/error popups if the port cannot be opened. -- Configuration hooks: extend `config.py` with optional defaults (e.g., `midi_input_enabled`, `midi_input_port_name`). - -## Implementation Steps -1. **Dependencies & Backend Selection** ✅ - - ✅ Added `python-rtmidi>=1.5.8` to dependencies via `uv add` - - ✅ Platform requirements: ALSA (Linux), CoreMIDI (macOS), Windows MM -2. **Translator Module** ✅ - - ✅ Created `qwerty_synth/midi_input.py` with `MidiPortTranslator` class - - ✅ Implemented start/stop lifecycle, threading, and graceful shutdown - - ✅ Converts `mido.Message` to `MidiEvent` (note_on/note_off with velocity 0 handling) - - ✅ Added `list_midi_ports()` helper function -3. **Controller Integration** ✅ - - ✅ Reuses existing `controller.handle_midi_message` for unified event processing - - ✅ Both keyboard and MIDI translators share same dispatcher - - 🔜 Sustain pedal (CC64) support deferred for future enhancement -4. **GUI Integration** ✅ - - ✅ Added "MIDI Input" tab with port selection dropdown - - ✅ Enable/disable checkbox for MIDI input - - ✅ Refresh ports button - - ✅ Status label showing connection state - - ✅ Requires restart to apply changes (noted in UI) -5. **Configuration & Defaults** ✅ - - ✅ Added `config.midi_input_enabled` (default: False) - - ✅ Added `config.midi_input_port` (default: None for auto-select) - - ✅ Settings persist through config module -6. **Testing & Validation** ✅ - - ✅ Unit tests in `tests/test_midi_input.py` (14 tests, 86% coverage) - - ✅ Integration tests in `tests/test_midi_integration.py` (3 tests) - - ✅ Verified note_on/note_off, velocity scaling, mono/poly modes - - 🔜 Manual QA with physical MIDI controller (user testing required) - -## Open Questions & Risks -- Cross-platform device naming and hot-plug behaviour (MIDI ports appearing/disappearing). -- Thread safety when multiple translators fire events simultaneously. -- Latency considerations (ensure background thread dispatch keeps jitter low). -- Handling of additional MIDI messages (aftertouch, pitch bend) — defer unless required. - -## Implementation Status -✅ **COMPLETED** - All core functionality implemented and tested. - -The MIDI controller input system is fully operational: -- External MIDI keyboards/controllers can now play notes alongside QWERTY input -- Velocity-sensitive input works correctly -- Mono/poly modes, arpeggiator, and all effects work with MIDI input -- GUI provides easy port selection and enable/disable controls - -## Next Steps (Optional Enhancements) -- Add sustain pedal (CC64) support -- Support pitch bend and aftertouch -- Add MIDI learn for parameter mapping -- Support MIDI channel filtering -- Handle hot-plug device detection