badline 0.11.1 → 0.12.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +35 -22
  11. data/lib/badline/c128/fast_serial.rb +1 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +9 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +22 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +16 -19
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +21 -3
  39. data/lib/badline/cia.rb +8 -5
  40. data/lib/badline/computer/attachments.rb +4 -56
  41. data/lib/badline/computer/true_drives.rb +80 -0
  42. data/lib/badline/computer.rb +4 -0
  43. data/lib/badline/core.rb +2 -0
  44. data/lib/badline/drive/core.rb +8 -33
  45. data/lib/badline/drive/fast_serial.rb +73 -0
  46. data/lib/badline/drive/idle.rb +1 -11
  47. data/lib/badline/drive/orbit.rb +2 -5
  48. data/lib/badline/drive/sleep.rb +0 -2
  49. data/lib/badline/drive/vias.rb +84 -0
  50. data/lib/badline/drive.rb +2 -0
  51. data/lib/badline/drive1541.rb +1 -0
  52. data/lib/badline/drive1571/wd1770.rb +94 -8
  53. data/lib/badline/drive1571.rb +8 -58
  54. data/lib/badline/drive1581/bus.rb +137 -0
  55. data/lib/badline/drive1581/cpu.rb +11 -0
  56. data/lib/badline/drive1581/disk.rb +135 -0
  57. data/lib/badline/drive1581/disk_state.rb +87 -0
  58. data/lib/badline/drive1581/mechanism.rb +117 -0
  59. data/lib/badline/drive1581/serial_port.rb +52 -0
  60. data/lib/badline/drive1581/slot.rb +78 -0
  61. data/lib/badline/drive1581/track.rb +100 -0
  62. data/lib/badline/drive1581/track_bytes.rb +75 -0
  63. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  64. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  65. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  66. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  67. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  68. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  69. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  70. data/lib/badline/drive1581/wd1772.rb +216 -0
  71. data/lib/badline/drive1581.rb +233 -0
  72. data/lib/badline/frontend/app.rb +1 -3
  73. data/lib/badline/frontend/audio_device.rb +67 -0
  74. data/lib/badline/frontend/audio_sink.rb +16 -50
  75. data/lib/badline/frontend/boot.rb +18 -10
  76. data/lib/badline/frontend/buttons.rb +7 -7
  77. data/lib/badline/frontend/keys.rb +33 -8
  78. data/lib/badline/frontend/media_slots.rb +42 -0
  79. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  80. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  81. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  82. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  83. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  84. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  85. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  86. data/lib/badline/frontend/menu/theme.rb +18 -0
  87. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  88. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  89. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  90. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  91. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  92. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  93. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  94. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  95. data/lib/badline/frontend/player/theme.rb +18 -0
  96. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  97. data/lib/badline/frontend/sound.rb +19 -38
  98. data/lib/badline/frontend/timeline.rb +3 -14
  99. data/lib/badline/frontend.rb +24 -20
  100. data/lib/badline/iec_bus.rb +14 -2
  101. data/lib/badline/machine.rb +5 -4
  102. data/lib/badline/media/boot_disk.rb +50 -0
  103. data/lib/badline/media/not_disk.rb +8 -0
  104. data/lib/badline/media/true_drive.rb +35 -15
  105. data/lib/badline/media.rb +9 -3
  106. data/lib/badline/options/event.rb +2 -2
  107. data/lib/badline/options/family.rb +3 -9
  108. data/lib/badline/options/help.rb +2 -1
  109. data/lib/badline/roms/README +9 -0
  110. data/lib/badline/roms/dos1581.rom +0 -0
  111. data/lib/badline/sdl.rb +37 -20
  112. data/lib/badline/snapshot/state.rb +5 -4
  113. data/lib/badline/storage/p00.rb +8 -0
  114. data/lib/badline/time_of_day.rb +3 -3
  115. data/lib/badline/version.rb +1 -1
  116. data/lib/badline/vic20/attachments.rb +21 -7
  117. data/lib/badline/vic20/saved_state.rb +2 -0
  118. data/lib/badline/vic20.rb +3 -0
  119. data/lib/badline/z80/core.rb +2 -0
  120. data/lib/badline/z80/saved_state.rb +69 -0
  121. metadata +54 -21
  122. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  123. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  124. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  125. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -2,38 +2,32 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The SID player's window, which `sid` plays in, drawn by PlayerScreen:
6
- # a header with the tune, its STIL credit, the views and the SID model,
7
- # and a footer with the time and the buttons that step through the
8
- # queue, framing one of three views: the visualizer, with each voice's
9
- # note and output and the mix, the SID view, with everything the chip
10
- # is doing, and the tune's STIL entry. The window's height follows the
11
- # view, and .sid files and folders dropped on it join the queue.
5
+ # The SID player that `sid` plays in, which runs its window,
6
+ # PlayerScreen: a header with the tune, its STIL credit, the views and
7
+ # the SID model, and a footer with the time and the buttons that step
8
+ # through the queue, framing one of three views: the visualizer, with
9
+ # each voice's note and output and the mix, the SID view, with
10
+ # everything the chip is doing, and the tune's STIL entry. The window's
11
+ # height follows the view, and .sid files and folders dropped on it
12
+ # join the queue.
12
13
  #
13
14
  # It stands in for the terminal, so Audio::Jukebox drives it the same
14
15
  # way: #wait handles the window's clicks, keys and drops between frames
15
16
  # and redraws it at the display's pace. The SID model buttons switch
16
17
  # the chips playing on the spot, and each tune after it starts on the
17
18
  # same choice. AUTO gives each of a tune's SIDs its own model.
18
- class PlayerWindow < Audio::Terminal
19
+ class PlayerController < Audio::Terminal
19
20
  FRAME = 1.0 / 50
20
21
 
21
- MOUSEWHEEL = 0x403
22
- DROPFILE = 0x1000
23
- UP = 82
24
- DOWN = 81
25
-
26
- WARNING = VIC::PALETTE[10]
27
-
28
22
  CHIPS = PlayerHeader::CHIPS
29
23
 
30
24
  KEYS = {
31
25
  17 => :next, 19 => :previous,
32
- 79 => :next_subtune, 80 => :previous_subtune,
33
- 44 => :pause, 22 => :shuffle, 15 => :loop, 4 => :all_subtunes,
34
- 20 => :quit, 41 => :quit,
26
+ Keys::RIGHT => :next_subtune, Keys::LEFT => :previous_subtune,
27
+ Keys::SPACE => :pause, 22 => :shuffle, 15 => :loop, 4 => :all_subtunes,
28
+ 20 => :quit, Keys::ESCAPE => :quit,
35
29
  54 => :back, 55 => :forward,
36
- 43 => :view, 6 => :chip
30
+ Keys::TAB => :view, 6 => :chip
37
31
  }.freeze
38
32
 
39
33
  def initialize(input:, output:)
@@ -143,17 +137,15 @@ module Badline
143
137
  when SDL::MOUSEMOTION then point
144
138
  when SDL::MOUSEBUTTONDOWN then click(actions) if SDL.event_button(SDL.event) == 1
145
139
  when SDL::MOUSEBUTTONUP then @scrolling = false
146
- when MOUSEWHEEL then @screen.info.scroll(-3 * SDL.event_x(SDL.event))
147
- when DROPFILE then drop(actions)
140
+ when SDL::MOUSEWHEEL then @screen.info.scroll(-3 * SDL.event_x(SDL.event))
141
+ when SDL::DROPFILE then drop(actions)
148
142
  end
149
143
  end
150
144
  actions
151
145
  end
152
146
 
153
147
  def drop(actions)
154
- file = SDL.event_file(SDL.event)
155
- @dropped << LibC.strstr(file, "")
156
- LibC.free(file)
148
+ @dropped << SDL.dropped_file
157
149
  actions << :drop unless actions.include?(:drop)
158
150
  end
159
151
 
@@ -164,7 +156,7 @@ module Badline
164
156
  end
165
157
 
166
158
  def key(scancode, actions)
167
- return @screen.info.scroll(scancode == UP ? -1 : 1) if [UP, DOWN].include?(scancode)
159
+ return @screen.info.scroll(scancode == Keys::UP ? -1 : 1) if [Keys::UP, Keys::DOWN].include?(scancode)
168
160
 
169
161
  action = KEYS[scancode]
170
162
  command(action, actions) unless action.nil?
@@ -29,7 +29,7 @@ module Badline
29
29
  x += @buttons.icon(x, row, state.paused? ? :play : :pause, :pause, scale: 2) + 4
30
30
  x += @buttons.icon(x, row, :next, :next, scale: 2) + 8
31
31
  unless state.tunes.zero?
32
- x += @painter.text(x, row + 6, "TUNE #{state.tune}/#{state.tunes}", SIDView::BRIGHT) + 24
32
+ x += @painter.text(x, row + 6, "TUNE #{state.tune}/#{state.tunes}", PlayerTheme::BRIGHT) + 24
33
33
  draw_subtunes(state, x, row + 4)
34
34
  end
35
35
  draw_toggles(state, row + 4)
@@ -41,9 +41,9 @@ module Badline
41
41
  private
42
42
 
43
43
  def draw_subtunes(state, left, row)
44
- x = left + @painter.text(left, row + 2, "SUBTUNE", SIDView::TEXT) + 4
44
+ x = left + @painter.text(left, row + 2, "SUBTUNE", PlayerTheme::TEXT) + 4
45
45
  x += @buttons.icon(x, row, :left, :previous_subtune)
46
- x += @painter.text(x + 2, row + 2, "#{state.subtune}/#{state.subtunes}", SIDView::BRIGHT) + 4
46
+ x += @painter.text(x + 2, row + 2, "#{state.subtune}/#{state.subtunes}", PlayerTheme::BRIGHT) + 4
47
47
  @buttons.icon(x, row, :right, :next_subtune)
48
48
  end
49
49
 
@@ -51,17 +51,17 @@ module Badline
51
51
  played = state.played
52
52
  width = BAR_WIDTH
53
53
  @buttons.area([BAR_LEFT, top - 2, width, 12], :seek)
54
- @painter.box(BAR_LEFT, top + 2, width, 4, SIDView::BOX)
54
+ @painter.box(BAR_LEFT, top + 2, width, 4, PlayerTheme::BOX)
55
55
  draw_target(state, top) if state.seeking?
56
- @painter.box(BAR_LEFT, top + 2, along(state, played), 4, SIDView::BRIGHT)
56
+ @painter.box(BAR_LEFT, top + 2, along(state, played), 4, PlayerTheme::BRIGHT)
57
57
  @painter.text(WIDTH - 16 - Painter.width(CLOCK), top, "#{clock(played)} / #{clock(state.length)}",
58
- SIDView::TEXT)
58
+ PlayerTheme::TEXT)
59
59
  end
60
60
 
61
61
  def draw_target(state, top)
62
62
  target = along(state, state.target)
63
- @painter.box(BAR_LEFT, top + 2, target, 4, SIDView::DIM)
64
- @painter.box(BAR_LEFT + [target, BAR_WIDTH - 1].min, top - 1, 1, 10, SIDView::BRIGHT)
63
+ @painter.box(BAR_LEFT, top + 2, target, 4, PlayerTheme::DIM)
64
+ @painter.box(BAR_LEFT + [target, BAR_WIDTH - 1].min, top - 1, 1, 10, PlayerTheme::BRIGHT)
65
65
  end
66
66
 
67
67
  # How far along the bar `seconds` into the subtune is.
@@ -21,10 +21,10 @@ module Badline
21
21
  # Underlines the models playing while AUTO picks them.
22
22
  def draw(state, tune, playing_models, credit)
23
23
  unless tune.nil?
24
- @painter.text(16, 8, fit(tune.name, 26), SIDView::BRIGHT, scale: 2)
25
- @painter.text(16, 28, fit([tune.author, tune.released].reject(&:empty?).join(" - "), 50), SIDView::TEXT)
26
- @painter.text(16, 40, fit(credit, 76), SIDView::BRIGHT)
27
- @painter.text(16, 52, warning(state), PlayerWindow::WARNING)
24
+ @painter.text(16, 8, fit(tune.name, 26), PlayerTheme::BRIGHT, scale: 2)
25
+ @painter.text(16, 28, fit([tune.author, tune.released].reject(&:empty?).join(" - "), 50), PlayerTheme::TEXT)
26
+ @painter.text(16, 40, fit(credit, 76), PlayerTheme::BRIGHT)
27
+ @painter.text(16, 52, warning(state), PlayerTheme::WARNING)
28
28
  end
29
29
  draw_views(state)
30
30
  draw_chips(state, playing_models)
@@ -48,7 +48,7 @@ module Badline
48
48
  CHIP_NAMES.each_with_index do |name, chip|
49
49
  width = @buttons.text(x, 24, name, CHIPS[chip], on: chip == state.chip)
50
50
  if state.chip.zero? && playing_models.include?(CHIPS[chip])
51
- @painter.box(x + 2, 37, width - 4, 1, SIDView::BRIGHT)
51
+ @painter.box(x + 2, 37, width - 4, 1, PlayerTheme::BRIGHT)
52
52
  end
53
53
  x += width + 4
54
54
  end
@@ -10,7 +10,6 @@ module Badline
10
10
  HEADER = 64
11
11
  SCALE = 2
12
12
  TITLE = "Badline"
13
- BACKGROUND = VIC::PALETTE[6]
14
13
 
15
14
  attr_reader :buttons, :info
16
15
 
@@ -72,8 +71,8 @@ module Badline
72
71
  def draw(state, player, history, tune, credit)
73
72
  return unless open?
74
73
 
75
- SDL.SDL_SetRenderDrawColor(@renderer, (BACKGROUND >> 16) & 0xff, (BACKGROUND >> 8) & 0xff,
76
- BACKGROUND & 0xff, 255)
74
+ back = PlayerTheme::BACKGROUND
75
+ SDL.SDL_SetRenderDrawColor(@renderer, (back >> 16) & 0xff, (back >> 8) & 0xff, back & 0xff, 255)
77
76
  SDL.SDL_RenderClear(@renderer)
78
77
  @buttons.forget
79
78
  @top.draw(state, tune, player.nil? ? [] : player.stereo.models, credit)
@@ -90,7 +89,7 @@ module Badline
90
89
 
91
90
  def build
92
91
  @painter = Painter.new(@renderer)
93
- @buttons = Buttons.new(@painter)
92
+ @buttons = Buttons.new(@painter, [PlayerTheme::TEXT, PlayerTheme::BRIGHT, PlayerTheme::BACKGROUND])
94
93
  @top = PlayerHeader.new(@painter, @buttons)
95
94
  @bottom = PlayerFooter.new(@painter, @buttons)
96
95
  @visualizer = VisualizerView.new(@painter, HEADER + 4)
@@ -117,7 +116,7 @@ module Badline
117
116
  def draw_empty(view)
118
117
  hint = "Drop .sid files or folders here"
119
118
  @painter.text((WIDTH - Painter.width(hint, scale: 2)) / 2, HEADER + (body_height(view) / 2) - 8, hint,
120
- SIDView::TEXT, scale: 2)
119
+ PlayerTheme::TEXT, scale: 2)
121
120
  end
122
121
  end
123
122
  end
@@ -21,7 +21,7 @@ module Badline
21
21
  end
22
22
 
23
23
  def draw(history, played, channel, rgb)
24
- @painter.box(@left, @top, @width, @height, SIDView::BOX)
24
+ @painter.box(@left, @top, @width, @height, PlayerTheme::BOX)
25
25
  span = @span
26
26
  range = @range
27
27
  start = history.scope_start(played, span, channel)
@@ -8,13 +8,6 @@ module Badline
8
8
  # a tune on more than one gets a row of buttons above the voices that
9
9
  # pick it, which makes the view PICKER taller.
10
10
  class SIDView
11
- COLORS = VIC::PALETTE
12
- TEXT = COLORS[14]
13
- BRIGHT = COLORS[1]
14
- DIM = COLORS[11]
15
- BOX = COLORS[0]
16
- VOICE_COLORS = [COLORS[7], COLORS[13], COLORS[3]].freeze
17
-
18
11
  NOTES = %w[C C# D D# E F F# G G# A A# B].freeze
19
12
  WAVES = %w[TRI SAW PUL NOI].freeze
20
13
  FLAGS = %w[GATE SYNC RING TEST].freeze
@@ -70,9 +63,11 @@ module Badline
70
63
  top = @top + (picker * PICKER)
71
64
  frame = history.frame(played, sid)
72
65
  3.times { |voice| draw_voice(history, frame, voice, top + (voice * VOICE_HEIGHT), clock_hz) }
73
- 3.times { |voice| @voices[picker][voice].draw(history, played, (sid * 4) + voice, VOICE_COLORS[voice]) }
66
+ 3.times do |voice|
67
+ @voices[picker][voice].draw(history, played, (sid * 4) + voice, PlayerTheme::VOICE_COLORS[voice])
68
+ end
74
69
  @filter.draw(history, frame, top + (3 * VOICE_HEIGHT) + 4, model)
75
- @outputs[picker].draw(history, played, history.output(sid), BRIGHT)
70
+ @outputs[picker].draw(history, played, history.output(sid), PlayerTheme::BRIGHT)
76
71
  end
77
72
 
78
73
  private
@@ -86,13 +81,14 @@ module Badline
86
81
 
87
82
  def draw_voice(history, frame, voice, top, clock_hz)
88
83
  base = voice * 7
89
- color = VOICE_COLORS[voice]
84
+ color = PlayerTheme::VOICE_COLORS[voice]
90
85
  control = history.register(frame, base + 4)
91
86
  hertz = history.hertz(frame, voice, clock_hz)
92
87
  paint = @painter
93
88
  paint.text(LEFT, top, "VOICE #{voice + 1}", color)
94
- paint.text(LEFT, top + 12, SIDView.note(hertz), history.level(frame, voice).zero? ? DIM : color, scale: 2)
95
- paint.text(LEFT + 112, top + 12, format("%<hertz>.1f HZ", hertz:), TEXT)
89
+ paint.text(LEFT, top + 12, SIDView.note(hertz), history.level(frame, voice).zero? ? PlayerTheme::DIM : color,
90
+ scale: 2)
91
+ paint.text(LEFT + 112, top + 12, format("%<hertz>.1f HZ", hertz:), PlayerTheme::TEXT)
96
92
  draw_waves(control, top, color)
97
93
  draw_flags(history, frame, voice, control, top + 24)
98
94
  draw_envelope(history, frame, voice, top + 36, color)
@@ -101,17 +97,17 @@ module Badline
101
97
  def draw_waves(control, top, color)
102
98
  x = LEFT + 224
103
99
  WAVES.each_with_index do |wave, bit|
104
- x += @painter.text(x, top, wave, control[bit + 4] == 1 ? color : DIM) + 8
100
+ x += @painter.text(x, top, wave, control[bit + 4] == 1 ? color : PlayerTheme::DIM) + 8
105
101
  end
106
102
  end
107
103
 
108
104
  def draw_flags(history, frame, voice, control, top)
109
105
  x = LEFT + 112
110
106
  FLAGS.each_with_index do |flag, bit|
111
- x += @painter.text(x, top, flag, control[bit] == 1 ? BRIGHT : DIM) + 8
107
+ x += @painter.text(x, top, flag, control[bit] == 1 ? PlayerTheme::BRIGHT : PlayerTheme::DIM) + 8
112
108
  end
113
109
  filtered = history.register(frame, 0x17)[voice] == 1
114
- @painter.text(x, top, "FILT", filtered ? BRIGHT : DIM)
110
+ @painter.text(x, top, "FILT", filtered ? PlayerTheme::BRIGHT : PlayerTheme::DIM)
115
111
  end
116
112
 
117
113
  def draw_envelope(history, frame, voice, top, color)
@@ -120,14 +116,14 @@ module Badline
120
116
  sustain_release = history.register(frame, base + 6)
121
117
  paint = @painter
122
118
  paint.text(LEFT + 112, top, format("PW %<pw>5.1f%%", pw: pulse_width(history, frame, voice) * 100.0 / 4096),
123
- TEXT)
119
+ PlayerTheme::TEXT)
124
120
  rates = "A#{hex(attack_decay >> 4)} D#{hex(attack_decay & 0x0f)} " \
125
121
  "S#{hex(sustain_release >> 4)} R#{hex(sustain_release & 0x0f)}"
126
- paint.text(LEFT + 224, top, rates, TEXT)
127
- paint.box(LEFT, top + 12, 128, 6, BOX)
122
+ paint.text(LEFT + 224, top, rates, PlayerTheme::TEXT)
123
+ paint.box(LEFT, top + 12, 128, 6, PlayerTheme::BOX)
128
124
  paint.box(LEFT, top + 12, history.level(frame, voice) / 2, 6, color)
129
- paint.box(LEFT + ((sustain_release >> 4) * 17 / 2), top + 10, 1, 10, BRIGHT)
130
- paint.text(LEFT + 136, top + 12, PHASES.fetch(history.state(frame, voice), ""), DIM)
125
+ paint.box(LEFT + ((sustain_release >> 4) * 17 / 2), top + 10, 1, 10, PlayerTheme::BRIGHT)
126
+ paint.text(LEFT + 136, top + 12, PHASES.fetch(history.state(frame, voice), ""), PlayerTheme::DIM)
131
127
  end
132
128
 
133
129
  def hex(nibble) = nibble.to_s(16).upcase
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The SID player's colours, which its window and views share, from the
6
+ # C64's palette: one for each voice, and light blue on blue as on the
7
+ # machine's screen.
8
+ module PlayerTheme
9
+ TEXT = VIC::PALETTE[14]
10
+ BRIGHT = VIC::PALETTE[1]
11
+ DIM = VIC::PALETTE[11]
12
+ BOX = VIC::PALETTE[0]
13
+ BACKGROUND = VIC::PALETTE[6]
14
+ WARNING = VIC::PALETTE[10]
15
+ VOICE_COLORS = [VIC::PALETTE[7], VIC::PALETTE[13], VIC::PALETTE[3]].freeze
16
+ end
17
+ end
18
+ end
@@ -69,10 +69,10 @@ module Badline
69
69
  top = row_top(count, sid)
70
70
  scopes = @voices[count - 1]
71
71
  3.times do |voice|
72
- color = SIDView::VOICE_COLORS[voice]
72
+ color = PlayerTheme::VOICE_COLORS[voice]
73
73
  note = SIDView.note(history.hertz(frame, voice, clock_hz))
74
74
  silent = history.level(frame, voice).zero?
75
- @painter.text(left(voice), top, note, silent ? SIDView::DIM : color, scale: count == 1 ? 2 : 1)
75
+ @painter.text(left(voice), top, note, silent ? PlayerTheme::DIM : color, scale: count == 1 ? 2 : 1)
76
76
  scopes[(sid * 3) + voice].draw(history, played, (sid * 4) + voice, color)
77
77
  end
78
78
  draw_label(top, sid) if count > 1
@@ -80,21 +80,21 @@ module Badline
80
80
 
81
81
  def draw_label(top, sid)
82
82
  label = "SID #{sid + 1}"
83
- @painter.text(SIDView::LEFT + MIX_WIDTH - Painter.width(label), top, label, SIDView::TEXT)
83
+ @painter.text(SIDView::LEFT + MIX_WIDTH - Painter.width(label), top, label, PlayerTheme::TEXT)
84
84
  end
85
85
 
86
86
  def draw_mix(history, played, count)
87
87
  if count == 1
88
- @mix.draw(history, played, SIDHistory::MIX, SIDView::BRIGHT)
88
+ @mix.draw(history, played, SIDHistory::MIX, PlayerTheme::BRIGHT)
89
89
  return
90
90
  end
91
91
 
92
92
  sides = @sides[count - 2]
93
- sides[0].draw(history, played, history.left, SIDView::BRIGHT)
94
- sides[1].draw(history, played, history.right, SIDView::BRIGHT)
93
+ sides[0].draw(history, played, history.left, PlayerTheme::BRIGHT)
94
+ sides[1].draw(history, played, history.right, PlayerTheme::BRIGHT)
95
95
  top = mix_top(count) + 2
96
- @painter.text(SIDView::LEFT + 2, top, "L", SIDView::DIM)
97
- @painter.text(SIDView::LEFT + MIX_WIDTH - 10, top, "R", SIDView::DIM)
96
+ @painter.text(SIDView::LEFT + 2, top, "L", PlayerTheme::DIM)
97
+ @painter.text(SIDView::LEFT + MIX_WIDTH - 10, top, "R", PlayerTheme::DIM)
98
98
  end
99
99
  end
100
100
  end
@@ -14,33 +14,33 @@ module Badline
14
14
  # frame that would take the queue past LIMIT is dropped whole, which
15
15
  # only happens unpaced.
16
16
  #
17
- # Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
18
- # The chip's cycles are converted at `clock_hz`, the machine's clock, so
19
- # an NTSC or Drean machine plays at its own pitch.
17
+ # Each frame's samples go to a mono AudioDevice. The chip's cycles are
18
+ # converted at `clock_hz`, the machine's clock, so an NTSC or Drean
19
+ # machine plays at its own pitch.
20
20
  class Sound
21
21
  RATE = 44_100
22
22
  AHEAD = 0.08
23
23
  LIMIT = 0.25
24
24
 
25
- attr_reader :rate, :underruns, :dropped, :queued, :low, :high
25
+ attr_reader :underruns, :dropped, :queued, :low, :high
26
26
 
27
27
  def initialize(source, clock_hz, wanted, verbose)
28
28
  @source = source
29
29
  @clock_hz = clock_hz
30
30
  @verbose = verbose
31
- @device = 0
32
- @rate = RATE
31
+ @device = AudioDevice.new(channels: 1, rate: RATE, samples: 512)
33
32
  @started = false
34
33
  @muted = false
35
34
  @underruns = 0
36
35
  @dropped = 0
37
36
  @queued = 0
38
- @buffer = IO::Buffer.new(4096)
39
37
  reset_levels
40
38
  open_device if wanted
41
39
  end
42
40
 
43
- def on? = @device != 0
41
+ def rate = @device.rate
42
+
43
+ def on? = @device.open?
44
44
 
45
45
  # Plays another machine's sound from here on, at that machine's clock.
46
46
  def switch(source, clock_hz)
@@ -60,7 +60,7 @@ module Badline
60
60
  return if @muted || samples.empty?
61
61
 
62
62
  level = queued_seconds
63
- if level + (samples.size.to_f / @rate) > LIMIT
63
+ if level + (samples.size.to_f / rate) > LIMIT
64
64
  @dropped += samples.size
65
65
  else
66
66
  play(samples, level)
@@ -80,7 +80,7 @@ module Badline
80
80
  return unless @muted
81
81
 
82
82
  halt
83
- SDL.SDL_ClearQueuedAudio(@device)
83
+ @device.clear
84
84
  end
85
85
 
86
86
  def reset_levels
@@ -89,32 +89,22 @@ module Badline
89
89
  end
90
90
 
91
91
  def close
92
- SDL.SDL_CloseAudioDevice(@device) if on?
92
+ @device.close
93
93
  end
94
94
 
95
95
  private
96
96
 
97
97
  def open_device
98
- unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
99
- puts "No sound: #{SDL.SDL_GetError}"
100
- return
101
- end
102
-
103
- SDL.spec_freq(SDL.wanted, RATE)
104
- SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
105
- SDL.spec_channels(SDL.wanted, 1)
106
- SDL.spec_samples(SDL.wanted, 512)
107
- @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, SDL::ALLOW_FREQUENCY_CHANGE)
98
+ @device.open(false)
108
99
  return puts "No sound: #{SDL.SDL_GetError}" unless on?
109
100
 
110
- @rate = SDL.read_i32(SDL.obtained)
111
101
  record
112
- puts "Sound at #{@rate} Hz" if @verbose
102
+ puts "Sound at #{rate} Hz" if @verbose
113
103
  end
114
104
 
115
- def record = @source.record(rate: @rate, clock_hz: @clock_hz)
105
+ def record = @source.record(rate: @device.rate, clock_hz: @clock_hz)
116
106
 
117
- def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
107
+ def queued_seconds = @device.queued_seconds
118
108
 
119
109
  def play(samples, level)
120
110
  @low = level if level < @low
@@ -126,29 +116,20 @@ module Badline
126
116
  end
127
117
 
128
118
  def queue(samples)
129
- count = samples.size
130
- bytes = count * 2
131
- @buffer.resize(bytes) if bytes > @buffer.size
132
- buffer = @buffer
133
- i = 0
134
- while i < count
135
- buffer.set_value(:s16, i * 2, samples[i])
136
- i += 1
137
- end
138
- SDL.SDL_QueueAudio(@device, buffer, bytes)
139
- @queued += count
119
+ @device.queue(samples)
120
+ @queued += samples.size
140
121
  end
141
122
 
142
123
  def start
143
124
  return if @started
144
125
 
145
126
  @started = true
146
- SDL.SDL_PauseAudioDevice(@device, 0)
127
+ @device.start
147
128
  end
148
129
 
149
130
  def halt
150
131
  @started = false
151
- SDL.SDL_PauseAudioDevice(@device, 1)
132
+ @device.pause
152
133
  end
153
134
 
154
135
  def underrun!
@@ -183,12 +183,11 @@ module Badline
183
183
  return puts "No #{what} to eject" unless ejectable?(computer, what)
184
184
 
185
185
  if what == "disk"
186
- eject_disk(computer)
186
+ MediaSlots.eject_disk(computer)
187
187
  elsif what == "tape"
188
188
  computer.datasette.eject
189
189
  else
190
- computer.address_bus.detach_cartridge
191
- computer.power_cycle!
190
+ MediaSlots.remove_cartridge(computer)
192
191
  end
193
192
  puts "Ejected the #{what}"
194
193
  end
@@ -200,17 +199,7 @@ module Badline
200
199
  return !computer.datasette.tape.nil? if what == "tape"
201
200
  return !computer.address_bus.cartridge.nil? if what == "cartridge"
202
201
 
203
- drive = Media::TrueDrive.drive(computer)
204
- drive.nil? ? computer.mounted? : !drive.disk.nil?
205
- end
206
-
207
- def eject_disk(computer)
208
- drive = Media::TrueDrive.drive(computer)
209
- if drive.nil?
210
- computer.unmount
211
- else
212
- drive.insert(nil)
213
- end
202
+ MediaSlots.disk?(computer)
214
203
  end
215
204
  end
216
205
  end
@@ -9,7 +9,9 @@ require "badline/frontend/drive_led"
9
9
  require "badline/frontend/keys"
10
10
  require "badline/frontend/snapshots"
11
11
  require "badline/frontend/screenshot"
12
+ require "badline/frontend/media_slots"
12
13
  require "badline/frontend/timeline"
14
+ require "badline/frontend/audio_device"
13
15
  require "badline/frontend/sound"
14
16
  require "badline/frontend/pacer"
15
17
  require "badline/frontend/frame_report"
@@ -20,24 +22,26 @@ require "badline/frontend/machine_window"
20
22
  require "badline/frontend/app"
21
23
  require "badline/frontend/audio_sink"
22
24
  require "badline/frontend/painter"
23
- require "badline/frontend/sid_history"
24
- require "badline/frontend/sid_view"
25
- require "badline/frontend/filter_view"
26
- require "badline/frontend/scope"
27
- require "badline/frontend/visualizer_view"
25
+ require "badline/frontend/player/theme"
26
+ require "badline/frontend/player/sid_history"
27
+ require "badline/frontend/player/sid_view"
28
+ require "badline/frontend/player/filter_view"
29
+ require "badline/frontend/player/scope"
30
+ require "badline/frontend/player/visualizer_view"
28
31
  require "badline/frontend/buttons"
29
- require "badline/frontend/player_state"
30
- require "badline/frontend/player_header"
31
- require "badline/frontend/player_footer"
32
- require "badline/frontend/info_view"
33
- require "badline/frontend/player_commands"
34
- require "badline/frontend/player_screen"
35
- require "badline/frontend/player_window"
36
- require "badline/frontend/pause_menu"
37
- require "badline/frontend/file_browser"
38
- require "badline/frontend/confirmation"
39
- require "badline/frontend/name_field"
40
- require "badline/frontend/menu_dialogs"
41
- require "badline/frontend/menu_media"
42
- require "badline/frontend/menu_ports"
43
- require "badline/frontend/menu_pages"
32
+ require "badline/frontend/player/player_state"
33
+ require "badline/frontend/player/player_header"
34
+ require "badline/frontend/player/player_footer"
35
+ require "badline/frontend/player/info_view"
36
+ require "badline/frontend/player/player_commands"
37
+ require "badline/frontend/player/player_screen"
38
+ require "badline/frontend/player/player_controller"
39
+ require "badline/frontend/menu/theme"
40
+ require "badline/frontend/menu/pause_menu"
41
+ require "badline/frontend/menu/file_browser"
42
+ require "badline/frontend/menu/confirmation"
43
+ require "badline/frontend/menu/name_field"
44
+ require "badline/frontend/menu/menu_dialogs"
45
+ require "badline/frontend/menu/menu_media"
46
+ require "badline/frontend/menu/menu_ports"
47
+ require "badline/frontend/menu/menu_pages"
@@ -15,7 +15,9 @@ module Badline
15
15
  # A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
16
16
  # also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
17
17
  # (an XOR gate), which answers ATN in hardware before the DOS gets to it.
18
- # SerialPort reads the lines back into VIA 1.
18
+ # SerialPort reads the lines back into VIA 1. A drive whose
19
+ # serial_output sets DRIVE_ATN_GATED pulls DATA that way only while
20
+ # ATN is low, as the 1581's NAND does.
19
21
  #
20
22
  # Fast serial adds SRQ, which the C64 leaves alone. A C128 and a 1571
21
23
  # clock bytes over it from a CIA's CNT, with the bits on DATA from its
@@ -48,6 +50,10 @@ module Badline
48
50
  DRIVE_FAST_DATA = 0x100
49
51
  DRIVE_FAST_SRQ = 0x200
50
52
 
53
+ # A drive's serial_output bit whose ATN acknowledge pulls DATA only
54
+ # while ATN is low.
55
+ DRIVE_ATN_GATED = 0x400
56
+
51
57
  attr_reader :drives, :host_lines
52
58
 
53
59
  # The lines the host's fast serial pins pull, as DATA and SRQ bits.
@@ -119,7 +125,7 @@ module Badline
119
125
  while i < @drives.length
120
126
  drive = @drives[i].serial_output
121
127
  low |= CLK if drive.anybits?(DRIVE_CLK_OUT)
122
- low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || atn != drive.anybits?(DRIVE_ATNA)
128
+ low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || acknowledging?(drive, atn)
123
129
  i += 1
124
130
  end
125
131
  low
@@ -127,6 +133,12 @@ module Badline
127
133
 
128
134
  def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
129
135
 
136
+ # Whether a drive's ATN acknowledge pulls DATA, with its serial_output
137
+ # +drive+, while ATN is +atn+.
138
+ def acknowledging?(drive, atn)
139
+ atn != drive.anybits?(DRIVE_ATNA) && (atn || drive.nobits?(DRIVE_ATN_GATED))
140
+ end
141
+
130
142
  def clk_low? = low_lines.anybits?(CLK)
131
143
 
132
144
  def data_low? = low_lines.anybits?(DATA)
@@ -25,8 +25,9 @@ module Badline
25
25
  # +mounted?+, +datasette+, +attach_cartridge+,
26
26
  # +press_cartridge_button+, +release_cartridge_button+,
27
27
  # +attach_drive1541+, +drive1541+, +true_drive+ and +plug_true_drive+
28
- # for the drive Media::TrueDrive plugs in, and +address_bus+ for its
29
- # +cartridge+ and +detach_cartridge+.
28
+ # for the drive Media::TrueDrive plugs in, +attach_drive1581+,
29
+ # +drive1581+ and +plug_drive1581+ for a 1581 (Drive1581::Slot), and
30
+ # +address_bus+ for its +cartridge+ and +detach_cartridge+.
30
31
  # - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
31
32
  # +install_debug_register+ and +capture_output+.
32
33
  #
@@ -34,8 +35,8 @@ module Badline
34
35
  # input parts, with its one joystick as both +joystick1+ and +joystick2+
35
36
  # and nil for +control_ports+, and of the rest +ram+, +load_prg+,
36
37
  # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
37
- # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+,
38
- # +install_debug_register+ and +capture_output+.
38
+ # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+, the
39
+ # 1581's, +install_debug_register+ and +capture_output+.
39
40
  #
40
41
  # The C128 answers all but the cartridge button, and besides +mode+,
41
42
  # :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front