Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ categories = ["emulators"]
publish = false

[features]
default = ["midi"]
display-plan-trace = []
# Local investigation switches that deliberately alter timing or rendering.
# Normal builds keep these environment-variable overrides inert so release
# behavior is hardware-derived and reproducible.
internal-diagnostics = []
# Host MIDI bridge for Paula's serial port (`[serial] mode = "midi"`). Built in
# by default; `--no-default-features` compiles it out, and a MIDI-free build
# pulls no MIDI code and links no MIDI framework. Native backends per platform:
# CoreMIDI (macOS), the ALSA sequencer (Linux), WinMM (Windows); other targets
# get a stub. See src/midi/.
midi = []

[dependencies]
# Path-only dependency on the in-tree fork (crates/m68k). No version requirement
Expand Down
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ The full reference -- every key, machine profiles, Zorro boards, CD/HDD
images, validation rules, and audio options -- is in the
[configuration guide](docs/guide/configuration.md).

## MIDI

Copperline can bridge Paula's serial port to the host's MIDI system, so an
Amiga sequencer or tracker plays real synths -- or is itself played from a
host MIDI keyboard -- over the emulated serial line. It is built in by default;
`cargo build --no-default-features` compiles it out, and such a build pulls no
MIDI code and links no MIDI framework.

The backend is selected at compile time and talks to each platform's native
API directly, with no wrapper crate: **CoreMIDI** on macOS, the **ALSA
sequencer** on Linux, and **WinMM** on Windows. Outgoing bytes are scheduled
so each one is delivered at the host instant it left the emulated wire,
keeping the guest's MIDI timing intact rather than collapsing it to whenever a
frame's worth of bytes is flushed.

List the host endpoints, then select them by name (a case-insensitive
substring is enough); `--midi-out`/`--midi-in` imply `--serial midi`:

```sh
./target/release/copperline --list-midi
./target/release/copperline --midi-out "FluidSynth" --midi-in "Keystation"
```

Devices can also be chosen in the launcher's **Serial** tab, swapped live from
the in-window **MIDI In / MIDI Out** menu, or set in the config file:

```toml
[serial]
mode = "midi" # off, stdout, or midi
midi_out = "FluidSynth" # host destination; substring match
midi_in = "Keystation" # host source
```

The ALSA development headers the Linux backend links against are already a
build requirement (see Requirements); macOS and Windows need nothing beyond
the OS.

## Documentation

User and developer documentation -- getting started, the UI and shortcuts,
Expand Down Expand Up @@ -217,7 +254,7 @@ release needs resolved first. Release steps for every channel are in
| ROM | Kickstart at $F80000 (512 KiB); optional extended ROM for CD32 ($E00000) and CDTV ($F00000). |
| Battery RTC | Read-only MSM6242-compatible register view at $DC0000; guest writes affect only emulated latch/control state. |
| CIA-A / CIA-B | I/O ports, /OVL, timers, TOD, keyboard SDR/ICR, disk control/status lines, and CIA-B FLAG disk index pulses. |
| Paula serial | SERDAT -> stdout through a one-word transmit buffer and timed shift register; SERDATR reports TBE/TSRE/RBF. |
| Paula serial | SERDAT through a one-word transmit buffer and timed shift register, out to stdout or -- with the optional `midi` feature -- bridged to host MIDI in/out; SERDATR reports TBE/TSRE/RBF, and serial receive is fed from the selected MIDI input. |
| Paula audio | 4-channel DMA/sample playback, stereo mix, LED filter. |
| Paula DMACON / INTENA / INTREQ | IRQ bits are stored and delivered through manual M68K autovectors with modelled 68000 interrupt-recognition latency; audio and disk DMA raise completion IRQs. |
| Floppy / ADF / DMS / SCP | DF0-DF3 standard DD ADF read/write, read-only ADZ/DMS, UAE extended ADF, initial read-only SCP flux import, track-timed disk DMA, CIA drive lines, index FLAG, DSKLEN/DSKBYTR/DSKSYNC/DSKDAT, per-drive multi-disk playlists with a swap key. |
Expand Down
16 changes: 16 additions & 0 deletions copperline.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@ floppy_sounds_volume = 100
# joystick = "auto"


# Serial port wiring. The Amiga serial port doubles as the MIDI port.
# [serial]
#
# mode selects where Paula's serial in/out is connected:
# "stdout" (default) serial output prints to the host terminal, matching
# the historical behaviour (DiagROM and similar tools log here).
# "off" serial output is discarded and there is no serial input.
# "midi" serial in/out is bridged to host MIDI endpoints. Needs a build
# with the `midi` feature; midi_out/midi_in name the endpoints
# (substring match, e.g. a USB interface or a virtual port).
# --serial MODE overrides this per run; --midi-out/--midi-in NAME imply "midi".
# mode = "stdout"
# midi_out = "USB MIDI Interface"
# midi_in = "USB MIDI Interface"


# Optional floppy drives. DF0 is always connected; set [floppy] drives
# to wire empty external mechanisms as well (1-4 total drives). Missing
# [floppy.dfN] tables mean no disk in that drive. Supported images:
Expand Down
46 changes: 46 additions & 0 deletions docs/internals/peripherals.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,49 @@ A `SerialSink` that can *produce* input (none of the built-in sinks do)
must override `has_pending_input` alongside `read_byte`/`read_word`:
Paula's per-tick UART step takes an idle fast path that skips the receiver
entirely while it reports false.

## MIDI serial bridge (`midi/`)

`[serial] mode = "midi"` (or `--midi-out`/`--midi-in`) bridges Paula's
serial port to host MIDI, behind the optional `midi` cargo feature -- a
plain build compiles none of it and the mode falls back with a clear
message. The whole thing hangs off one `SerialSink`, `MidiSerialSink`, so
the emulator core is unchanged from any other serial target.

The load-bearing detail is that byte timing survives to the wire. Paula
stamps each transmitted byte with the emulated colour clock it left on
(`SerialTimeAnchor`); `MidiSerialSink` maps that to a host `Instant` and
asks the backend to *schedule* the message for that instant rather than
send it now, so a frame's worth of bytes flushed together still leaves at
the original spacing. Two host-agnostic pieces sit above the backend: a
`MidiFramer` reassembles the single-byte serial stream into whole MIDI
messages (a receiver rejects lone data bytes), tracking running status and
SysEx and passing interleaved real-time bytes straight through; and Active
Sensing (`0xFE`) is forwarded by default -- a real Amiga passes it down the
wire -- and only dropped under `COPPERLINE_MIDI_STRIP_ACTIVE_SENSE=1`.
Input arrives on a lock-free SPSC ring the receiver drains on its idle
fast path, so the poll never locks.

The host connection lives behind the `MidiBackend` trait, chosen by
`cfg(target_os)`: macOS drives CoreMIDI (`coremidi.rs`), Linux the ALSA
sequencer (`alsa.rs`), and Windows WinMM (`winmm.rs`); any other target gets
`stub.rs`, which enumerates nothing and refuses to open. Each backend links its
platform library directly with no wrapper crate, and each maps the
scheduled send onto that platform's timed-delivery primitive: a CoreMIDI
packet timestamp, an ALSA real-time queue event, or -- since WinMM carries no
timestamp -- a scheduler thread that fires each message when it comes due. A
new backend implements `send`/`set_output`/`set_input`/`current_output`/`current_input`
plus free `enumerate`/`open`; nothing else changes. The raw FFI is
layout-sensitive -- CoreMIDI packs its packet list to 4 bytes, the ALSA
`snd_seq_event_t` scheduling helpers are header-only inlines whose field writes
are replicated by hand, and WinMM's `MIDIHDR` is packed -- so the mirrors are
pinned with compile-time layout assertions and want checking against live MIDI,
not just review.

Two debug knobs help tell a dead path from a routing one:
`COPPERLINE_MIDI_DEBUG=1` reports per-second tx/rx byte counts and the
first bytes sent (no tx while a song plays means the guest is not driving
serial, i.e. the fault is upstream of the bridge); `=2` decodes every
message in each direction. `COPPERLINE_MIDI_IMMEDIATE=1` bypasses
scheduling and sends each message for immediate delivery, to separate a
timing problem from a connection one.
19 changes: 18 additions & 1 deletion src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2714,6 +2714,20 @@ impl Bus {
self.emulated_cck
}

/// Publish the emulated-to-host time mapping to the serial sink so a
/// timing-sensitive backend (MIDI) can schedule output. See
/// [`crate::serial::SerialTimeAnchor`].
pub fn set_serial_time_anchor(&mut self, anchor: crate::serial::SerialTimeAnchor) {
self.paula.set_serial_time_anchor(anchor);
}

/// The live MIDI sink, when the serial port is in MIDI mode, for switching
/// devices from the runtime menu.
#[cfg(feature = "midi")]
pub fn midi_serial_mut(&mut self) -> Option<&mut crate::midi::MidiSerialSink> {
self.paula.serial.as_midi()
}

pub fn live_audio_output_lead_seconds(&self) -> f64 {
self.paula.live_audio_output_lead_seconds()
}
Expand Down Expand Up @@ -3659,7 +3673,10 @@ impl Bus {
self.slice_preempted = true;
}

self.paula.intreq |= self.paula.tick_serial(cck);
// emulated_cck already covers this span, so it is the color clock at
// the span's end. Paula uses it to stamp any serial byte that finishes
// here; passing it avoids arithmetic on the common (no byte) path.
self.paula.intreq |= self.paula.tick_serial(cck, self.emulated_cck);
self.paula.tick_pots(cck);
let dmacon = self.agnus.dmacon;
self.flush_audio();
Expand Down
2 changes: 1 addition & 1 deletion src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ fn render_color_write_x(hpos: u32) -> usize {
struct NoopSerial;

impl SerialSink for NoopSerial {
fn write_byte(&mut self, _b: u8) {}
fn write_byte(&mut self, _b: u8, _at_cck: u64) {}
fn flush(&mut self) {}
}

Expand Down
Loading
Loading