badline 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CLAUDE.md +3 -1
  4. data/README.md +132 -247
  5. data/doc/library.md +62 -0
  6. data/doc/media.md +69 -0
  7. data/doc/sid-player.md +132 -0
  8. data/doc/snapshots.md +69 -0
  9. data/exe/badline-ruby +9 -1
  10. data/lib/badline/address_bus/extra_sids.rb +30 -0
  11. data/lib/badline/address_bus/sid_slots.rb +42 -0
  12. data/lib/badline/address_bus.rb +5 -0
  13. data/lib/badline/audio/aiff.rb +2 -2
  14. data/lib/badline/audio/bare_player.rb +23 -12
  15. data/lib/badline/audio/cli.rb +153 -58
  16. data/lib/badline/audio/jukebox.rb +121 -36
  17. data/lib/badline/audio/machine_player.rb +25 -10
  18. data/lib/badline/audio/pcm_writer.rb +10 -6
  19. data/lib/badline/audio/playback.rb +6 -5
  20. data/lib/badline/audio/queued_tune.rb +89 -0
  21. data/lib/badline/audio/renderer.rb +57 -10
  22. data/lib/badline/audio/stereo.rb +97 -0
  23. data/lib/badline/audio/terminal.rb +39 -6
  24. data/lib/badline/audio/wav.rb +1 -1
  25. data/lib/badline/audio.rb +2 -0
  26. data/lib/badline/cartridge/freezer.rb +2 -0
  27. data/lib/badline/cartridge.rb +3 -0
  28. data/lib/badline/frontend/app.rb +38 -35
  29. data/lib/badline/frontend/audio_sink.rb +6 -5
  30. data/lib/badline/frontend/boot.rb +9 -2
  31. data/lib/badline/frontend/buttons.rb +184 -0
  32. data/lib/badline/frontend/confirmation.rb +50 -0
  33. data/lib/badline/frontend/controls.rb +44 -32
  34. data/lib/badline/frontend/file_browser.rb +225 -0
  35. data/lib/badline/frontend/filter_view.rb +96 -0
  36. data/lib/badline/frontend/frame_report.rb +52 -0
  37. data/lib/badline/frontend/info_view.rb +120 -0
  38. data/lib/badline/frontend/menu_dialogs.rb +122 -0
  39. data/lib/badline/frontend/menu_media.rb +128 -0
  40. data/lib/badline/frontend/menu_pages.rb +198 -0
  41. data/lib/badline/frontend/painter.rb +185 -0
  42. data/lib/badline/frontend/pause_menu.rb +246 -0
  43. data/lib/badline/frontend/player_commands.rb +33 -0
  44. data/lib/badline/frontend/player_footer.rb +86 -0
  45. data/lib/badline/frontend/player_header.rb +60 -0
  46. data/lib/badline/frontend/player_screen.rb +124 -0
  47. data/lib/badline/frontend/player_state.rb +67 -0
  48. data/lib/badline/frontend/player_window.rb +228 -0
  49. data/lib/badline/frontend/scope.rb +42 -0
  50. data/lib/badline/frontend/sid_history.rb +175 -0
  51. data/lib/badline/frontend/sid_view.rb +140 -0
  52. data/lib/badline/frontend/snapshots.rb +1 -1
  53. data/lib/badline/frontend/sound.rb +0 -1
  54. data/lib/badline/frontend/timeline.rb +5 -1
  55. data/lib/badline/frontend/visualizer_view.rb +101 -0
  56. data/lib/badline/frontend.rb +21 -0
  57. data/lib/badline/media/disk_set.rb +108 -0
  58. data/lib/badline/media/queue.rb +158 -0
  59. data/lib/badline/media.rb +14 -8
  60. data/lib/badline/options/help.rb +34 -5
  61. data/lib/badline/options/option.rb +1 -1
  62. data/lib/badline/options/table.rb +5 -3
  63. data/lib/badline/options/validation.rb +54 -0
  64. data/lib/badline/options.rb +65 -46
  65. data/lib/badline/sdl.rb +16 -6
  66. data/lib/badline/sid/catch_up.rb +11 -1
  67. data/lib/badline/sid/filter.rb +9 -0
  68. data/lib/badline/sid/voice.rb +6 -0
  69. data/lib/badline/sid/waveform.rb +20 -5
  70. data/lib/badline/sid.rb +35 -3
  71. data/lib/badline/storage/hvsc.rb +41 -0
  72. data/lib/badline/storage/sid_file.rb +45 -25
  73. data/lib/badline/storage/song_lengths.rb +6 -29
  74. data/lib/badline/storage/stil/credit.rb +50 -0
  75. data/lib/badline/storage/stil.rb +126 -0
  76. data/lib/badline/storage.rb +8 -0
  77. data/lib/badline/version.rb +1 -1
  78. metadata +36 -1
data/doc/media.md ADDED
@@ -0,0 +1,69 @@
1
+ # Media
2
+
3
+ The [README](../README.md#media) lists the formats badline takes. This
4
+ page covers how device 8 and the true 1541 work, and how to attach media
5
+ from Ruby.
6
+
7
+ ## Device 8 without a drive
8
+
9
+ Apart from a `.g64`, which only a true drive can read, there is no 1541
10
+ unless `--true-drive` asks for one. Device 8 works by trapping the
11
+ KERNAL's `LOAD` and `SAVE` routines and its serial bus primitives, so
12
+ files open by name through `OPEN` and `CHRIN` as well. `LOAD"$",8` lists
13
+ the directory of any medium mounted there, as a 1541 does.
14
+
15
+ The command channel answers `I`, `B-P` and `U1` block reads, which
16
+ covers loaders that read blocks directly. On a writable disk image it
17
+ also takes `SAVE` (with `@0:` to replace a file), files opened for
18
+ writing or appending, `S` to scratch, `U2` and `B-W` block writes, and
19
+ `B-A` and `B-F`. It doesn't rename, copy, format or validate disks.
20
+ Loaders that upload their own code to the drive with `M-W` and `M-E`,
21
+ and copy protection that reads raw GCR, won't work there, but do on a
22
+ true drive.
23
+
24
+ A directory mounted as device 8 serves the `.prg` and `.p00` files in it
25
+ and the contents of any `.t64`, and `SAVE` writes a new `.prg`.
26
+
27
+ ## The true 1541
28
+
29
+ `--true-drive` puts an emulated 1541 on device 8 instead, running its
30
+ own DOS ROM on its own 6502 and talking to the machine over the serial
31
+ bus. It reads `.d64` and `.g64` images, which it autostarts with the
32
+ same `LOAD"*",8,1` and `RUN`, but not `.d71`, `.d81` or `.t64` images
33
+ or directories. Loading runs at the speed of a real 1541, and the
34
+ drive's red LED lights in the bottom right corner of the border.
35
+
36
+ A `.g64` holds the disk's raw GCR, half tracks and all, so copy
37
+ protection and fast loaders that read it work.
38
+
39
+ ## Write protection
40
+
41
+ Disks given on the command line, inserted from the pause menu or by an
42
+ `--at` insert go in write-protected, so the drive reports
43
+ `26,WRITE PROTECT ON` for any write and the image file stays as it was.
44
+ `--writable`, or WRITABLE on the pause menu's Drive page, lets writes go
45
+ straight back to the image file. An image the host can't write, and a
46
+ `.t64`, stay write-protected.
47
+
48
+ ## From Ruby
49
+
50
+ `Badline::Media.attach(computer, path)` attaches any medium the
51
+ command line takes, and autostarts it unless given `autostart: false`.
52
+ `Badline::Media.insert_disk(computer, path)` swaps the disk image or
53
+ directory in device 8 while the machine runs, for software that asks for
54
+ another disk. Both write to disk images unless told otherwise:
55
+ `insert_disk` takes `read_only: true`, and `attach` takes
56
+ `disk: { read_only: true }`, as the test harnesses under `bin/` do. A
57
+ `.g64` goes in the true 1541, and takes out a disk mounted through the
58
+ traps, so `LOAD` and `SAVE` reach the 1541 too. With the 1541 in device
59
+ 8, a `.d64` goes in its drive as well.
60
+
61
+ `Badline::Media::DiskSet.around(path)` lists the disks of the set a disk
62
+ image belongs to, found by name in its folder, which the pause menu's
63
+ PREVIOUS DISK and NEXT DISK step through.
64
+
65
+ ## The light pen
66
+
67
+ Control port 1's fire line is also the VIC-II's light pen input, so
68
+ joystick 1's fire button and the 1351's left button in port 1 latch the
69
+ light pen registers.
data/doc/sid-player.md ADDED
@@ -0,0 +1,132 @@
1
+ # The SID player
2
+
3
+ The [README](../README.md#playing-and-rendering-sid-tunes) shows how to
4
+ start it. This page covers the player in detail.
5
+
6
+ ```sh
7
+ badline sid ~/C64Music/MUSICIANS/H/Hubbard_Rob # play every tune below a directory
8
+ badline sid tune.sid other.sid # play a queue of tunes
9
+ badline --headless tune.sid # play, length from HVSC
10
+ badline --headless -s 3 tune.sid # play the third subtune
11
+ badline --headless --all-subtunes tune.sid # play on through every subtune
12
+ badline --seconds 180 tune.sid --audio-out out.aiff
13
+ badline -s 3 --rate 48000 tune.sid --audio-out out.wav
14
+ badline --headless --sid 8580 tune.sid
15
+ badline --filter-chunk 1 tune.sid --audio-out out.wav # exact filter, slower
16
+ ```
17
+
18
+ ## The queue
19
+
20
+ `badline sid FILE|DIR...` plays a queue of tunes in the SID player's
21
+ window, in the order given, and a directory adds every `.sid` tune below
22
+ it in path order. `badline sid --headless` plays the queue in the
23
+ terminal instead. `badline sid` on its own opens the window with an
24
+ empty queue. Dropping `.sid` files or folders on the window adds them
25
+ to the end of the queue, and an empty queue starts playing them. A file
26
+ that isn't a tune is skipped.
27
+
28
+ `badline tune.sid` plays the tune in the SID player too, unless an
29
+ option of the emulator's window, such as `--ntsc` or `--reu`, asks for
30
+ the machine: then a tune for one SID runs on the emulated C64, started
31
+ through a small driver after boot.
32
+
33
+ `--subtune` (or `-s`) picks the first tune's subtune, counting from 1 as
34
+ HVSC does, and defaults to the tune's own start subtune. Each tune plays
35
+ that one subtune, and when it ends the player goes on to the next tune,
36
+ stopping after the last. `a`, or `--all-subtunes`, turns on playing all
37
+ subtunes, so that a subtune that ends goes on to the tune's next
38
+ subtune, and a tune stepped to starts on its first subtune.
39
+
40
+ `--sid auto`, the default, fits each of a tune's SIDs the model its
41
+ header names. A tune written for 2 or 3 SIDs plays on as many, at the
42
+ addresses its header gives, in stereo: SID 1 on the left, SID 2 on the
43
+ right and SID 3 in the centre. A tune on one SID plays the same on both
44
+ channels.
45
+
46
+ Playback asks the device for 44.1 kHz and takes whatever rate it
47
+ offers, unless `--rate` says otherwise. Ctrl-C stops it. The emulator
48
+ window's own options, `--no-autostart`, `--writable`, `--sound`,
49
+ `--true-drive`, `--reu`, `--ntsc` and `--verbose`, don't apply.
50
+
51
+ ## The window
52
+
53
+ The window is worked with the mouse. Its header shows the tune's name,
54
+ author and release, the tune the subtune covers at the moment when
55
+ HVSC's STIL credits one, following the times STIL gives, and buttons
56
+ that switch between three views and between the tune's own SID model,
57
+ the 6581 and the 8580, which changes the chips playing on the spot. Its
58
+ footer shows the time played on a bar you can click to seek, and buttons
59
+ that pause, step between tunes and between a tune's subtunes, and turn
60
+ shuffle, looping and all subtunes on and off.
61
+
62
+ - The visualizer shows each voice's note, and how far off it is in
63
+ cents, over a scope of the voice's output, and the mixed output below
64
+ them. A tune on more than one SID gets a row of voices for each SID,
65
+ and the mix splits into its left and right.
66
+ - The SID view shows one SID at a time, and for a tune on more than one,
67
+ a row of SID 1, 2 and 3 buttons at its top picks which. It shows each
68
+ voice's output, its control bits, pulse width, envelope settings, and
69
+ the envelope's level and stage, and the filter's modes, cutoff,
70
+ resonance, volume and its response on the chip playing.
71
+ - The INFO view shows the tune's STIL entry and the subtune's, with a
72
+ scroll bar, the mouse wheel or the up and down keys for the long ones.
73
+
74
+ The terminal's keys work in the window too, along with Tab to switch
75
+ views and `c` to step through the SID models.
76
+
77
+ ## The terminal
78
+
79
+ Played on a terminal, `--headless` and `sid` show each tune's name,
80
+ author and release as it starts, then the tune's place in the queue, the
81
+ subtune number and the time played against the subtune's length.
82
+ `--headless` queues just the one tune.
83
+
84
+ → and ← step to the tune's next and previous subtune, stopping at its
85
+ first and last. `n` and `p` step to the next and previous tune. `s`
86
+ turns shuffle on and off, which plays the queue's tunes in a random
87
+ order, and `l` turns looping on and off, so that the end of the queue
88
+ goes on to its start. `,` and `.` seek 10 seconds back and forward
89
+ within the subtune: a seek forward runs on silently from where the
90
+ subtune is, and a seek back plays it again from its start up to the
91
+ point asked for. Space pauses and `q` quits. The status line shows which
92
+ modes are on, and they last until the player quits.
93
+
94
+ `--no-tui`, or output that isn't a terminal, gives plain progress output
95
+ instead and plays through the queue without the keys.
96
+
97
+ ## Rendering
98
+
99
+ `--audio-out` renders one subtune to a 16-bit stereo PCM file. The
100
+ file's extension picks the format, `.wav` or `.aiff`.
101
+
102
+ ## Lengths and STIL
103
+
104
+ A `.sid` file doesn't store its length, so the player looks the tune up
105
+ by MD5 in HVSC's `Songlengths.md5`. It finds the database through
106
+ `--songlengths`, in a `DOCUMENTS` directory in any of the tune's parent
107
+ directories (the layout of an HVSC collection), or under
108
+ `$HVSC_BASE/DOCUMENTS`. Without a database or `--seconds` it runs for 60
109
+ seconds, but a subtune that falls silent for 5 seconds before then ends
110
+ there, when played and when rendered alike. Silent means the output
111
+ holds within 16 steps of one level, since a 6581 idles at a DC offset
112
+ rather than at zero. A subtune with a known length plays to its length
113
+ whatever it sounds like.
114
+
115
+ A tune in an HVSC collection also gets its entry in HVSC's `STIL.txt`,
116
+ found the same way: comments, covers, and subtune names and composers.
117
+ The tune's own fields follow its header, and each subtune's print as it
118
+ starts.
119
+
120
+ ## Speed
121
+
122
+ `badline` plays PSID and RSID tunes in real time. In `badline-ruby`,
123
+ PSID tunes run on a CPU and RAM with only the SID clocked, at about
124
+ twice real time, so they play smoothly, but RSID tunes set up their own
125
+ interrupts, so they boot a full C64 first and run at about half real
126
+ time. They render fine but stutter when played, and `badline-ruby` says
127
+ so when it falls behind. Both builds run the same code, so they render
128
+ the same file sample for sample. See
129
+ [native/README.md](../native/README.md#without-the-window).
130
+
131
+ The filter steps four cycles at a time; `--filter-chunk 1` steps it
132
+ every cycle, which is exact and takes about twice as long.
data/doc/snapshots.md ADDED
@@ -0,0 +1,69 @@
1
+ # Snapshots
2
+
3
+ In the window, `F11` saves the whole machine to a new
4
+ `badline-<date>-<time>.vsf` in the working directory, and `F12` goes
5
+ back to the snapshot last saved or opened, in a new machine built as the
6
+ saved one was. Both executables do this, and both open a `.vsf` given as
7
+ the media. A snapshot that fails to open leaves the machine running as
8
+ it was.
9
+
10
+ ## From Ruby
11
+
12
+ `computer.save_snapshot(path)` saves, `computer.restore_snapshot(path)`
13
+ takes a machine back to a snapshot, and `Badline::Snapshot.load(path)`
14
+ builds a new machine as the saved one was built and restores it.
15
+ `computer.snapshot` and `computer.restore(state)` do the same in memory,
16
+ without a file. A restore that fails leaves the machine as it was.
17
+
18
+ ## What a snapshot holds
19
+
20
+ A snapshot holds the machine as it was on the cycle it was saved: the
21
+ chips down to the instruction step and the pixel pipeline, the RAM and
22
+ any +60K or +256K expansion, the cartridge with its RAM and flash, a
23
+ GEO-RAM, a true 1541 with its RAM, its VIAs and the disk under the head,
24
+ a disk or directory mounted through the traps with its open channels,
25
+ and the tape with its place on it. A restored machine runs on exactly
26
+ as the saved one would have.
27
+
28
+ A directory, a tape and a true drive's disk image open again from their
29
+ paths when the snapshot is restored, the disk with its tracks as the
30
+ drive last saw them. A disk image mounted through the traps comes back
31
+ with its contents from the snapshot. What the host holds stays the
32
+ host's: the keyboard, the joysticks, the mouse and paddles, sound, and
33
+ blocks given to `on_init` that hadn't run yet, which a restore reports.
34
+
35
+ A snapshot only restores in the badline version that wrote it, into a
36
+ machine with the same chip models, RAM expansion and REU.
37
+ `computer.snapshot` holds an REU's RAM, registers and transfer too, but
38
+ a machine with an REU doesn't save to a file yet, as badline doesn't
39
+ write VICE's REU module. Nor does an NTSC machine, as badline doesn't
40
+ write an NTSC VIC-II in VICE's terms yet.
41
+
42
+ ## VICE
43
+
44
+ Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
45
+ the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, plus the
46
+ ones x64sc needs to open the file, with nothing attached to the
47
+ cartridge, tape or user ports and no true drive. A `BADLINE` module,
48
+ which VICE skips, holds the whole machine, and badline restores its own
49
+ snapshots from it.
50
+
51
+ x64sc 3.10 opens badline's snapshots, taken at the end of the
52
+ instruction the CPU was in, as long as its VIC-II model matches
53
+ (`-model c64` for the default machine). It keeps its own drives and
54
+ leaves out the cartridge, the expansions and the tape.
55
+
56
+ Snapshots x64sc saves open in badline through the same modules: the CPU
57
+ at its instruction boundary, RAM, the CPU port, the CIAs' registers,
58
+ timers and clocks, the SID's registers and reSID voice state, and the
59
+ VIC-II's registers, beam position, counters and colour RAM. The VIC-II's
60
+ pixel pipeline starts empty. badline reads the modules x64sc 3.7 to 3.10
61
+ write, and VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A
62
+ module version it doesn't know is left out, or fails the restore for the
63
+ CPU and RAM. It reports the modules it leaves out, such as the 1541
64
+ drives, the cartridge, the datasette and the keyboard, and carries on
65
+ without them. A snapshot of an NTSC machine fails, as badline reads only
66
+ VICE's PAL VIC-II. Restored into a running machine, a VICE snapshot
67
+ fails for a machine built another way, such as a C64C's snapshot in a
68
+ C64, and otherwise takes the cartridge out and switches the machine off
69
+ and on.
data/exe/badline-ruby CHANGED
@@ -36,13 +36,21 @@ end
36
36
 
37
37
  if options.headless?
38
38
  require "badline/audio/console"
39
+ console = if options.player_window?
40
+ lambda do |input:, output:|
41
+ load_sdl
42
+ Badline::Frontend::PlayerWindow.new(input:, output:)
43
+ end
44
+ else
45
+ Badline::Audio::Console.method(:new)
46
+ end
39
47
  sink = lambda do |rate:, exact_rate:|
40
48
  load_sdl
41
49
  Badline::Frontend::AudioSink.new(rate:, exact_rate:)
42
50
  rescue Badline::FFI::Error => e
43
51
  raise Badline::Audio::Playback::DeviceError, e.message
44
52
  end
45
- exit Badline::Audio::CLI.run(options, sink:, console: Badline::Audio::Console.method(:new))
53
+ exit Badline::Audio::CLI.run(options, sink:, console:)
46
54
  end
47
55
 
48
56
  begin
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class AddressBus
5
+ # The second and third SIDs the SID player fits for a tune written for
6
+ # them, each over its 32 bytes of the I/O area (SIDSlots). The SID
7
+ # player's machine has nothing else in I/O 1 and 2, so the rest of a
8
+ # page there reads the open bus.
9
+ module ExtraSIDs
10
+ # Fits a SID at the address it was built at, where it takes its 32
11
+ # bytes over from whatever maps there.
12
+ def add_sid(sid)
13
+ page = sid.start >> 8
14
+ slots = @sid_slots.find { |candidate| candidate.page == page }
15
+ if slots.nil?
16
+ slots = SIDSlots.new(page, @sid, @open_bus)
17
+ @sid_slots << slots
18
+ end
19
+ slots.place(sid)
20
+ update_overlays!
21
+ end
22
+
23
+ private
24
+
25
+ def map_extra_sids
26
+ @sid_slots.each { |slots| @read_pages[slots.page] = @write_pages[slots.page] = slots }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class AddressBus
5
+ # Stands in for a page of I/O that a second or third SID shares: each 32
6
+ # bytes of it go to the SID placed there, or else to the first SID's
7
+ # mirrors in `$d400-$d7ff`, and to the open bus in I/O 1 and 2.
8
+ class SIDSlots
9
+ attr_reader :page
10
+
11
+ def initialize(page, first, open_bus)
12
+ @page = page
13
+ @io = page >= 0xde
14
+ @first = first
15
+ @open_bus = open_bus
16
+ @sids = []
17
+ @slots = Array.new(8, -1)
18
+ end
19
+
20
+ def place(sid)
21
+ @slots[(sid.start >> 5) & 7] = @sids.size
22
+ @sids << sid
23
+ end
24
+
25
+ def peek(addr)
26
+ index = @slots[(addr >> 5) & 7]
27
+ return @sids[index].peek(addr) unless index.negative?
28
+
29
+ @io ? @open_bus.peek(addr) : @first.peek(addr)
30
+ end
31
+
32
+ def poke(addr, value)
33
+ index = @slots[(addr >> 5) & 7]
34
+ if !index.negative?
35
+ @sids[index].poke(addr, value)
36
+ elsif !@io
37
+ @first.poke(addr, value)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/address_bus/saved_state"
4
+ require "badline/address_bus/sid_slots"
5
+ require "badline/address_bus/extra_sids"
4
6
  require "badline/address_bus/ultimax_pages"
5
7
 
6
8
  module Badline
@@ -32,6 +34,7 @@ module Badline
32
34
  class AddressBus
33
35
  include Addressable
34
36
  include UltimaxPages
37
+ include ExtraSIDs
35
38
 
36
39
  # I/O 1 and 2, and the Ultimax holes, with nothing on the bus. A read
37
40
  # picks up the byte the VIC fetched in the preceding phi1 half-cycle,
@@ -82,6 +85,7 @@ module Badline
82
85
  @control_ports.port_a_source = @cia1
83
86
  @cia1.on_port_b4_change { |high| @vic.lightpen_level(high) }
84
87
  @sid = SID.new(model: sid_model, pots: @control_ports)
88
+ @sid_slots = []
85
89
 
86
90
  @datasette = Datasette.new
87
91
  @datasette.on_flag { @cia1.flag! }
@@ -252,6 +256,7 @@ module Badline
252
256
  @read_pages[0xd7] = @write_pages[0xd7] = @debug_register if @debug_register
253
257
  @read_pages[0xdf] = @write_pages[0xdf] = @reu if @reu
254
258
  map_cartridge_io if @cartridge
259
+ map_extra_sids unless @sid_slots.empty?
255
260
  end
256
261
 
257
262
  def map_cartridge_io
@@ -13,14 +13,14 @@ module Badline
13
13
 
14
14
  def header
15
15
  ["FORM", 0, "AIFF",
16
- "COMM", 18, 1, 0, 16].pack("a4Na4a4NnNn") +
16
+ "COMM", 18, channels, 0, 16].pack("a4Na4a4NnNn") +
17
17
  extended(rate) +
18
18
  ["SSND", 0, 0, 0].pack("a4NNN")
19
19
  end
20
20
 
21
21
  def patch
22
22
  write_at(4, [46 + data_size].pack("N"))
23
- write_at(22, [samples].pack("N"))
23
+ write_at(22, [frames].pack("N"))
24
24
  write_at(42, [8 + data_size].pack("N"))
25
25
  end
26
26
 
@@ -44,12 +44,12 @@ module Badline
44
44
  # The KERNAL's PAL/NTSC flag, 1 for PAL.
45
45
  VIDEO_STANDARD = 0x02a6
46
46
 
47
- attr_reader :sid
47
+ attr_reader :sid, :stereo
48
48
 
49
- def initialize(tune, song: nil, sid_model: tune.sid_model)
49
+ def initialize(tune, subtune: nil, sid_models: tune.sid_models)
50
50
  @tune = tune
51
- @song = (song || tune.start_song).clamp(1, tune.songs) - 1
52
- @bus = AddressBus.new(sid_model:)
51
+ @subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes) - 1
52
+ @bus = AddressBus.new(sid_model: sid_models[0])
53
53
  # The CPU port as the KERNAL leaves it, which a PSID tune expects.
54
54
  @bus.poke(0x00, 0x2f)
55
55
  @bus.poke(0x01, 0x37)
@@ -57,28 +57,38 @@ module Badline
57
57
  @bus.cia1.timer_a_latch = tune.ntsc? ? NTSC_KERNAL_TIMER : KERNAL_TIMER
58
58
  @cpu = CPU.new(@bus)
59
59
  @sid = @bus.sid
60
+ @stereo = Stereo.new(@bus, tune, sid_models)
61
+ @extras = @stereo.extras
62
+ @multi = !@extras.empty?
60
63
  @idle = false
61
64
  end
62
65
 
63
66
  def start
64
67
  @bus.ram.write(@tune.load_address, @tune.data)
65
68
  @bus.ram.write(stub_address, stub)
66
- @sid.synthesize!
69
+ @stereo.synthesize!
67
70
  @cpu.status.interrupt = true
68
71
  @cpu.program_counter = idle_address
69
72
  install_dispatch
70
- call(@tune.init_address, @song)
73
+ call(@tune.init_address, @subtune)
71
74
  settle
72
75
  end
73
76
 
74
77
  # Advances one call of play, or `budget` cycles if that is shorter,
75
- # then yields whatever the SID recorded over it. Returns the cycles
76
- # advanced.
78
+ # then yields whatever the SIDs recorded over it, in stereo. Returns
79
+ # the cycles advanced.
77
80
  def frame(budget, &)
78
81
  call(@tune.play_address)
79
82
  cycles = [budget, period].min
80
- cycles.times { step }
81
- @sid.drain_samples.each(&)
83
+ if @multi
84
+ cycles.times { step }
85
+ else
86
+ cycles.times do
87
+ @sid.cycle!
88
+ @cpu.cycle!
89
+ end
90
+ end
91
+ @stereo.mix(&)
82
92
  cycles
83
93
  end
84
94
 
@@ -86,10 +96,10 @@ module Badline
86
96
 
87
97
  private
88
98
 
89
- # A video frame, or for a CIA-timed song one period of CIA 1 timer A,
99
+ # A video frame, or for a CIA-timed subtune one period of CIA 1 timer A,
90
100
  # which init and play are both free to reprogram.
91
101
  def period
92
- return (@tune.ntsc? ? NTSC_FRAME_CYCLES : FRAME_CYCLES) unless @tune.cia_timed?(@song + 1)
102
+ return (@tune.ntsc? ? NTSC_FRAME_CYCLES : FRAME_CYCLES) unless @tune.cia_timed?(@subtune + 1)
93
103
 
94
104
  @bus.cia1.timer_a_latch + 1
95
105
  end
@@ -117,6 +127,7 @@ module Badline
117
127
 
118
128
  def step
119
129
  @sid.cycle!
130
+ @extras.each(&:cycle!) if @multi
120
131
  @cpu.cycle!
121
132
  end
122
133