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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ and `--audio-stereo-separation 0-100` narrows the Amiga's hardware left/right
panning (100 = full, 0 = mono). Both are also `[audio]` keys and configuration-
screen fields.

## Parallel port

The parallel port is a small device framework (`src/parallel.rs`): whatever is
plugged in -- currently an audio sampler, later perhaps a printer or a game's
protection dongle -- is offered the data lines on a `$BFE101` access. The one
device so far is an **8-bit audio sampler** (digitizer), the kind Amiga sampling
software (ProTracker, OctaMED, AudioMaster) records through. `--parallel sampler`
(or `[parallel] device = "sampler"`, or the launcher's Parallel tab) attaches it;
`--sampler-audio-input NAME` picks a host audio input (`--sampler-list-audio-inputs`
lists them, cpal again, so the same CoreAudio/WASAPI/ALSA path as audio), and
`--sampler-input-gain` sets a preamp. It is a mono sampler -- host left+right are
summed. The input and gain switch live from the runtime menu, and `Cmd+Shift +/-`
(`Alt+Shift +/-`) nudges the gain when a sampler is attached.

## MIDI

Copperline can bridge Paula's serial port to the host's MIDI system, so an
Expand Down
17 changes: 17 additions & 0 deletions copperline.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,23 @@ floppy_sounds_volume = 100
# stereo_separation = 100


# What is connected to the parallel port.
# [parallel]

# Device on the port: "none" (default, nothing connected) or "sampler", an 8-bit
# audio digitizer that the Amiga reads to record. Also `--parallel sampler`.
# device = "sampler"

# Sampler host capture device, matched by a case-insensitive substring against
# the names `--sampler-list-audio-inputs` prints. Omitted uses the system default
# input. Also `--sampler-audio-input`. A mono sampler: host left+right are summed.
# sampler_input = "External Mic"

# Sampler input gain, a linear multiplier (a preamp), 1.0 = unity, max 16. In the
# GUI and with Cmd/Alt+Shift +/- it steps in dB. Also `--sampler-input-gain`.
# sampler_gain = 1.0


# Host input preferences.
# [input]

Expand Down
25 changes: 25 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,31 @@ only the `default`/`pipewire` route is offered; pick the output in the desktop
sound settings (or route Copperline in `pavucontrol`) and it follows. macOS and
Windows select each device directly.

## `[parallel]`

What is connected to the parallel port.

```toml
[parallel]
device = "sampler" # "none" (default) or "sampler"
# sampler_input = "..." # host capture device (substring); omit = system default
# sampler_gain = 1.0 # linear input gain (preamp), max 16
```

`device = "sampler"` attaches an 8-bit audio digitizer -- the classic Amiga
parallel-port sampler (AMAS, DSS, the open-amiga-sampler and the like). Sampling
software (ProTracker, OctaMED, AudioMaster) reads it to record. It captures from
`sampler_input` (a host audio input, `--sampler-audio-input`, matched like the
audio device; the default input otherwise), summing left+right into the single
mono input, with `sampler_gain` as a preamp (`--sampler-input-gain`, or
Cmd/Alt+Shift +/- live). The device and its input/gain are also set on the
launcher's **Parallel** tab, and the input and gain switch live from the runtime
menu. Equivalent flags: `--parallel sampler`, `--sampler-audio-input`,
`--sampler-input-gain`, `--sampler-list-audio-inputs` (naming a sampler option
selects the sampler, like a MIDI endpoint selects `--serial midi`). The same
host-audio note above applies to inputs. Nothing here changes the emulated
machine; a host-IO device, not stored in save states.

## `[input]`

```toml
Expand Down
1 change: 1 addition & 0 deletions docs/guide/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The app shortcut modifier is `Cmd` on macOS and `Alt` on Linux/Windows.
| `Cmd+K` | `Alt+K` | Open the [debugger console](../debugger/console) |
| `Cmd+J` | `Alt+J` | Toggle joystick input mode: gamepad / keyboard (also the status-bar icon) |
| `Cmd+Shift+A` | `Alt+Shift+A` | Cycle the audio output: Default, each host device, then Disabled (also the menu's Audio Out item) |
| `Cmd+Shift +/-` | `Alt+Shift +/-` | Raise / lower the parallel-port sampler's input gain, 3 dB per step (only when a sampler is attached) |
| `Cmd+W` | `Alt+W` | Toggle Warp Speed (turbo) on / off |
| `Cmd+Shift+W` | `Alt+Shift+W` | Cycle the Warp Speed limit: 2x, 4x, 8x, 16x, Max |
| `Esc` | `Esc` | Close an open menu, tool window, or overlay panel; otherwise passed through to the Amiga |
Expand Down
13 changes: 7 additions & 6 deletions src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ extern "C" fn alsa_ignore_error(
/// a no-op error handler once, process-wide, keeping `--list-audio-devices` and
/// the picker readable. No-op off Linux, where the handler does not exist.
#[cfg(target_os = "linux")]
fn quiet_alsa_probe_logging() {
pub(crate) fn quiet_alsa_probe_logging() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
Expand All @@ -183,7 +183,7 @@ fn quiet_alsa_probe_logging() {
}

#[cfg(not(target_os = "linux"))]
fn quiet_alsa_probe_logging() {}
pub(crate) fn quiet_alsa_probe_logging() {}

/// Whether an ALSA device name is a low-level *plugin* handle rather than a
/// device a user would pick: the channel-layout plugins (`front`, `surround51`,
Expand All @@ -196,7 +196,7 @@ fn quiet_alsa_probe_logging() {}
/// off any card or device name -- and hidden handles are still selectable by
/// name in the config/CLI, since only the displayed list is filtered. A no-op on
/// macOS/Windows, whose device names never take this form.
fn is_alsa_plugin_variant(name: &str) -> bool {
pub(crate) fn is_alsa_plugin_variant(name: &str) -> bool {
let plugin = name.split(':').next().unwrap_or(name);
matches!(
plugin,
Expand Down Expand Up @@ -341,9 +341,10 @@ pub fn list_output_devices() -> Vec<String> {
/// Whether `name` is redundant with the GUI picker's own "Default" entry: it is
/// ALSA's `default` pseudo-device *and* that is what the system default resolves
/// to, so selecting it and selecting "Default" (the `None` option) do the same
/// thing. `default_name` is the host's default output device name. When the
/// default is some other device, `default` is a distinct choice and kept.
fn is_redundant_default(name: &str, default_name: Option<&str>) -> bool {
/// thing. `default_name` is the host's default device name (output or input).
/// When the default is some other device, `default` is a distinct choice and
/// kept. Shared with the sampler input picker ([`crate::sampler`]).
pub(crate) fn is_redundant_default(name: &str, default_name: Option<&str>) -> bool {
name.eq_ignore_ascii_case("default")
&& default_name.is_some_and(|d| d.eq_ignore_ascii_case("default"))
}
Expand Down
43 changes: 43 additions & 0 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,12 @@ pub struct Bus {
/// in hundredths. Default 200 (= 2.00 cck/word, the real 68000 figure);
/// diagnostic builds can override it for timing experiments.
dbg_ext_cck_x100: u32,
/// Optional device on the parallel port (see [`crate::parallel`]). When
/// attached, a CIA-A PRB access ($BFE101) is offered to it: a read may return
/// a driven byte (the sampler's ADC), a write is delivered to it. A host-IO
/// device, not machine state: never serialized, re-attached after a load.
#[serde(skip)]
parallel_device: Option<Box<dyn crate::parallel::ParallelPortDevice>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -2076,6 +2082,7 @@ impl Bus {
bus_accounting: BusAccounting::from_env(),
uhres_dual_warned: false,
dbg_ext_cck_x100: external_access_cck_x100_setting(),
parallel_device: None,
};
bus.configure_chip_dma_masks();
// Re-derive the per-frame capture buffers from the same helper the
Expand Down Expand Up @@ -2780,6 +2787,22 @@ impl Bus {
self.emulated_cck as f64 / PAULA_CLOCK_HZ as f64
}

/// Attach (or detach with `None`) a device on the parallel port. While one is
/// attached the CIA-A PRB access hooks (`cia_a_read`/`cia_a_write`) offer it
/// the data lines. A host-IO device, not machine state.
pub fn attach_parallel_device(
&mut self,
device: Option<Box<dyn crate::parallel::ParallelPortDevice>>,
) {
self.parallel_device = device;
}

/// Whether a parallel-port device is currently attached (drives the runtime
/// menu's sampler live-control items).
pub fn has_parallel_device(&self) -> bool {
self.parallel_device.is_some()
}

pub fn emulated_frames(&self) -> u64 {
self.emulated_frames
}
Expand Down Expand Up @@ -4107,6 +4130,19 @@ impl Bus {
// status lines: /CHNG, /WPRO, /TK0, /RDY.
v = (v & !0x3C) | self.floppy.cia_a_status_bits();
}
if reg == REG_PRB && self.parallel_device.is_some() {
// Offer the PRB read to the attached parallel-port device; if it
// drives the data lines (the sampler's ADC) its byte replaces the
// port register value.
let seconds = self.emulated_seconds();
if let Some(byte) = self
.parallel_device
.as_mut()
.and_then(|dev| dev.read_data(seconds))
{
v = byte;
}
}
trace!("cia_a R reg={:X} sz={} val={:02X}", reg, size, v);
self.poll_stats.tick_read("cia_a", reg);
v as u64
Expand All @@ -4118,6 +4154,13 @@ impl Bus {
let reg = reg_from_addr(addr);
trace!("cia_a W reg={:X} sz={} val={:02X}", reg, size, byte);
let eff = self.cia_a.write(reg, byte);
if reg == REG_PRB {
// Deliver PRB writes to an attached output device (a printer). The
// sampler ignores them (the trait default).
if let Some(dev) = self.parallel_device.as_mut() {
dev.write_data(byte);
}
}
if self.cia_a.irq_line_asserted() {
self.paula.intreq |= INT_PORTS;
}
Expand Down
32 changes: 32 additions & 0 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8887,6 +8887,38 @@ fn joystick_fire_drives_cia_a_pra_fir1() {
assert_eq!(bus.cia_a_read((REG_PRA as u64) * 256, 1) & 0x80, 0);
}

#[test]
fn parallel_device_drives_cia_a_prb() {
use crate::chipset::cia::REG_PRB;
use crate::parallel::ParallelPortDevice;

// A mock device driving the data lines, to prove a PRB read is routed to it.
struct MockDevice;
impl ParallelPortDevice for MockDevice {
fn read_data(&mut self, _emu_seconds: f64) -> Option<u8> {
Some(0x55)
}
fn label(&self) -> &str {
"mock"
}
}

let mut bus = empty_bus();
let prb = (REG_PRB as u64) << 8;

// No device attached: PRB reads the port register, never the driven byte.
let plain = bus.cia_a_read(prb, 1) as u8;
assert_ne!(plain, 0x55);

// Attached: the PRB read returns the device's byte.
bus.attach_parallel_device(Some(Box::new(MockDevice)));
assert_eq!(bus.cia_a_read(prb, 1) as u8, 0x55);

// Detaching restores the plain port read.
bus.attach_parallel_device(None);
assert_eq!(bus.cia_a_read(prb, 1) as u8, plain);
}

#[test]
fn joystick_button2_drives_pot1y_through_potgor() {
let mut bus = empty_bus();
Expand Down
Loading
Loading