Skip to content

parallel: audio sampler, on a general parallel-port framework#138

Open
hobbo91 wants to merge 1 commit into
LinuxJedi:mainfrom
hobbo91:feature/parallel-sampler
Open

parallel: audio sampler, on a general parallel-port framework#138
hobbo91 wants to merge 1 commit into
LinuxJedi:mainfrom
hobbo91:feature/parallel-sampler

Conversation

@hobbo91

@hobbo91 hobbo91 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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.rs defines ParallelPortDevice (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 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. 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-gain is 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 Stream is !Send on 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

  • clippy -D warnings + fmt clean, default and --no-default-features; 1323 tests, 0 failures. STATE_VERSION unchanged.
  • Recording verified on macOS with ProTracker, OctaMED, AudioMaster and TurboSound, capturing both from an external USB audio interface and via inter-app routing (BlackHole on macOS; a virtual audio cable on Windows). The bus PRB read/write routing, mono ADC conversion, [parallel] config parse/override (incl. the sampler-selects-implicitly rule), and the launcher's dynamic Serial/Parallel rows are unit-tested.
  • Docs: README "Parallel port", configuration guide [parallel], copperline.example.toml, and --help.

Screens

Screenshot 2026-07-06 at 16 40 59 Screenshot 2026-07-06 at 13 55 09 Screenshot 2026-07-06 at 13 54 54 Screenshot 2026-07-06 at 13 57 59

@hobbo91 hobbo91 force-pushed the feature/parallel-sampler branch from e24cc0e to 6dc6eab Compare July 6, 2026 16:48
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/sampler.rs
Comment on lines +77 to +80
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
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot, will clean that up

Comment thread src/sampler.rs
Comment on lines +371 to +374
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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Comment thread src/video/window.rs
} else {
(String::new(), String::new())
};
let sampler_active = self.emu.bus().has_parallel_device();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Comment thread src/video/window.rs
Comment on lines +1122 to +1125
fn step_sampler_gain(&mut self, delta_db: f32, wrap: bool) {
if !self.emu.bus().has_parallel_device() {
return;
}
Comment thread src/video/ui.rs
Comment on lines +107 to +108
// 7 leading + up to 2 MIDI + up to 3 sampler + 10 trailing items, sized so
// appending never reallocates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted - was originally going to have an option for stereo/mono but realised it was a lot more complexity for such a niche feature.

Comment thread src/video/launcher.rs
Comment on lines +989 to +995
// 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);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Comment thread src/video/launcher.rs
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above I ended up dropping channel mode

Comment thread src/video/window.rs
Comment on lines +2442 to +2446
self.ui.control_at(
pos,
self.serial_is_midi,
self.emu.bus().has_parallel_device(),
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, will clean this up

@LinuxJedi LinuxJedi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks @hobbo91, cool idea.

Can you please take a look at the Copilot comments, I think mostly minor, but some probably should be addressed.

@hobbo91

hobbo91 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Many thanks @hobbo91, cool idea.

Can you please take a look at the Copilot comments, I think mostly minor, but some probably should be addressed.

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.

@LinuxJedi

Copy link
Copy Markdown
Owner

Many thanks @hobbo91, cool idea.
Can you please take a look at the Copilot comments, I think mostly minor, but some probably should be addressed.

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).

Happy to support this kind of thing.

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 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.

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.

No rush at all. Enjoy the holiday!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants