badline 0.11.1 → 0.12.1

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -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/address_bus/saved_state.rb +4 -10
  10. data/lib/badline/address_bus.rb +8 -27
  11. data/lib/badline/audio/bare_player.rb +1 -1
  12. data/lib/badline/c128/bus.rb +17 -52
  13. data/lib/badline/c128/bus_state.rb +5 -5
  14. data/lib/badline/c128/drives.rb +35 -22
  15. data/lib/badline/c128/fast_serial.rb +1 -0
  16. data/lib/badline/c128/io_pages.rb +35 -0
  17. data/lib/badline/c128/mmu.rb +9 -7
  18. data/lib/badline/c128/modes.rb +17 -4
  19. data/lib/badline/c128/saved_state.rb +22 -0
  20. data/lib/badline/c128/z80.rb +49 -0
  21. data/lib/badline/c128/z80_pages.rb +118 -0
  22. data/lib/badline/c128/z80_turns.rb +106 -0
  23. data/lib/badline/c128.rb +21 -83
  24. data/lib/badline/cartridge/action_replay.rb +2 -2
  25. data/lib/badline/cartridge/comal80.rb +1 -7
  26. data/lib/badline/cartridge/dinamic.rb +0 -8
  27. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  28. data/lib/badline/cartridge/fun_play.rb +0 -8
  29. data/lib/badline/cartridge/game_system.rb +0 -8
  30. data/lib/badline/cartridge/kcs_power.rb +10 -7
  31. data/lib/badline/cartridge/mach5.rb +2 -4
  32. data/lib/badline/cartridge/magic_desk.rb +0 -8
  33. data/lib/badline/cartridge/ocean.rb +0 -8
  34. data/lib/badline/cartridge/pagefox.rb +2 -2
  35. data/lib/badline/cartridge/retro_replay.rb +7 -6
  36. data/lib/badline/cartridge/rex_utility.rb +2 -6
  37. data/lib/badline/cartridge/rgcd.rb +2 -2
  38. data/lib/badline/cartridge/simons_basic.rb +0 -5
  39. data/lib/badline/cartridge/super_games.rb +4 -2
  40. data/lib/badline/cartridge/westermann.rb +2 -6
  41. data/lib/badline/cartridge.rb +37 -9
  42. data/lib/badline/cia/fast_forward.rb +21 -3
  43. data/lib/badline/cia.rb +8 -5
  44. data/lib/badline/computer/attachments.rb +14 -95
  45. data/lib/badline/computer/common.rb +34 -0
  46. data/lib/badline/computer/kernal_traps.rb +65 -5
  47. data/lib/badline/computer/true_drives.rb +69 -0
  48. data/lib/badline/computer.rb +9 -68
  49. data/lib/badline/core.rb +4 -2
  50. data/lib/badline/cpu_port.rb +112 -0
  51. data/lib/badline/drive/core.rb +8 -33
  52. data/lib/badline/drive/fast_serial.rb +73 -0
  53. data/lib/badline/drive/idle.rb +1 -11
  54. data/lib/badline/drive/orbit.rb +2 -5
  55. data/lib/badline/drive/sleep.rb +0 -2
  56. data/lib/badline/drive/vias.rb +84 -0
  57. data/lib/badline/drive.rb +2 -0
  58. data/lib/badline/drive1541/disk.rb +14 -17
  59. data/lib/badline/drive1541/disk_state.rb +7 -15
  60. data/lib/badline/drive1541.rb +1 -0
  61. data/lib/badline/drive1571/wd1770.rb +94 -8
  62. data/lib/badline/drive1571.rb +8 -58
  63. data/lib/badline/drive1581/bus.rb +137 -0
  64. data/lib/badline/drive1581/cpu.rb +11 -0
  65. data/lib/badline/drive1581/disk.rb +135 -0
  66. data/lib/badline/drive1581/disk_state.rb +87 -0
  67. data/lib/badline/drive1581/mechanism.rb +117 -0
  68. data/lib/badline/drive1581/serial_port.rb +52 -0
  69. data/lib/badline/drive1581/slot.rb +78 -0
  70. data/lib/badline/drive1581/track.rb +100 -0
  71. data/lib/badline/drive1581/track_bytes.rb +75 -0
  72. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  73. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  74. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  75. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  76. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  77. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  78. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  79. data/lib/badline/drive1581/wd1772.rb +216 -0
  80. data/lib/badline/drive1581.rb +233 -0
  81. data/lib/badline/frontend/app.rb +1 -3
  82. data/lib/badline/frontend/audio_device.rb +67 -0
  83. data/lib/badline/frontend/audio_sink.rb +16 -50
  84. data/lib/badline/frontend/boot.rb +24 -67
  85. data/lib/badline/frontend/buttons.rb +7 -7
  86. data/lib/badline/frontend/keys.rb +33 -8
  87. data/lib/badline/frontend/media_slots.rb +42 -0
  88. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  89. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  90. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  91. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  92. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  93. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  94. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  95. data/lib/badline/frontend/menu/theme.rb +18 -0
  96. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  97. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  98. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  99. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  100. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  101. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  102. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  103. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  104. data/lib/badline/frontend/player/theme.rb +18 -0
  105. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  106. data/lib/badline/frontend/sound.rb +19 -38
  107. data/lib/badline/frontend/timeline.rb +3 -14
  108. data/lib/badline/frontend.rb +24 -20
  109. data/lib/badline/iec_bus.rb +14 -2
  110. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  111. data/lib/badline/kernal_trap/load.rb +1 -1
  112. data/lib/badline/kernal_trap/save.rb +1 -1
  113. data/lib/badline/kernal_trap.rb +1 -1
  114. data/lib/badline/machine/clocking.rb +50 -0
  115. data/lib/badline/machine.rb +63 -6
  116. data/lib/badline/media/boot_disk.rb +50 -0
  117. data/lib/badline/media/disk_list.rb +3 -3
  118. data/lib/badline/media/disk_set.rb +1 -1
  119. data/lib/badline/media/extensions.rb +33 -0
  120. data/lib/badline/media/not_disk.rb +8 -0
  121. data/lib/badline/media/program.rb +28 -0
  122. data/lib/badline/media/true_drive.rb +38 -17
  123. data/lib/badline/media/tune.rb +35 -0
  124. data/lib/badline/media/vic20_cartridge.rb +1 -7
  125. data/lib/badline/media/vic20_media.rb +38 -80
  126. data/lib/badline/media.rb +90 -81
  127. data/lib/badline/options/event.rb +4 -2
  128. data/lib/badline/options/family.rb +3 -9
  129. data/lib/badline/options/help.rb +2 -1
  130. data/lib/badline/roms/README +9 -0
  131. data/lib/badline/roms/dos1581.rom +0 -0
  132. data/lib/badline/sdl.rb +37 -20
  133. data/lib/badline/sid.rb +1 -1
  134. data/lib/badline/snapshot/machine_state.rb +9 -1
  135. data/lib/badline/snapshot/state.rb +6 -4
  136. data/lib/badline/snapshot.rb +11 -9
  137. data/lib/badline/storage/d64_image.rb +15 -7
  138. data/lib/badline/storage/disk_image/writing.rb +2 -2
  139. data/lib/badline/storage/disk_image.rb +1 -1
  140. data/lib/badline/storage/host_directory.rb +1 -1
  141. data/lib/badline/storage/p00.rb +8 -0
  142. data/lib/badline/storage/t64.rb +1 -1
  143. data/lib/badline/time_of_day.rb +3 -3
  144. data/lib/badline/version.rb +1 -1
  145. data/lib/badline/vic/bank.rb +1 -1
  146. data/lib/badline/vic/border_mask.rb +1 -1
  147. data/lib/badline/vic/collisions.rb +1 -1
  148. data/lib/badline/vic/color_patches.rb +1 -1
  149. data/lib/badline/vic/display_state.rb +1 -1
  150. data/lib/badline/vic/graphics_mode.rb +1 -1
  151. data/lib/badline/vic/graphics_shifter.rb +1 -1
  152. data/lib/badline/vic/register_log.rb +1 -1
  153. data/lib/badline/vic/registers.rb +1 -1
  154. data/lib/badline/vic/sequencer.rb +1 -1
  155. data/lib/badline/vic/sequencer_output.rb +1 -1
  156. data/lib/badline/vic/sequencer_state.rb +1 -1
  157. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  158. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  159. data/lib/badline/vic/sprite/shifter.rb +1 -1
  160. data/lib/badline/vic/sprite/timing.rb +1 -1
  161. data/lib/badline/vic/sprite.rb +1 -1
  162. data/lib/badline/vic/sprites.rb +1 -1
  163. data/lib/badline/vic/sprites_state.rb +1 -1
  164. data/lib/badline/vic.rb +6 -5
  165. data/lib/badline/vic20/attachments.rb +15 -71
  166. data/lib/badline/vic20/saved_state.rb +2 -30
  167. data/lib/badline/vic20.rb +4 -37
  168. data/lib/badline/z80/core.rb +2 -0
  169. data/lib/badline/z80/saved_state.rb +69 -0
  170. metadata +61 -23
  171. data/lib/badline/cycleable.rb +0 -38
  172. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  173. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  174. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  175. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -9,13 +9,10 @@ module Badline
9
9
  class MenuPages
10
10
  include MenuPorts
11
11
 
12
- TEXT = PauseMenu::TEXT
13
- DIM = PauseMenu::DIM
14
12
  LINE = 12
15
13
  COLUMNS = 25
16
14
  ROW_WIDTH = 200
17
15
 
18
- WARNING = 0xff7a6b
19
16
  RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
20
17
 
21
18
  SID_MODELS = %i[mos6581 mos8580].freeze
@@ -51,7 +48,7 @@ module Badline
51
48
 
52
49
  def draw(page, left, top)
53
50
  problem = @media.problem
54
- @painter.text(left, top + 186, problem[0, COLUMNS], WARNING) unless problem.empty?
51
+ @painter.text(left, top + 186, problem[0, COLUMNS], MenuTheme::WARNING) unless problem.empty?
55
52
  @left = left
56
53
  @top = top
57
54
  case page
@@ -95,7 +92,7 @@ module Badline
95
92
  # Says in the dim colour that the VIC-20 hasn't got what the page
96
93
  # shows.
97
94
  def note
98
- @painter.text(@left, @top, "NOT ON THE VIC-20 YET", DIM)
95
+ @painter.text(@left, @top, "NOT ON THE VIC-20 YET", MenuTheme::DIM)
99
96
  end
100
97
 
101
98
  def draw_drive
@@ -182,11 +179,11 @@ module Badline
182
179
  # A label, and its value under it, or NONE in the dim colour when the
183
180
  # value is empty.
184
181
  def info(label, value)
185
- @painter.text(@left, @top, label, DIM)
182
+ @painter.text(@left, @top, label, MenuTheme::DIM)
186
183
  if value.empty?
187
- @painter.text(@left, @top + LINE, "NONE", DIM)
184
+ @painter.text(@left, @top + LINE, "NONE", MenuTheme::DIM)
188
185
  else
189
- @painter.text(@left, @top + LINE, Painter.fit(value, COLUMNS), TEXT)
186
+ @painter.text(@left, @top + LINE, Painter.fit(value, COLUMNS), MenuTheme::TEXT)
190
187
  end
191
188
  @top += (LINE * 2) + 6
192
189
  end
@@ -8,14 +8,8 @@ module Badline
8
8
  # name and Esc leaves it.
9
9
  class NameField
10
10
  COLUMNS = 30
11
- LETTERS = "abcdefghijklmnopqrstuvwxyz"
12
- DIGITS = "1234567890"
13
11
  OTHERS = { 44 => [" ", " "], 45 => ["-", "_"], 55 => [".", "."] }.freeze
14
12
 
15
- RETURN = 40
16
- ESCAPE = 41
17
- BACKSPACE = 42
18
-
19
13
  attr_reader :text
20
14
 
21
15
  def initialize(painter)
@@ -47,11 +41,11 @@ module Badline
47
41
  # Handles a key, and returns :done for Return, :cancel for Esc, or
48
42
  # nil.
49
43
  def key(scancode, shift)
50
- return :done if scancode == RETURN
51
- return :cancel if scancode == ESCAPE
44
+ return :done if scancode == Keys::RETURN
45
+ return :cancel if scancode == Keys::ESCAPE
52
46
 
53
47
  @problem = ""
54
- if scancode == BACKSPACE
48
+ if scancode == Keys::BACKSPACE
55
49
  @text = @text[0, @text.length - 1].to_s
56
50
  else
57
51
  char = character(scancode, shift)
@@ -62,26 +56,19 @@ module Badline
62
56
 
63
57
  def draw(left, top, width)
64
58
  painter = @painter
65
- painter.text(left, top, @title, PauseMenu::BRIGHT)
66
- painter.box(left, top + 18, width, Buttons::HEIGHT + 4, PauseMenu::FILL)
67
- painter.text(left + 4, top + 22, "#{@text}_", PauseMenu::TEXT)
68
- painter.text(left, top + 42, @problem, MenuPages::WARNING) unless @problem.empty?
69
- painter.text(left, top + 60, "RETURN: SAVE ESC: CANCEL", PauseMenu::DIM)
59
+ painter.text(left, top, @title, MenuTheme::BRIGHT)
60
+ painter.box(left, top + 18, width, Buttons::HEIGHT + 4, MenuTheme::FILL)
61
+ painter.text(left + 4, top + 22, "#{@text}_", MenuTheme::TEXT)
62
+ painter.text(left, top + 42, @problem, MenuTheme::WARNING) unless @problem.empty?
63
+ painter.text(left, top + 60, "RETURN: SAVE ESC: CANCEL", MenuTheme::DIM)
70
64
  end
71
65
 
72
66
  private
73
67
 
74
68
  def character(scancode, shift)
75
- if scancode.between?(4, 29)
76
- letter = LETTERS[scancode - 4]
77
- shift ? letter.upcase : letter
78
- elsif scancode.between?(30, 39)
79
- DIGITS[scancode - 30]
80
- elsif OTHERS.key?(scancode)
81
- OTHERS[scancode][shift ? 1 : 0]
82
- else
83
- ""
84
- end
69
+ return OTHERS[scancode][shift ? 1 : 0] if OTHERS.key?(scancode)
70
+
71
+ Keys.character(scancode, shift)
85
72
  end
86
73
  end
87
74
  end
@@ -17,30 +17,18 @@ module Badline
17
17
  SECTIONS = ["SNAPSHOTS", "DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
18
18
  PAGES = %i[snapshots drive datasette expansion ports sound power].freeze
19
19
 
20
- PANEL = 0x1d2230
21
- EDGE = 0x3a4256
22
- TEXT = 0xc9d1e0
23
- BRIGHT = 0xffc66d
24
- DIM = 0x6b7489
25
- FILL = 0x343c50
26
-
27
20
  MARGIN = 16
28
21
  WIDTH = 352
29
22
  HEIGHT = 240
30
23
  BODY = 138
31
24
 
32
- ESCAPE = 41
33
- RETURN = 40
34
- SPACE = 44
35
25
  SNAPSHOT_ACTIONS = %i[quicksave_now save_as load_save].freeze
36
- ARROWS = { 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
37
- MOUSEWHEEL = 0x403
38
26
 
39
27
  # Takes the media, the REU and whether disks are writable from Options.
40
28
  def initialize(painter, options, snapshots)
41
29
  media_path = options.media_path.to_s
42
30
  @painter = painter
43
- @buttons = Buttons.new(painter, [TEXT, BRIGHT, PANEL, FILL])
31
+ @buttons = Buttons.new(painter, [MenuTheme::TEXT, MenuTheme::BRIGHT, MenuTheme::PANEL, MenuTheme::FILL])
44
32
  @pages = MenuPages.new(painter, @buttons, media_path, options, snapshots)
45
33
  @snapshots = snapshots
46
34
  @dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options, snapshots)
@@ -76,10 +64,7 @@ module Badline
76
64
  # goes straight in, and anything else asks first.
77
65
  def drop(computer, controls, sound)
78
66
  show(computer, controls, sound)
79
- file = SDL.event_file(SDL.event)
80
- asks = @dialogs.drop(LibC.strstr(file, ""))
81
- LibC.free(file)
82
- asks
67
+ @dialogs.drop(SDL.dropped_file)
83
68
  end
84
69
 
85
70
  # Handles a key pressed while the menu is open: the arrows move
@@ -91,10 +76,10 @@ module Badline
91
76
  def key(scancode)
92
77
  return :resume if scancode == Keys::F9
93
78
  return @dialogs.key(scancode) if @dialogs.open?
94
- return :resume if scancode == ESCAPE
95
- return choose(@buttons.focus) if [RETURN, SPACE].include?(scancode)
79
+ return :resume if scancode == Keys::ESCAPE
80
+ return choose(@buttons.focus) if [Keys::RETURN, Keys::SPACE].include?(scancode)
96
81
 
97
- direction = ARROWS[scancode]
82
+ direction = Keys::DIRECTIONS[scancode]
98
83
  move(direction) unless direction.nil?
99
84
  nil
100
85
  end
@@ -134,18 +119,18 @@ module Badline
134
119
  def draw
135
120
  painter = @painter
136
121
  @buttons.forget
137
- painter.box(@left, @top, WIDTH, HEIGHT, PANEL)
138
- painter.box(@left, @top + 24, WIDTH, 1, EDGE)
139
- painter.text(@left + 8, @top + 9, "PAUSED", BRIGHT)
122
+ painter.box(@left, @top, WIDTH, HEIGHT, MenuTheme::PANEL)
123
+ painter.box(@left, @top + 24, WIDTH, 1, MenuTheme::EDGE)
124
+ painter.text(@left + 8, @top + 9, "PAUSED", MenuTheme::BRIGHT)
140
125
  resume = "RESUME"
141
126
  @buttons.plain(@left + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), @top + 7, resume, :resume)
142
127
  return @dialogs.draw(@left + 8, @top + 34, WIDTH - 16) if @dialogs.open?
143
128
 
144
- painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, EDGE)
129
+ painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, MenuTheme::EDGE)
145
130
  draw_sections
146
131
  @buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
147
- painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
148
- painter.text(@left + BODY, @top + HEIGHT - 12, "F6: 40/80 KEY F8: SCREEN", DIM) if @pages.c128?
132
+ painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", MenuTheme::DIM)
133
+ painter.text(@left + BODY, @top + HEIGHT - 12, "F6: 40/80 KEY F8: SCREEN", MenuTheme::DIM) if @pages.c128?
149
134
  @pages.draw(PAGES[@section], @left + BODY, @top + 34)
150
135
  end
151
136
 
@@ -241,13 +226,13 @@ module Badline
241
226
  when SDL::MOUSEMOTION then @buttons.point(SDL.event_x(event), SDL.event_y(event)) && nil
242
227
  when SDL::MOUSEBUTTONDOWN
243
228
  SDL.event_button(event) == 1 ? click(SDL.event_x(event), SDL.event_y(event)) : nil
244
- when MOUSEWHEEL then @dialogs.scroll(-3 * SDL.event_x(event)) && nil
229
+ when SDL::MOUSEWHEEL then @dialogs.scroll(-3 * SDL.event_x(event)) && nil
245
230
  end
246
231
  end
247
232
 
248
233
  # A key held down repeats only the arrows.
249
234
  def key_down(scancode, repeat)
250
- repeat.zero? || ARROWS.key?(scancode) ? key(scancode) : nil
235
+ repeat.zero? || Keys::DIRECTIONS.key?(scancode) ? key(scancode) : nil
251
236
  end
252
237
 
253
238
  def draw_sections
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The pause menu's colours, which its pages and dialogs share. They lie
6
+ # outside the C64's palette, so the menu reads as the emulator's and
7
+ # not the machine's.
8
+ module MenuTheme
9
+ PANEL = 0x1d2230
10
+ EDGE = 0x3a4256
11
+ TEXT = 0xc9d1e0
12
+ BRIGHT = 0xffc66d
13
+ DIM = 0x6b7489
14
+ FILL = 0x343c50
15
+ WARNING = 0xff7a6b
16
+ end
17
+ end
18
+ end
@@ -6,10 +6,10 @@ module Badline
6
6
  # resonance and volume, and its response across the audible range on
7
7
  # the chip playing, whose cutoff curve the two models don't share.
8
8
  class FilterView
9
- TEXT = SIDView::TEXT
10
- BRIGHT = SIDView::BRIGHT
11
- DIM = SIDView::DIM
12
- BOX = SIDView::BOX
9
+ TEXT = PlayerTheme::TEXT
10
+ BRIGHT = PlayerTheme::BRIGHT
11
+ DIM = PlayerTheme::DIM
12
+ BOX = PlayerTheme::BOX
13
13
 
14
14
  MODES = ["LP", "BP", "HP", "3 OFF"].freeze
15
15
 
@@ -52,7 +52,7 @@ module Badline
52
52
  def draw
53
53
  index = 0
54
54
  while index < ROWS && @offset + index < @lines.size
55
- @painter.text(LEFT, @top + (index * LINE), @lines[@offset + index], SIDView::TEXT)
55
+ @painter.text(LEFT, @top + (index * LINE), @lines[@offset + index], PlayerTheme::TEXT)
56
56
  index += 1
57
57
  end
58
58
  draw_bar if @lines.size > ROWS
@@ -110,10 +110,10 @@ module Badline
110
110
 
111
111
  def draw_bar
112
112
  @buttons.area([BAR_LEFT, @top, BAR, HEIGHT], :scroll)
113
- @painter.box(BAR_LEFT, @top, BAR, HEIGHT, SIDView::BOX)
113
+ @painter.box(BAR_LEFT, @top, BAR, HEIGHT, PlayerTheme::BOX)
114
114
  thumb = [HEIGHT * ROWS / @lines.size, 8].max
115
115
  place = (HEIGHT - thumb) * @offset / [last_offset, 1].max
116
- @painter.box(BAR_LEFT, @top + place, BAR, thumb, SIDView::TEXT)
116
+ @painter.box(BAR_LEFT, @top + place, BAR, thumb, PlayerTheme::TEXT)
117
117
  end
118
118
  end
119
119
  end
@@ -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