badline 0.11.0 → 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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CLAUDE.md +24 -6
  4. data/CONTRIBUTING.md +15 -9
  5. data/README.md +16 -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 +108 -0
  11. data/lib/badline/c128/fast_serial.rb +63 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +24 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +23 -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 +25 -26
  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 +60 -0
  39. data/lib/badline/cia/serial.rb +12 -0
  40. data/lib/badline/cia/timer.rb +39 -0
  41. data/lib/badline/cia.rb +10 -5
  42. data/lib/badline/computer/attachments.rb +4 -50
  43. data/lib/badline/computer/true_drives.rb +80 -0
  44. data/lib/badline/computer.rb +4 -0
  45. data/lib/badline/core.rb +2 -0
  46. data/lib/badline/drive/core.rb +147 -0
  47. data/lib/badline/drive/fast_serial.rb +73 -0
  48. data/lib/badline/{drive1541 → drive}/idle.rb +18 -29
  49. data/lib/badline/drive/idle_cpu.rb +28 -0
  50. data/lib/badline/{drive1541 → drive}/orbit.rb +3 -5
  51. data/lib/badline/drive/serial_lines.rb +73 -0
  52. data/lib/badline/{drive1541 → drive}/sleep.rb +4 -5
  53. data/lib/badline/drive/vias.rb +84 -0
  54. data/lib/badline/drive/watch.rb +150 -0
  55. data/lib/badline/drive.rb +11 -0
  56. data/lib/badline/drive1541/bus.rb +5 -129
  57. data/lib/badline/drive1541/byte_ready.rb +48 -0
  58. data/lib/badline/drive1541/cpu.rb +1 -19
  59. data/lib/badline/drive1541/disk.rb +77 -20
  60. data/lib/badline/drive1541/disk_state.rb +7 -2
  61. data/lib/badline/drive1541/disk_via.rb +3 -0
  62. data/lib/badline/drive1541/mechanism.rb +48 -14
  63. data/lib/badline/drive1541/mechanism_state.rb +5 -4
  64. data/lib/badline/drive1541/rotation.rb +5 -5
  65. data/lib/badline/drive1541/serial_port.rb +4 -60
  66. data/lib/badline/drive1541.rb +14 -172
  67. data/lib/badline/drive1571/bus.rb +111 -0
  68. data/lib/badline/drive1571/cpu.rb +11 -0
  69. data/lib/badline/drive1571/serial_port.rb +37 -0
  70. data/lib/badline/drive1571/serial_via.rb +25 -0
  71. data/lib/badline/drive1571/wd1770.rb +120 -0
  72. data/lib/badline/drive1571.rb +175 -0
  73. data/lib/badline/drive1581/bus.rb +137 -0
  74. data/lib/badline/drive1581/cpu.rb +11 -0
  75. data/lib/badline/drive1581/disk.rb +135 -0
  76. data/lib/badline/drive1581/disk_state.rb +87 -0
  77. data/lib/badline/drive1581/mechanism.rb +117 -0
  78. data/lib/badline/drive1581/serial_port.rb +52 -0
  79. data/lib/badline/drive1581/slot.rb +78 -0
  80. data/lib/badline/drive1581/track.rb +100 -0
  81. data/lib/badline/drive1581/track_bytes.rb +75 -0
  82. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  83. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  84. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  85. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  86. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  87. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  88. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  89. data/lib/badline/drive1581/wd1772.rb +216 -0
  90. data/lib/badline/drive1581.rb +233 -0
  91. data/lib/badline/frontend/app.rb +2 -4
  92. data/lib/badline/frontend/audio_device.rb +67 -0
  93. data/lib/badline/frontend/audio_sink.rb +16 -50
  94. data/lib/badline/frontend/boot.rb +18 -10
  95. data/lib/badline/frontend/buttons.rb +7 -7
  96. data/lib/badline/frontend/drive_led.rb +1 -1
  97. data/lib/badline/frontend/keys.rb +33 -8
  98. data/lib/badline/frontend/media_slots.rb +42 -0
  99. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  100. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  101. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  102. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  103. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +7 -9
  104. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  105. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  106. data/lib/badline/frontend/menu/theme.rb +18 -0
  107. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  108. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  109. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  110. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  111. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  112. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  113. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  114. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  115. data/lib/badline/frontend/player/theme.rb +18 -0
  116. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  117. data/lib/badline/frontend/sound.rb +19 -38
  118. data/lib/badline/frontend/timeline.rb +3 -14
  119. data/lib/badline/frontend.rb +24 -20
  120. data/lib/badline/iec_bus.rb +68 -6
  121. data/lib/badline/machine.rb +9 -6
  122. data/lib/badline/media/boot_disk.rb +50 -0
  123. data/lib/badline/media/not_disk.rb +8 -0
  124. data/lib/badline/media/true_drive.rb +48 -25
  125. data/lib/badline/media.rb +9 -3
  126. data/lib/badline/options/event.rb +2 -2
  127. data/lib/badline/options/family.rb +3 -9
  128. data/lib/badline/options/help.rb +3 -1
  129. data/lib/badline/options/table.rb +1 -1
  130. data/lib/badline/roms/README +18 -0
  131. data/lib/badline/roms/dos1571.rom +0 -0
  132. data/lib/badline/roms/dos1581.rom +0 -0
  133. data/lib/badline/sdl.rb +37 -20
  134. data/lib/badline/snapshot/state.rb +5 -4
  135. data/lib/badline/storage/d64_image.rb +3 -0
  136. data/lib/badline/storage/d71_image.rb +9 -4
  137. data/lib/badline/storage/g64_image.rb +7 -2
  138. data/lib/badline/storage/p00.rb +8 -0
  139. data/lib/badline/time_of_day.rb +9 -0
  140. data/lib/badline/version.rb +1 -1
  141. data/lib/badline/via/fast_forward.rb +3 -3
  142. data/lib/badline/vic20/attachments.rb +23 -3
  143. data/lib/badline/vic20/saved_state.rb +2 -0
  144. data/lib/badline/vic20.rb +3 -0
  145. data/lib/badline/z80/arithmetic.rb +117 -0
  146. data/lib/badline/z80/bit_page.rb +55 -0
  147. data/lib/badline/z80/block_io.rb +58 -0
  148. data/lib/badline/z80/block_transfers.rb +59 -0
  149. data/lib/badline/z80/bus_cycles.rb +111 -0
  150. data/lib/badline/z80/core.rb +113 -0
  151. data/lib/badline/z80/exchanges.rb +50 -0
  152. data/lib/badline/z80/extended_page.rb +89 -0
  153. data/lib/badline/z80/flags.rb +37 -0
  154. data/lib/badline/z80/interrupt_acceptance.rb +46 -0
  155. data/lib/badline/z80/jumps.rb +82 -0
  156. data/lib/badline/z80/loads.rb +98 -0
  157. data/lib/badline/z80/main_page.rb +134 -0
  158. data/lib/badline/z80/registers.rb +120 -0
  159. data/lib/badline/z80/saved_state.rb +69 -0
  160. data/lib/badline/z80/shifts.rb +64 -0
  161. data/lib/badline/z80.rb +11 -0
  162. metadata +89 -24
  163. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  164. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  165. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  166. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -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
@@ -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"