parallel: audio sampler, on a general parallel-port framework#138
parallel: audio sampler, on a general parallel-port framework#138hobbo91 wants to merge 1 commit into
Conversation
e24cc0e to
6dc6eab
Compare
Add a small device framework for the Amiga parallel port and its first device, an 8-bit audio sampler. The framework is the point: the port is modelled so future devices (a printer, a game's protection dongle) slot in without touching the core, and the sampler is a worked example. Framework: src/parallel.rs defines ParallelPortDevice (object-safe, no host backend types so the core stays testable). The bus holds an optional attached device and offers it the data lines on a CIA-A PRB access ($BFE101): a CPU read calls read_data -- a device that drives the lines returns the byte, others return None and the port register value stands -- and a CPU write calls write_data, which an output device consumes and others ignore. A new device is a [parallel] device value (like [serial] mode), a trait impl, and an attach point. Host-IO, not machine state: serde-skipped on the bus, re-attached after a state load, so no STATE_VERSION change and existing saves keep loading. Sampler: --sampler / [parallel] device = "sampler" attaches the digitizer the classic Amiga samplers (AMAS, DSS, Megalosound, the open-amiga-sampler) are -- an ADC on the data lines that sampling software (ProTracker, OctaMED, AudioMaster) reads in a tight loop to record. Captured from a host audio input through cpal (same CoreAudio/WASAPI/ALSA path as audio output; --sampler-input, --list-audio-inputs), modelled as WinUAE's sampler.cpp does: a capture stream fills a ring in real time, and each PRB read returns the sample for the elapsed emulated time, so the input lines up however fast/slow the Amiga polls. 8-bit offset-binary (128 = silence). These units are mono, so host L+R are summed into the single input (a stereo source is captured whole). --sampler-gain is a linear preamp (max 16x/~+24 dB). GUI: a Parallel tab (below Serial) with a Device picker (None / Sampler); the sampler's Audio input and Input gain (in dB) rows appear only when Sampler is selected, so a future device's options never clutter the tab. The runtime menu gains Sampler In and Sampler Gain items (only while a sampler is attached) that switch the input device and gain live -- the capture stream is rebuilt on the main thread (the cpal Stream is !Send on macOS). Cmd/Alt+Shift +/- nudges the gain 3 dB when attached. The same dynamic-rows treatment is applied to the Serial tab: the MIDI in/out pickers appear only in MIDI mode instead of being greyed out otherwise. Host input handling mirrors the audio output picker: on Linux the GUI drops ALSA's redundant "default" when it is the system default input (kept in the CLI list), and the picker re-enumerates on demand so a newly connected input shows up without a restart. Individual PipeWire/PulseAudio sinks are not ALSA devices, so only the default route is offered there; macOS/Windows select each directly. Docs: README "Parallel port", configuration guide [parallel], example TOML, --help. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6dc6eab to
f25668f
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a small, extensible parallel-port device framework and wires up its first device: an 8‑bit audio sampler (digitizer) that sources samples from a host audio input via cpal. It also integrates the new parallel/sampler functionality end-to-end across config parsing, CLI flags, the launcher UI, and the runtime menu/shortcuts.
Changes:
- Add a parallel-port device abstraction (
ParallelPortDevice) and bus hooks for CIA-A PRB reads/writes. - Implement a
cpal-backed parallel-port audio sampler device with input device selection and gain handling. - Expose the sampler via
[parallel]config, CLI options, and UI (launcher tab + runtime menu + shortcuts).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/video/window/tests.rs | Updates window tests to pass the new sampler request through App construction. |
| src/video/window.rs | Adds sampler session state + runtime controls (menu + shortcuts) and (re)attachment logic. |
| src/video/ui.rs | Extends runtime menu plumbing and hit-testing to include sampler controls when active. |
| src/video/launcher.rs | Adds a Parallel tab with dynamic rows and sampler input/gain configuration support. |
| src/sampler.rs | New sampler device implementation: host capture ringbuffer + emulated-time consumption. |
| src/parallel.rs | New parallel-port device trait defining read/write hooks and device labeling. |
| src/main.rs | Adds CLI flags for parallel device selection and sampler input/gain + listing inputs. |
| src/lib.rs | Exposes new parallel and sampler modules. |
| src/config.rs | Adds [parallel] config model, raw serialization, parsing, and CLI override plumbing. |
| src/bus/tests.rs | Adds unit coverage proving CIA-A PRB reads route through an attached parallel device. |
| src/bus.rs | Adds optional parallel device storage and PRB read/write dispatch hooks (serde-skipped). |
| src/audio.rs | Makes ALSA filtering helpers usable by the sampler input picker/listing paths. |
| README.md | Documents the new parallel-port framework and sampler usage at a high level. |
| docs/guide/ui.md | Documents the sampler gain shortcut. |
| docs/guide/configuration.md | Documents [parallel] configuration options and equivalently-named CLI flags. |
| copperline.example.toml | Adds example [parallel] sampler configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn sample_to_byte(v: f32) -> u8 { | ||
| let scaled = (v.clamp(-1.0, 1.0) * 127.0).round() as i32 + 128; | ||
| scaled.clamp(0, 255) as u8 | ||
| } |
There was a problem hiding this comment.
Good spot, will clean that up
| assert_eq!(sample_to_byte(-1.0), 1); | ||
| // Clamps beyond full-scale rather than wrapping. | ||
| assert_eq!(sample_to_byte(2.0), 255); | ||
| assert_eq!(sample_to_byte(-2.0), 1); |
| } else { | ||
| (String::new(), String::new()) | ||
| }; | ||
| let sampler_active = self.emu.bus().has_parallel_device(); |
| fn step_sampler_gain(&mut self, delta_db: f32, wrap: bool) { | ||
| if !self.emu.bus().has_parallel_device() { | ||
| return; | ||
| } |
| // 7 leading + up to 2 MIDI + up to 3 sampler + 10 trailing items, sized so | ||
| // appending never reallocates. |
There was a problem hiding this comment.
Noted - was originally going to have an option for stereo/mono but realised it was a lot more complexity for such a niche feature.
| // Parallel port. Emit the device only when a sampler is selected (None is | ||
| // the default), and the sampler sub-options only then, minimally. | ||
| if self.parallel_device == ParallelDevice::Sampler { | ||
| raw.parallel.device = Some(ParallelDevice::Sampler.label().to_string()); | ||
| raw.parallel.sampler_input = self.sampler_input.clone(); | ||
| raw.parallel.sampler_gain = (self.sampler_gain != 1.0).then_some(self.sampler_gain); | ||
| } |
| assert_eq!(s.value_label(LauncherField::ParallelDevice), "None"); | ||
| assert_eq!(s.to_raw().parallel.device, None); | ||
|
|
||
| // Sampler: its four rows appear and it round-trips. |
There was a problem hiding this comment.
Similar to the above I ended up dropping channel mode
| self.ui.control_at( | ||
| pos, | ||
| self.serial_is_midi, | ||
| self.emu.bus().has_parallel_device(), | ||
| ) |
There was a problem hiding this comment.
Ack, will clean this up
Thanks, always bugged me it never worked properly in WinUAE/AmiBerry, and AFAIK none of the other Amiga emulators support it at all - Copperline is perfect for this sort of feature (same with MIDI). I do appreciate 99% of people who use this will probably be to play games / not make music, so wanted to implement in a way where someone could add other devices to the parallel port in the future. I'm in Greece for the next 10 days, and as per the promise to my girlfriend didn't bring my laptop lol, but I'll address the comments as soon as I'm back. |
Happy to support this kind of thing.
I want this to be for everyone, not just for gamers. I figured if I get things right, I can use it to prototype hardware and firmware before I even design it.
No rush at all. Enjoy the holiday! |
Add a small device framework for the Amiga parallel port and its first device, an 8-bit audio sampler.
The framework is the point: the port is modelled so future devices (a printer, a game's protection dongle.etc) slot in without touching the core, and the sampler is a working example.
Framework
src/parallel.rsdefinesParallelPortDevice(object-safe, host-backend-free so the core stays testable). The bus holds an optional attached device and offers it the data lines on a CIA-A PRB access ($BFE101): a CPU read callsread_data-- a device that drives the lines returns the byte, others returnNoneand the port register value stands -- and a CPU write callswrite_data, which an output device consumes and others ignore. It is host-IO, not machine state: serde-skipped on the bus, re-attached after a state load, so no STATE_VERSION change and existing saves keep loading.Both the config and the CLI mirror
[serial]:[parallel] device = "none" | "sampler"(or--parallel DEVICE) selects the device, then device-prefixed options configure it. A future printer is--parallel printer+--printer-*, no clashes.Audio sampler
--parallel sampler(or[parallel] device = "sampler") attaches the digitizer the classic Amiga samplers (AMAS, DSS, Megalosound, the open-amiga-sampler) are -- an ADC on the data lines that sampling software reads in a tight loop to record. It captures from a host audio input through cpal (same CoreAudio/WASAPI/ALSA path as the audio output;--sampler-audio-input NAME,--sampler-list-audio-inputs), and each PRB read returns the sample for the elapsed emulated time, so the input lines up however fast/slow the Amiga polls. The value is 8-bit offset-binary (128 = silence). These are mono units, so host left+right are summed into the single input -- which also means a stereo source is captured whole.--sampler-input-gainis a linear preamp (max 16x/~+24 dB). Naming a sampler option selects the sampler, as a MIDI endpoint selects--serial midi.GUI
A Parallel tab (below Serial): a Device picker (None / Sampler); the sampler's Audio input and Input gain (in dB) rows appear only when Sampler is selected, so a future device's options never clutter the tab. The runtime menu gains Sampler In and Sampler Gain items (only while a sampler is attached) that switch the input device and gain live -- the capture stream is rebuilt on the main thread (the cpal
Streamis!Sendon macOS).Cmd+Shift +/-(Alt+Shift +/-) nudges the gain 3 dB when attached. While here, the same dynamic-rows treatment is applied to the Serial tab: the MIDI in/out pickers now appear only in MIDI mode instead of being greyed out otherwise.Host input handling
Mirrors the audio output picker: on Linux the GUI drops ALSA's redundant "default" when it is the system default input (kept in the CLI list), and the picker re-enumerates on demand so a newly connected input shows up in the GUI/menu without a restart. Individual PipeWire/PulseAudio sinks aren't ALSA devices, so only the default route is offered there; macOS/Windows select each directly. (macOS note: a bare CLI binary can't prompt for the microphone permission -- grant the terminal mic access, or use a virtual loopback; a bundled .app would carry the usage description.)
Credit and licensing
The sampler's emulation approach -- a real-time capture ring read by elapsed emulated time, 8-bit offset-binary, the latency trim -- is an independent Rust implementation following the method in WinUAE's
sampler.cpp(Toni Wilen); no code was copied. WinUAE is GPL-2.0-or-later, compatible with this GPL-3.0-or-later project. The hardware model (an ADC0820-style 8-bit ADC on the data lines, mono) follows the open-amiga-sampler project's schematics (github.com/echolevel/open-amiga-sampler). Both are credited in the source.Verification
-D warnings+ fmt clean, default and--no-default-features; 1323 tests, 0 failures. STATE_VERSION unchanged.[parallel]config parse/override (incl. the sampler-selects-implicitly rule), and the launcher's dynamic Serial/Parallel rows are unit-tested.[parallel],copperline.example.toml, and--help.Screens