badline 0.6.1 → 0.7.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +125 -46
  4. data/exe/badline-ruby +9 -1
  5. data/lib/badline/address_bus/extra_sids.rb +30 -0
  6. data/lib/badline/address_bus/sid_slots.rb +42 -0
  7. data/lib/badline/address_bus.rb +5 -0
  8. data/lib/badline/audio/aiff.rb +2 -2
  9. data/lib/badline/audio/bare_player.rb +23 -12
  10. data/lib/badline/audio/cli.rb +153 -58
  11. data/lib/badline/audio/jukebox.rb +121 -36
  12. data/lib/badline/audio/machine_player.rb +25 -10
  13. data/lib/badline/audio/pcm_writer.rb +10 -6
  14. data/lib/badline/audio/playback.rb +6 -5
  15. data/lib/badline/audio/queued_tune.rb +89 -0
  16. data/lib/badline/audio/renderer.rb +57 -10
  17. data/lib/badline/audio/stereo.rb +97 -0
  18. data/lib/badline/audio/terminal.rb +39 -6
  19. data/lib/badline/audio/wav.rb +1 -1
  20. data/lib/badline/audio.rb +2 -0
  21. data/lib/badline/frontend/audio_sink.rb +6 -5
  22. data/lib/badline/frontend/boot.rb +1 -1
  23. data/lib/badline/frontend/buttons.rb +90 -0
  24. data/lib/badline/frontend/filter_view.rb +96 -0
  25. data/lib/badline/frontend/info_view.rb +120 -0
  26. data/lib/badline/frontend/painter.rb +175 -0
  27. data/lib/badline/frontend/player_commands.rb +33 -0
  28. data/lib/badline/frontend/player_footer.rb +86 -0
  29. data/lib/badline/frontend/player_header.rb +60 -0
  30. data/lib/badline/frontend/player_screen.rb +124 -0
  31. data/lib/badline/frontend/player_state.rb +67 -0
  32. data/lib/badline/frontend/player_window.rb +228 -0
  33. data/lib/badline/frontend/scope.rb +42 -0
  34. data/lib/badline/frontend/sid_history.rb +175 -0
  35. data/lib/badline/frontend/sid_view.rb +140 -0
  36. data/lib/badline/frontend/visualizer_view.rb +101 -0
  37. data/lib/badline/frontend.rb +14 -0
  38. data/lib/badline/media/queue.rb +158 -0
  39. data/lib/badline/media.rb +13 -8
  40. data/lib/badline/options/help.rb +34 -5
  41. data/lib/badline/options/option.rb +1 -1
  42. data/lib/badline/options/table.rb +4 -2
  43. data/lib/badline/options/validation.rb +54 -0
  44. data/lib/badline/options.rb +62 -43
  45. data/lib/badline/sdl.rb +16 -6
  46. data/lib/badline/sid/catch_up.rb +11 -1
  47. data/lib/badline/sid/filter.rb +9 -0
  48. data/lib/badline/sid/voice.rb +6 -0
  49. data/lib/badline/sid/waveform.rb +20 -5
  50. data/lib/badline/sid.rb +35 -3
  51. data/lib/badline/storage/hvsc.rb +41 -0
  52. data/lib/badline/storage/sid_file.rb +45 -25
  53. data/lib/badline/storage/song_lengths.rb +6 -29
  54. data/lib/badline/storage/stil/credit.rb +50 -0
  55. data/lib/badline/storage/stil.rb +126 -0
  56. data/lib/badline/storage.rb +8 -0
  57. data/lib/badline/version.rb +1 -1
  58. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d667997df6e72a7efd6d8789896402ef09b757c49ac0e1890f9041a22d37d83b
4
- data.tar.gz: ca5e42cebc1e7c1b4879062fab85bd97961242213f6701db86fa57717b270e8d
3
+ metadata.gz: 41235fb28f9f33c9a88333beec1c09cd9825d45875f24d22b8cb5f66f9528467
4
+ data.tar.gz: c214b37efce97c404dcd0838283e65e0f42aaa86a8376f18ef2323933b01b2f2
5
5
  SHA512:
6
- metadata.gz: 8965b452eb8836de285dc76e7d8220a3b302b81341ee277ab9738ea334e5d27f108b14e1b162b3fe28e88f7aa2fba1e1e30e4dd5fe2b4f7c579643c45e820070
7
- data.tar.gz: 6603b0599428aef7a8bd0b1b5b99300281d02dbde1bd327895dc055a30ce16f2bdfc758b37661bca18eca256b0e98e5080926426dabbf0997c6a96762d7e3793
6
+ metadata.gz: ab2c530282a35d8ac65c217d6901d46c6f844cde1d7c7ca143acb8ea8f05ac08d1da6b8ee290fb41bd016ce2079e308bfd5c5f338be18ed269be693612e07a50
7
+ data.tar.gz: 6c27c6d41e73e495bb27e536fae7424b5524c12b9c1d12cdb3bccc2efb3a73be6137e5209c4356394594d71ceaa205309989f193344003a3ed65245df0ec74fa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/elektronaut/badline/compare/v0.6.1...v0.7.0) (2026-10-02)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * call a .sid tune's parts subtunes everywhere
9
+
10
+ ### Features
11
+
12
+ * add a badline sid subcommand that plays tunes and directories ([7dfc21b](https://github.com/elektronaut/badline/commit/7dfc21b5e4ba47cd37a4593da61e95bb1d2329ab))
13
+ * drop .sid files and folders on the SID player to queue them ([cc1f434](https://github.com/elektronaut/badline/commit/cc1f434b7404ded067ef43d1d3d5aa53d338de4c))
14
+ * end a subtune with no known length when it falls silent ([46c4f10](https://github.com/elektronaut/badline/commit/46c4f1064f6382872ec22989253bc9c159f78dcf))
15
+ * open a .sid tune, or no tune at all, in the SID player ([396c010](https://github.com/elektronaut/badline/commit/396c010ee626f95fbb71ced8bf3905909c470c8e))
16
+ * play 2SID and 3SID tunes in stereo in the SID player ([7c72eef](https://github.com/elektronaut/badline/commit/7c72eeff7e5e7b1c56286f9bf6030ae3408f7176))
17
+ * play a queue of tunes, with shuffle, loop and play-all subtunes ([d163106](https://github.com/elektronaut/badline/commit/d163106eea99d90f6ffe74fb91474a2aca4c64eb))
18
+ * play sid tunes in a window that shows what the SID is doing ([60bfd3f](https://github.com/elektronaut/badline/commit/60bfd3f75fe82d928afa374d257d3d21e5ac29ad))
19
+ * seek forward from where the subtune is, not from its start ([34059e8](https://github.com/elektronaut/badline/commit/34059e8f9b12cab44c94124e6455fef9c915d8eb))
20
+ * seek within a subtune, and let the slow warning lapse ([b98ac6e](https://github.com/elektronaut/badline/commit/b98ac6eb6f9c0ce088545f55cc5f2445dd38dd2d))
21
+ * show a tune's STIL entry in the SID player ([82d9dca](https://github.com/elektronaut/badline/commit/82d9dca26d7cefe853a6af41e50fc723a3e90170))
22
+ * show each voice's live output in the SID view ([c61a128](https://github.com/elektronaut/badline/commit/c61a1289983887c0175fca3dc4c37b470e96de92))
23
+ * show HVSC's STIL in the SID player ([bd55c0c](https://github.com/elektronaut/badline/commit/bd55c0c87695ba8cc84dbfd51ab2119a97eccded))
24
+ * show where a seek is headed on the SID player's bar ([1a84b37](https://github.com/elektronaut/badline/commit/1a84b3746a960dba28dc9c789b3d2cd1520d2f94))
25
+ * step subtunes and tunes on separate keys, add --all-subtunes ([eaef338](https://github.com/elektronaut/badline/commit/eaef3384e6205163e2a4b63f5aa8d9204fe5190f))
26
+ * switch a SID between the 6581 and 8580 while it runs ([5920391](https://github.com/elektronaut/badline/commit/592039123d7936b8d22c8fd5f144a9bc9a8f383b))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * build the native badline with a Spinel that keeps an array joined mid-expression alive ([d2c4eb2](https://github.com/elektronaut/badline/commit/d2c4eb20d6b80494fb2a20105c0532f3778b205c))
32
+ * build the native badline with a Spinel that reports render progress ([58524ea](https://github.com/elektronaut/badline/commit/58524eaf4881661e7e0e9dd38c32071545bf1fae))
33
+ * show non-ASCII text from a .sid header in the native badline ([959bb6f](https://github.com/elektronaut/badline/commit/959bb6f7d07de4c90ac50ae51ae63f3c66601a78))
34
+
35
+
36
+ ### Performance Improvements
37
+
38
+ * draw the SID player's traces in one call each ([afcae15](https://github.com/elektronaut/badline/commit/afcae15ed273aa7a6dd49cb31e0f7180c56feb75))
39
+
3
40
  ## [0.6.1](https://github.com/elektronaut/badline/compare/v0.6.0...v0.6.1) (2026-09-30)
4
41
 
5
42
 
data/README.md CHANGED
@@ -56,46 +56,53 @@ badline game.prg # Load and run a program
56
56
  badline game.d64 # Mount a disk image as device 8 and load it
57
57
  badline game.tap # Insert a tape and load it
58
58
  badline game.crt # Attach a cartridge
59
- badline tune.sid # Play a SID tune
59
+ badline tune.sid # Play a SID tune in the SID player
60
60
  badline game.vsf # Restore a snapshot, from badline or VICE
61
61
  badline ~/c64 # Mount a directory as device 8
62
62
  ```
63
63
 
64
- `badline-ruby` takes the same media and the same options, with the
65
- differences noted below.
66
-
67
64
  Programs, disk and tape images and SID tunes start automatically, and
68
65
  a cartridge starts itself. A mounted directory waits for you to `LOAD`
69
- from it. `--no-autostart` attaches the media and stops at `READY.`, so
70
- you can type the `LOAD` yourself. `--read-only` mounts a disk image
71
- write-protected, so the drive reports `26,WRITE PROTECT ON` for any
72
- write and the image file stays as it was. `--song N` picks a subtune of a
73
- `.sid` file, `--sid 8580` fits the newer SID and `--reu 512` plugs in a
74
- 512K RAM Expansion Unit. `--ntsc` runs an NTSC C64, with the 6567R8
75
- VIC-II, instead of a PAL one. `badline-ruby` also
76
- takes `--disable-jit`, which runs without YJIT, otherwise switched on at
77
- startup. `--help` lists the options.
66
+ from it.
78
67
 
79
- The KERNAL, BASIC and character ROMs come with the gem. To run other
80
- images, such as a patched KERNAL, point `BADLINE_ROM_PATH` at a
81
- directory that holds `kernal.rom`, `basic.rom` and `character.rom`,
82
- plus `eapi/eapi-am29f040-14` if you attach EasyFlash cartridges. From
83
- Ruby, `Badline.rom_path = dir` does the same before a
84
- `Badline::Computer` is built, and `nil` restores the bundled set.
68
+ `badline-ruby` takes the same media and the same options, with the
69
+ differences noted in the table. `--help` lists the options for either.
70
+
71
+ ### Options
72
+
73
+ | Option | Effect |
74
+ | --- | --- |
75
+ | `--no-autostart` | Attach the media and stop at `READY.`, so you can type the `LOAD` yourself |
76
+ | `--read-only` | Mount a disk image write-protected. The drive reports `26,WRITE PROTECT ON` for any write and the image file stays as it was |
77
+ | `--true-drive` | Put an emulated 1541 on device 8 instead of the KERNAL traps. See [Media](#media) |
78
+ | `-s`, `--subtune N` | Pick a subtune of a `.sid` file, counting from 1 |
79
+ | `--sid 6581`, `--sid 8580` | Fit the older or newer SID. `--sid auto`, the default, takes a `.sid` tune's own |
80
+ | `--reu SIZE` | Plug in a RAM Expansion Unit of `SIZE` K: 128, 256, 512 (a 1750) or up to 16384 |
81
+ | `--ntsc` | Run an NTSC C64, with the 6567R8 VIC-II, instead of a PAL one |
82
+ | `--no-sound` | Don't play the SID (`badline` only, where sound is on by default) |
83
+ | `--sound` | Play the SID (`badline-ruby`, where sound is off by default) |
84
+ | `--no-vsync` | Pace the window by a timer, or by the sound while it plays, instead of the display's vsync |
85
+ | `--verbose` | Print the display's refresh rate, the sound's sample rate and the game controllers found as the window opens, then the frame rate and the time each frame takes, once a second |
86
+ | `--disable-jit` | Run without YJIT, otherwise switched on at startup (`badline-ruby` only) |
87
+ | `--version` | Show the version and what built it (`badline` only) |
88
+ | `-h`, `--help` | List the options |
85
89
 
86
90
  Both run the same window. `badline` plays the SID through the host's
87
- audio device, and `F10` mutes and unmutes it. `--no-sound` turns it
88
- off. The window is paced by the display's vsync; `--no-vsync` paces it
89
- by a timer, or by the sound while it plays.
90
-
91
- `--verbose` prints the display's refresh rate, the sound's sample rate
92
- and the game controllers found as the window opens, then the frame rate
93
- and the time each frame takes, once a second.
94
-
95
- For a scripted run, `--frames N` quits after N frames, `--unpaced` runs
96
- as fast as it can and `--at FRAME:EVENT` presses keys, types, swaps
97
- media and takes screenshots once that many frames have run. `--script
98
- FILE` takes one `FRAME:EVENT` a line:
91
+ audio device, and `F10` mutes and unmutes it. In `badline-ruby` the
92
+ machine runs below real time, so the sound stutters: it plays in bursts
93
+ with silent gaps between them, at the right pitch, and never slows the
94
+ emulation down.
95
+
96
+ ### Scripted runs
97
+
98
+ | Option | Effect |
99
+ | --- | --- |
100
+ | `--frames N` | Quit after N frames |
101
+ | `--unpaced` | Run as fast as it can, without vsync or pacing |
102
+ | `--at FRAME:EVENT` | Press keys, type, swap media or take a screenshot once `FRAME` frames have run |
103
+ | `--script FILE` | Run the events in `FILE`, one `FRAME:EVENT` a line |
104
+ | `--screenshot FILE` | Save the last frame as a `.bmp` |
105
+ | `--save-snapshot FILE` | Save the machine as a `.vsf` snapshot after the last frame |
99
106
 
100
107
  ```sh
101
108
  badline --unpaced --frames 12000 --true-drive disk1.d64 \
@@ -106,10 +113,14 @@ badline --unpaced --frames 12000 --true-drive disk1.d64 \
106
113
  `--help` lists the events, and [native/README.md](native/README.md#running)
107
114
  describes them.
108
115
 
109
- In `badline-ruby` sound is off by default, and `--sound` turns it on.
110
- The machine runs below real time there, so the sound stutters: it plays
111
- in bursts with silent gaps between them, at the right pitch, and never
112
- slows the emulation down.
116
+ ### ROMs
117
+
118
+ The KERNAL, BASIC and character ROMs come with the gem. To run other
119
+ images, such as a patched KERNAL, point `BADLINE_ROM_PATH` at a
120
+ directory that holds `kernal.rom`, `basic.rom` and `character.rom`,
121
+ plus `eapi/eapi-am29f040-14` if you attach EasyFlash cartridges. From
122
+ Ruby, `Badline.rom_path = dir` does the same before a
123
+ `Badline::Computer` is built, and `nil` restores the bundled set.
113
124
 
114
125
  ## Media
115
126
 
@@ -215,39 +226,107 @@ out and switches the machine off and on.
215
226
 
216
227
  `badline-ruby --headless` plays a `.sid` tune on the host's audio
217
228
  device without opening the window, and `--audio-out` renders it to a
218
- 16-bit PCM file instead. The file's extension picks the format, `.wav`
229
+ 16-bit stereo PCM file instead. The file's extension picks the format, `.wav`
219
230
  or `.aiff`. The native `badline` has both modes too.
220
231
 
221
232
  ```sh
233
+ badline sid ~/C64Music/MUSICIANS/H/Hubbard_Rob # play every tune below a directory
234
+ badline sid tune.sid other.sid # play a queue of tunes
222
235
  badline-ruby --headless tune.sid # play, length from HVSC
223
236
  badline-ruby --headless -s 3 tune.sid # play the third subtune
237
+ badline-ruby --headless --all-subtunes tune.sid # play on through every subtune
224
238
  badline-ruby --seconds 180 tune.sid --audio-out out.aiff
225
239
  badline-ruby -s 3 --rate 48000 tune.sid --audio-out out.wav
226
240
  badline-ruby --headless --sid 8580 tune.sid
227
241
  badline-ruby --filter-chunk 1 tune.sid --audio-out out.wav # exact filter, slower
228
242
  ```
229
243
 
244
+ `badline sid FILE|DIR...` plays a queue of tunes in the SID player's
245
+ window, in the order given, and a directory adds every `.sid` tune below
246
+ it in path order. `badline sid --headless` plays the queue in the
247
+ terminal instead. `badline sid` on its own opens the window with an
248
+ empty queue. Dropping `.sid` files or folders on the window adds them
249
+ to the end of the queue, and an empty queue starts playing them. It
250
+ takes
251
+ the options below except `--audio-out`, and `--subtune` picks the first
252
+ tune's subtune. `--sid auto`, the default, fits each of a tune's SIDs
253
+ the model its header names. A tune written for 2 or 3 SIDs plays on as
254
+ many, at the addresses its header gives, in stereo: SID 1 on the left,
255
+ SID 2 on the right and SID 3 in the centre. A tune on one SID plays the
256
+ same on both channels. A file that isn't a tune is skipped.
257
+ `badline tune.sid` plays the tune in the SID player too, unless an
258
+ option of the emulator's window, such as `--ntsc` or `--reu`, asks for
259
+ the machine: then a tune for one SID runs on the emulated C64, started
260
+ through a small driver after boot.
261
+
230
262
  Both modes take the same options. The window's own, `--no-autostart`,
231
263
  `--read-only`, `--sound`, `--true-drive`, `--reu`, `--ntsc` and
232
- `--verbose`, don't apply to them. `--song` (or `-s`) picks the subtune, counting from 1 as HVSC
233
- does, and defaults to the tune's own start song. Playback asks the
264
+ `--verbose`, don't apply to them. `--subtune` (or `-s`) picks the subtune, counting from 1 as HVSC
265
+ does, and defaults to the tune's own start subtune. Playback asks the
234
266
  device for 44.1 kHz and takes whatever rate it offers, unless `--rate`
235
267
  says otherwise. Ctrl-C stops it.
236
268
 
237
- Played on a terminal, `--headless` shows the tune's name, author and
238
- release, the song number and the time played against the song's
239
- length. When a song reaches its length it goes on to the next, and it
240
- stops after the last. `n` or → skips to the next song, `p` or ← goes
241
- back one, space pauses and `q` quits. `--no-tui`, or output that isn't a
242
- terminal, gives plain progress output instead and plays just the one
243
- song, as `--audio-out` renders just the one.
269
+ Played on a terminal, `--headless` and `sid` show each tune's name,
270
+ author and release as it starts, then the tune's place in the queue,
271
+ the subtune number and the time played against the subtune's length. `--headless`
272
+ queues just the one tune. Each tune plays its own subtune: `--subtune`, or
273
+ the tune's start subtune. When that subtune ends the player goes on to the next
274
+ tune, and it stops after the last. `a`, or `--all-subtunes`, turns on
275
+ playing all subtunes, so that a subtune that ends goes on to the tune's next
276
+ subtune, and a tune stepped to starts on its first subtune.
277
+
278
+ → and ← step to the tune's next and previous subtune, stopping at its
279
+ first and last. `n` and `p` step to the next and previous tune. `s`
280
+ turns shuffle on and off, which plays the queue's tunes in a random
281
+ order, and `l` turns looping on and off, so that the end of the queue
282
+ goes on to its start. `,` and `.` seek 10 seconds back and forward
283
+ within the subtune, which plays it again from its start and runs silently
284
+ up to the point asked for. Space pauses and `q` quits. The status line
285
+ shows which modes are on, and they last until the player quits.
286
+
287
+ ![The SID player's visualizer playing Rob Hubbard's Delta](doc/images/sid-player.png)
288
+
289
+ The SID player's window is worked with the mouse. Its header shows the
290
+ tune's name, author and release, the tune the subtune covers at the
291
+ moment when HVSC's STIL credits one, following the times STIL gives, and
292
+ buttons that switch between three views and between the tune's own SID
293
+ model, the 6581 and the 8580, which changes the chips playing on the
294
+ spot. Its footer shows the time played on a bar you can click to seek,
295
+ and buttons that pause, step between tunes and between a tune's
296
+ subtunes, and turn shuffle, looping and all subtunes on and off. The
297
+ visualizer view shows each voice's note, and how far off it is in cents,
298
+ over a scope of the voice's output, and the mixed output below them. A
299
+ tune on more than one SID gets a row of voices for each SID, and the mix
300
+ splits into its left and right. The SID view shows one SID at a time,
301
+ and for a tune on more than one, a row of SID 1, 2 and 3 buttons at its
302
+ top picks which. It shows each voice's waveforms and the shape
303
+ they make, its control bits, pulse width, envelope settings, and the
304
+ envelope's level and stage, and the filter's modes, cutoff, resonance,
305
+ volume and its response on the chip playing. The INFO view shows the
306
+ tune's STIL entry and the subtune's, with a scroll bar, the mouse wheel
307
+ or the up and down keys for the long ones. The terminal's keys work in
308
+ the window too, along with Tab to switch views and `c` to step through
309
+ the SID models.
310
+
311
+ `--no-tui`, or output that isn't a terminal, gives plain progress
312
+ output instead and plays through the queue without the keys, while
313
+ `--audio-out` renders just the one subtune.
244
314
 
245
315
  A `.sid` file doesn't store its length, so `badline-ruby` looks the
246
316
  tune up by MD5 in HVSC's `Songlengths.md5`. It finds the database
247
317
  through `--songlengths`, in a `DOCUMENTS` directory in any of the
248
318
  tune's parent directories (the layout of an HVSC collection), or under
249
319
  `$HVSC_BASE/DOCUMENTS`. Without a database or `--seconds` it runs for
250
- 60 seconds.
320
+ 60 seconds, but a subtune that falls silent for 5 seconds before then
321
+ ends there, when played and when rendered alike. Silent means the
322
+ output holds within 16 steps of one level, since a 6581 idles at a DC
323
+ offset rather than at zero. A subtune with a known length plays to its
324
+ length whatever it sounds like.
325
+
326
+ A tune in an HVSC collection also gets its entry in HVSC's `STIL.txt`,
327
+ found the same way: comments, covers, and subtune names and composers.
328
+ The tune's own fields follow its header, and each subtune's print as it
329
+ starts.
251
330
 
252
331
  PSID tunes run on a CPU and RAM with only the SID clocked, at about
253
332
  twice real time, so they play smoothly. RSID tunes set up their own
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