badline 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +125 -46
  4. data/exe/badline-ruby +9 -1
  5. data/lib/badline/address_bus/extra_sids.rb +30 -0
  6. data/lib/badline/address_bus/sid_slots.rb +42 -0
  7. data/lib/badline/address_bus.rb +5 -0
  8. data/lib/badline/audio/aiff.rb +2 -2
  9. data/lib/badline/audio/bare_player.rb +23 -12
  10. data/lib/badline/audio/cli.rb +153 -58
  11. data/lib/badline/audio/jukebox.rb +121 -36
  12. data/lib/badline/audio/machine_player.rb +25 -10
  13. data/lib/badline/audio/pcm_writer.rb +10 -6
  14. data/lib/badline/audio/playback.rb +6 -5
  15. data/lib/badline/audio/queued_tune.rb +89 -0
  16. data/lib/badline/audio/renderer.rb +57 -10
  17. data/lib/badline/audio/stereo.rb +97 -0
  18. data/lib/badline/audio/terminal.rb +39 -6
  19. data/lib/badline/audio/wav.rb +1 -1
  20. data/lib/badline/audio.rb +2 -0
  21. data/lib/badline/frontend/audio_sink.rb +6 -5
  22. data/lib/badline/frontend/boot.rb +1 -1
  23. data/lib/badline/frontend/buttons.rb +90 -0
  24. data/lib/badline/frontend/filter_view.rb +96 -0
  25. data/lib/badline/frontend/info_view.rb +120 -0
  26. data/lib/badline/frontend/painter.rb +175 -0
  27. data/lib/badline/frontend/player_commands.rb +33 -0
  28. data/lib/badline/frontend/player_footer.rb +86 -0
  29. data/lib/badline/frontend/player_header.rb +60 -0
  30. data/lib/badline/frontend/player_screen.rb +124 -0
  31. data/lib/badline/frontend/player_state.rb +67 -0
  32. data/lib/badline/frontend/player_window.rb +228 -0
  33. data/lib/badline/frontend/scope.rb +42 -0
  34. data/lib/badline/frontend/sid_history.rb +175 -0
  35. data/lib/badline/frontend/sid_view.rb +140 -0
  36. data/lib/badline/frontend/visualizer_view.rb +101 -0
  37. data/lib/badline/frontend.rb +14 -0
  38. data/lib/badline/media/queue.rb +158 -0
  39. data/lib/badline/media.rb +13 -8
  40. data/lib/badline/options/help.rb +34 -5
  41. data/lib/badline/options/option.rb +1 -1
  42. data/lib/badline/options/table.rb +4 -2
  43. data/lib/badline/options/validation.rb +54 -0
  44. data/lib/badline/options.rb +62 -43
  45. data/lib/badline/sdl.rb +16 -6
  46. data/lib/badline/sid/catch_up.rb +11 -1
  47. data/lib/badline/sid/filter.rb +9 -0
  48. data/lib/badline/sid/voice.rb +6 -0
  49. data/lib/badline/sid/waveform.rb +20 -5
  50. data/lib/badline/sid.rb +35 -3
  51. data/lib/badline/storage/hvsc.rb +41 -0
  52. data/lib/badline/storage/sid_file.rb +45 -25
  53. data/lib/badline/storage/song_lengths.rb +6 -29
  54. data/lib/badline/storage/stil/credit.rb +50 -0
  55. data/lib/badline/storage/stil.rb +126 -0
  56. data/lib/badline/storage.rb +8 -0
  57. data/lib/badline/version.rb +1 -1
  58. metadata +24 -1
@@ -16,3 +16,17 @@ require "badline/frontend/pad_port"
16
16
  require "badline/frontend/gamepads"
17
17
  require "badline/frontend/app"
18
18
  require "badline/frontend/audio_sink"
19
+ require "badline/frontend/painter"
20
+ require "badline/frontend/sid_history"
21
+ require "badline/frontend/sid_view"
22
+ require "badline/frontend/filter_view"
23
+ require "badline/frontend/scope"
24
+ require "badline/frontend/visualizer_view"
25
+ require "badline/frontend/buttons"
26
+ require "badline/frontend/player_state"
27
+ require "badline/frontend/player_header"
28
+ require "badline/frontend/player_footer"
29
+ require "badline/frontend/info_view"
30
+ require "badline/frontend/player_commands"
31
+ require "badline/frontend/player_screen"
32
+ require "badline/frontend/player_window"
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # An ordered list of media files to go through one at a time, such as
6
+ # the tunes of a player. Each entry is a file and the part of it to
7
+ # start on, counting from 1, out of the parts the file holds: a .sid
8
+ # tune's subtunes, say, or 1 of 1 for media with no parts.
9
+ #
10
+ # #next_entry and #previous_entry step through the entries, and
11
+ # #next_part and #previous_part through the current entry's parts,
12
+ # stopping at its ends. Each returns the entry it moved to, or nil
13
+ # where it stays put. With #loop? the entry steps wrap around at
14
+ # either end, and with #shuffle? they follow a shuffled order that
15
+ # starts from the current entry. #all_parts? decides where #advance
16
+ # goes once a part has played out: on through the entry's parts, or
17
+ # to the next entry. An entry stepped to starts on its first part
18
+ # with #all_parts?, otherwise on its own.
19
+ #
20
+ # `shuffle`, if given, returns the entry indices it's given in a new
21
+ # order in place of Array#shuffle. `build`, if given, turns the paths
22
+ # #add is given into entries.
23
+ class Queue
24
+ # A file and the part of it to start on.
25
+ class Entry
26
+ attr_reader :path, :part, :parts
27
+
28
+ def initialize(path, part: 1, parts: 1)
29
+ @path = path
30
+ @part = part
31
+ @parts = parts
32
+ end
33
+ end
34
+
35
+ attr_reader :part
36
+
37
+ # The files `paths` name, in order: a file as given, and a directory
38
+ # as every file below it with the extension, in path order.
39
+ def self.files(paths, extension)
40
+ paths.flat_map { |path| File.directory?(path) ? below(path, extension).sort : [path] }
41
+ end
42
+
43
+ def self.below(directory, extension)
44
+ paths = []
45
+ Dir.children(directory).each do |name|
46
+ path = File.join(directory, name)
47
+ if File.directory?(path)
48
+ paths.concat(below(path, extension))
49
+ elsif File.extname(name).casecmp?(extension)
50
+ paths << path
51
+ end
52
+ end
53
+ paths
54
+ end
55
+ private_class_method :below
56
+
57
+ def initialize(entries, all_parts: false, shuffle: nil, build: nil)
58
+ @entries = entries
59
+ @shuffler = shuffle
60
+ @builder = build
61
+ @order = (0...entries.size).to_a
62
+ @position = 0
63
+ @part = entries.empty? ? 0 : entries.first.part
64
+ @all_parts = all_parts
65
+ @shuffle = false
66
+ @loop = false
67
+ end
68
+
69
+ def size = @entries.size
70
+
71
+ def empty? = @entries.empty?
72
+
73
+ # Where the current entry stands in the order played, from 1.
74
+ def position = empty? ? 0 : @position + 1
75
+
76
+ def entry = empty? ? nil : @entries[@order[@position]]
77
+
78
+ def all_parts? = @all_parts
79
+
80
+ def shuffle? = @shuffle
81
+
82
+ def loop? = @loop
83
+
84
+ # Puts the entries `build` makes of the paths at the end of the order,
85
+ # shuffled or not. An empty queue starts on the first of them.
86
+ def add(paths)
87
+ return if @builder.nil?
88
+
89
+ started = empty?
90
+ first = @entries.size
91
+ @entries.concat(@builder.call(paths))
92
+ @order.concat((first...@entries.size).to_a)
93
+ @part = entry.part if started && !empty?
94
+ end
95
+
96
+ def toggle_all_parts = @all_parts = !@all_parts
97
+
98
+ def toggle_loop = @loop = !@loop
99
+
100
+ def toggle_shuffle
101
+ return @shuffle = !@shuffle if empty?
102
+
103
+ current = @order[@position]
104
+ @shuffle = !@shuffle
105
+ @order = (0...size).to_a
106
+ @order = [current] + shuffled(@order - [current]) if @shuffle
107
+ @position = @order.index(current)
108
+ @shuffle
109
+ end
110
+
111
+ def next_entry
112
+ return if empty?
113
+
114
+ move(@position + 1)
115
+ end
116
+
117
+ def previous_entry
118
+ return if empty?
119
+
120
+ move(@position - 1)
121
+ end
122
+
123
+ def next_part
124
+ return if empty? || @part >= entry.parts
125
+
126
+ @part += 1
127
+ entry
128
+ end
129
+
130
+ def previous_part
131
+ return if empty? || @part <= 1
132
+
133
+ @part -= 1
134
+ entry
135
+ end
136
+
137
+ # Where to go once the current part has played to its end: the
138
+ # entry's next part with #all_parts?, otherwise the next entry.
139
+ def advance
140
+ return next_part if @all_parts && !empty? && @part < entry.parts
141
+
142
+ next_entry
143
+ end
144
+
145
+ private
146
+
147
+ def move(position)
148
+ return unless position.between?(0, size - 1) || @loop
149
+
150
+ @position = position % size
151
+ @part = @all_parts ? 1 : entry.part
152
+ entry
153
+ end
154
+
155
+ def shuffled(indices) = @shuffler ? @shuffler.call(indices) : indices.shuffle
156
+ end
157
+ end
158
+ end
data/lib/badline/media.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/media/true_drive"
4
+ require "badline/media/queue"
4
5
 
5
6
  module Badline
6
7
  module Media
@@ -25,7 +26,7 @@ module Badline
25
26
  # A .g64 plugs in a true drive as device 8 (TrueDrive), and with one
26
27
  # there, a .d64 goes into it instead of the KERNAL traps too. The
27
28
  # autostart then loads through it.
28
- def attach(computer, path, autostart: true, song: nil, **options)
29
+ def attach(computer, path, autostart: true, subtune: nil, **options)
29
30
  if TrueDrive.takes?(computer, path)
30
31
  attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
31
32
  elsif File.directory?(path)
@@ -34,7 +35,7 @@ module Badline
34
35
  elsif File.extname(path).downcase == ".crt"
35
36
  attach_cartridge(computer, path, options.fetch(:cartridge, {}))
36
37
  elsif File.extname(path).downcase == ".sid"
37
- attach_sid(computer, path, autostart:, song:)
38
+ attach_sid(computer, path, autostart:, subtune:)
38
39
  elsif File.extname(path).downcase == ".tap"
39
40
  attach_tape(computer, path, autostart:)
40
41
  elsif MOUNT_TYPES.key?(File.extname(path).downcase)
@@ -91,18 +92,22 @@ module Badline
91
92
  "Attached cartridge #{path}"
92
93
  end
93
94
 
94
- def attach_sid(computer, path, autostart:, song:)
95
+ def attach_sid(computer, path, autostart:, subtune:)
95
96
  tune = Storage::SIDFile.new(path)
96
- song = (song || tune.start_song).clamp(1, tune.songs)
97
- computer.on_init { start_tune(computer, tune, autostart:, song:) }
97
+ if tune.sids > 1
98
+ raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
99
+ end
100
+
101
+ subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
102
+ computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
98
103
  title = tune.name.empty? ? path : tune.name
99
- title += " (song #{song})" if tune.songs > 1
104
+ title += " (subtune #{subtune})" if tune.subtunes > 1
100
105
  autostart ? "Playing #{title}" : "Loaded #{title}"
101
106
  end
102
107
 
103
- def start_tune(computer, tune, autostart:, song:)
108
+ def start_tune(computer, tune, autostart:, subtune:)
104
109
  computer.ram.write(tune.load_address, tune.data)
105
- tune.boot_memory(song:).each { |address, bytes| computer.ram.write(address, bytes) }
110
+ tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
106
111
  computer.type_text(tune.boot_command) if autostart
107
112
  end
108
113
 
@@ -19,7 +19,7 @@ module Badline
19
19
  # What --help prints: the usage, then the options this build takes,
20
20
  # section by section, laid out as OptionParser did.
21
21
  def help
22
- lines = [banner]
22
+ lines = [@sid_command ? sid_banner : banner]
23
23
  SECTIONS.each do |section|
24
24
  options = TABLE.select { |option| option.section == section && takes?(option) }
25
25
  next if options.empty?
@@ -40,6 +40,7 @@ module Badline
40
40
  Usage: #{@program} [options] [media]
41
41
  #{@program} --headless [options] tune.sid
42
42
  #{@program} [options] tune.sid --audio-out FILE
43
+ #{@program} sid [options] FILE|DIR...
43
44
 
44
45
  Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
45
46
  .g64 disk image for the true 1541, a .t64 tape archive, a .tap
@@ -48,18 +49,23 @@ module Badline
48
49
  saves a snapshot and F12 restores it. Device 8 answers through
49
50
  traps on the KERNAL's disk routines, unless --true-drive puts an
50
51
  emulated 1541 there, which runs its own DOS and reads .d64 and .g64
51
- images only.
52
+ images only. A .sid tune opens in the SID player, as `sid` plays it,
53
+ unless an option of the emulator's window asks for the machine.
52
54
 
53
55
  --headless plays a .sid tune on the host's audio device without the
54
56
  window, and --audio-out renders it to 16-bit PCM instead. The
55
57
  container follows the file's extension, .wav or .aiff. Played on a
56
- terminal, n or → skips to the next song, p or ← to the previous one,
57
- space pauses and q quits.
58
+ terminal, → and ← step between the tune's subtunes, n and p between
59
+ tunes, , and . seek, space pauses and q quits. s shuffles the tunes,
60
+ l loops them, and a, or --all-subtunes, plays on through each tune's
61
+ subtunes instead of just the one. `#{@program} sid` plays many tunes, and whole
62
+ directories of them, the same way.
58
63
 
59
64
  A .sid file carries no length of its own. Without --seconds the tune is
60
65
  looked up by MD5 in HVSC's Songlengths.md5, taken from --songlengths, from
61
66
  the DOCUMENTS directory of an HVSC collection above the tune, or from
62
- $HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds.
67
+ $HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds, or
68
+ until it has been silent for #{SILENCE_SECONDS.to_i}.
63
69
 
64
70
  #{@native ? NATIVE_SPEED : RUBY_SPEED}
65
71
 
@@ -74,5 +80,28 @@ module Badline
74
80
  argument.
75
81
  BANNER
76
82
  end
83
+
84
+ def sid_banner
85
+ <<~BANNER
86
+ Usage: #{@program} sid [options] FILE|DIR...
87
+
88
+ Plays .sid tunes in the SID player's window, or in the terminal with
89
+ --headless, one after another in the order given. A directory adds
90
+ every .sid tune below it, in path order. → and ← step between a
91
+ tune's subtunes, n and p between tunes, , and . seek 10 seconds back
92
+ and forward, space pauses and q quits. s shuffles the tunes, l loops
93
+ them, and a, or --all-subtunes, plays on through each tune's subtunes
94
+ instead of just the one. The window has buttons for all of these,
95
+ and for switching the SID between the tune's own, the 6581 and the
96
+ 8580. --subtune picks the first tune's subtune, and each tune after
97
+ it starts on its own. Without tunes, the window opens with an empty
98
+ queue, and .sid files or folders dropped on the window join it.
99
+
100
+ Each tune gets the SID its header names unless --sid says otherwise,
101
+ and its length from HVSC's Songlengths.md5 as #{@program} --help
102
+ describes. A tune written for 2 or 3 SIDs plays on the one badline
103
+ emulates, with a notice.
104
+ BANNER
105
+ end
77
106
  end
78
107
  end
@@ -3,7 +3,7 @@
3
3
  module Badline
4
4
  class Options
5
5
  # One option of the table: its flags and value as the help shows them
6
- # (`-s, --song N`), what it does, the help section it's listed in, the
6
+ # (`-s, --subtune N`), what it does, the help section it's listed in, the
7
7
  # mode it needs (:window, :headless or :either), and the build that
8
8
  # takes it (:native, :ruby or :both).
9
9
  class Option
@@ -8,8 +8,8 @@ module Badline
8
8
 
9
9
  # Every option of both builds, in the order the help lists them.
10
10
  TABLE = [
11
- Option.new("-s, --song N", "Subtune of a .sid, from 1 (default: the tune's own)", section: "Options"),
12
- Option.new("--sid MODEL", "SID to fit: 6581 or 8580 (default: a .sid tune's own, else 6581)",
11
+ Option.new("-s, --subtune N", "Subtune of a .sid, from 1 (default: the tune's own)", section: "Options"),
12
+ Option.new("--sid MODEL", "SID to fit: auto, 6581 or 8580 (default: auto, a .sid tune's own, else 6581)",
13
13
  section: "Options"),
14
14
  Option.new("--disable-jit", "Run without enabling YJIT", section: "Options", build: :ruby),
15
15
  Option.new("-h, --help", "Show this help", section: "Options"),
@@ -44,6 +44,8 @@ module Badline
44
44
  Option.new("--filter-chunk N", "Filter step in cycles, 1 is exact (default: 4)",
45
45
  section: "Options without the window", needs: :headless),
46
46
  Option.new("--quiet", "Don't report progress", section: "Options without the window", needs: :headless),
47
+ Option.new("--all-subtunes", "Play on through each tune's subtunes, not just the one",
48
+ section: "Options without the window", needs: :headless),
47
49
  Option.new("--no-tui", "Play without the interactive display, even on a terminal",
48
50
  section: "Options without the window", needs: :headless),
49
51
  Option.new("--frames N", "Quit after N frames", section: "Testing options", needs: :window),
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Options
5
+ # The checks Options#parse makes once every argument has been read:
6
+ # the options fit the mode, the numbers are positive and the files
7
+ # exist.
8
+ module Validation
9
+ private
10
+
11
+ def validate
12
+ validate_mode
13
+ validate_numbers
14
+ validate_paths
15
+ end
16
+
17
+ def validate_mode
18
+ if headless?
19
+ raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
20
+ raise Error, "no tune given" if tune_paths.empty? && !player_window?
21
+
22
+ tune_paths.each { |path| validate_tune(path) }
23
+ elsif !@headless_only.empty?
24
+ raise Error, "#{@headless_only.first} needs --headless or --audio-out"
25
+ end
26
+ end
27
+
28
+ # `sid` takes directories as well as tunes.
29
+ def validate_tune(path)
30
+ return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
31
+ return if @sid_command && File.directory?(path)
32
+
33
+ raise Error, "not a .sid tune: #{path}"
34
+ end
35
+
36
+ def validate_numbers
37
+ positive("--subtune", @subtune)
38
+ positive("--seconds", @seconds)
39
+ positive("--rate", @rate)
40
+ positive("--filter-chunk", @filter_chunk)
41
+ end
42
+
43
+ def positive(name, value)
44
+ raise Error, "invalid argument: #{name} #{value}" if !value.nil? && value <= 0
45
+ end
46
+
47
+ def validate_paths
48
+ missing = tune_paths.find { |path| !File.exist?(path) }
49
+ raise Error, "no such file or directory: #{missing}" unless missing.nil?
50
+ raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -4,33 +4,47 @@ require "badline/options/option"
4
4
  require "badline/options/event"
5
5
  require "badline/options/table"
6
6
  require "badline/options/help"
7
+ require "badline/options/validation"
7
8
 
8
9
  module Badline
9
10
  # The command line of both builds, badline-ruby and the native badline.
10
11
  # Either opens the window for any media, or with --headless or
11
- # --audio-out plays or renders a .sid tune without one. The native build
12
- # adds --no-sound and --version, and badline-ruby adds --disable-jit.
12
+ # --audio-out plays or renders a .sid tune without one. `sid`, given
13
+ # first, plays .sid tunes and directories of them in the terminal. The
14
+ # native build adds --no-sound and --version, and badline-ruby adds
15
+ # --disable-jit.
13
16
  #
14
17
  # TABLE lists the options, and both the parser and the help read it. It
15
18
  # parses by hand, inside the subset of Ruby Spinel compiles. Values come
16
- # as `--song 2` or `--song=2`, and `--` ends the options. It loads
19
+ # as `--subtune 2` or `--subtune=2`, and `--` ends the options. It loads
17
20
  # nothing else from badline, so badline-ruby can parse and reject its
18
21
  # arguments before paying for the emulator.
19
22
  class Options
23
+ include Validation
24
+
20
25
  class Error < StandardError; end
21
26
 
22
27
  FALLBACK_SECONDS = 60.0
23
28
 
29
+ # A tune on the fallback length ends sooner once it has been silent
30
+ # this long.
31
+ SILENCE_SECONDS = 5.0
32
+
24
33
  SID_MODELS = { "6581" => :mos6581, "8580" => :mos8580 }.freeze
25
34
 
35
+ # What `sid` leaves out besides the window's options.
36
+ NOT_FOR_SID = %w[--audio-out].freeze
37
+
26
38
  REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
27
39
 
28
40
  # A Float as OptionParser took one: digits with an optional sign,
29
41
  # fraction and exponent.
30
42
  DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
31
43
 
32
- attr_reader :program, :media_path, :song, :sid_model, :reu, :frames, :screenshot, :save_snapshot, :audio_out,
33
- :seconds, :songlengths, :filter_chunk, :timeline
44
+ # tune_paths holds the media given, which `sid` takes more than one of:
45
+ # tunes and directories of tunes, in the order given.
46
+ attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
47
+ :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
34
48
 
35
49
  def self.parse(argv, native: false) = new(native:).parse(argv)
36
50
 
@@ -41,7 +55,9 @@ module Badline
41
55
  @native = native
42
56
  @program = native ? "badline" : "badline-ruby"
43
57
  @media_path = nil
44
- @song = nil
58
+ @sid_command = false
59
+ @tune_paths = []
60
+ @subtune = nil
45
61
  @sid_model = nil
46
62
  @reu = nil
47
63
  @autostart = true
@@ -67,13 +83,16 @@ module Badline
67
83
  @filter_chunk = nil
68
84
  @quiet = false
69
85
  @tui = true
86
+ @all_subtunes = false
70
87
  @window_only = []
71
88
  @headless_only = []
72
89
  end
73
90
 
74
91
  def parse(argv)
75
92
  args = argv.dup
93
+ sid_command(args) if args.first == "sid"
76
94
  argument(args.shift, args) until args.empty?
95
+ play_lone_tune
77
96
  validate unless help? || version?
78
97
  self
79
98
  end
@@ -82,7 +101,14 @@ module Badline
82
101
  def snapshot? = !@media_path.nil? && File.extname(@media_path).casecmp?(".vsf")
83
102
 
84
103
  # Plays or renders a .sid tune without the window.
85
- def headless? = @headless || render?
104
+ def headless? = @headless || render? || @sid_command
105
+
106
+ # Whether `sid` came first, to play the tunes and directories given.
107
+ def sid_command? = @sid_command
108
+
109
+ # Whether `sid` plays in the SID player's window rather than the
110
+ # terminal.
111
+ def player_window? = @sid_command && !@headless
86
112
 
87
113
  def window? = !headless?
88
114
 
@@ -94,6 +120,9 @@ module Badline
94
120
  # How long to play a tune whose length nothing gives.
95
121
  def fallback_seconds = FALLBACK_SECONDS
96
122
 
123
+ # How long a tune playing for fallback_seconds may stay silent.
124
+ def silence_seconds = SILENCE_SECONDS
125
+
97
126
  def rate = @rate.nil? ? DEFAULT_RATE : @rate
98
127
 
99
128
  def rate_given? = !@rate.nil?
@@ -102,6 +131,8 @@ module Badline
102
131
 
103
132
  def tui? = @tui
104
133
 
134
+ def all_subtunes? = @all_subtunes
135
+
105
136
  def autostart? = @autostart
106
137
 
107
138
  def read_only? = @read_only
@@ -126,7 +157,24 @@ module Badline
126
157
 
127
158
  private
128
159
 
129
- def takes?(option) = [:both, @native ? :native : :ruby].include?(option.build)
160
+ def takes?(option)
161
+ return false if @sid_command && (option.needs == :window || NOT_FOR_SID.include?(option.name))
162
+
163
+ [:both, @native ? :native : :ruby].include?(option.build)
164
+ end
165
+
166
+ def sid_command(args)
167
+ args.shift
168
+ @sid_command = true
169
+ end
170
+
171
+ # A .sid on its own plays in the SID player, as `sid` would, unless an
172
+ # option of the emulator's window asks for the machine.
173
+ def play_lone_tune
174
+ return if @sid_command || headless? || !@window_only.empty? || @media_path.nil?
175
+
176
+ @sid_command = File.extname(@media_path).casecmp?(".sid")
177
+ end
130
178
 
131
179
  def argument(arg, args)
132
180
  if arg == "--"
@@ -172,7 +220,7 @@ module Badline
172
220
 
173
221
  def valued_option(name, flag, value)
174
222
  case name
175
- when "--song" then @song = number(flag, value)
223
+ when "--subtune" then @subtune = number(flag, value)
176
224
  when "--sid" then @sid_model = sid_model_for(value)
177
225
  when "--reu" then @reu = reu_size(value)
178
226
  when "--frames" then @frames = number(flag, value)
@@ -203,11 +251,13 @@ module Badline
203
251
  when "--version" then @version = true
204
252
  when "--headless" then @headless = true
205
253
  when "--quiet" then @quiet = true
254
+ when "--all-subtunes" then @all_subtunes = true
206
255
  else @tui = false
207
256
  end
208
257
  end
209
258
 
210
259
  def sid_model_for(value)
260
+ return if value == "auto"
211
261
  raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
212
262
 
213
263
  SID_MODELS[value]
@@ -232,41 +282,10 @@ module Badline
232
282
  end
233
283
 
234
284
  def media_argument(arg)
235
- raise Error, "unexpected argument: #{arg}" unless @media_path.nil?
236
-
237
- @media_path = arg
238
- end
239
-
240
- def validate
241
- validate_mode
242
- validate_numbers
243
- validate_paths
244
- end
245
-
246
- def validate_mode
247
- if headless?
248
- raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
249
- raise Error, "no tune given" if @media_path.nil?
250
- raise Error, "not a .sid tune: #{@media_path}" unless File.extname(@media_path).casecmp?(".sid")
251
- elsif !@headless_only.empty?
252
- raise Error, "#{@headless_only.first} needs --headless or --audio-out"
253
- end
254
- end
255
-
256
- def validate_numbers
257
- positive("--song", @song)
258
- positive("--seconds", @seconds)
259
- positive("--rate", @rate)
260
- positive("--filter-chunk", @filter_chunk)
261
- end
262
-
263
- def positive(name, value)
264
- raise Error, "invalid argument: #{name} #{value}" if !value.nil? && value <= 0
265
- end
285
+ raise Error, "unexpected argument: #{arg}" unless @sid_command || @media_path.nil?
266
286
 
267
- def validate_paths
268
- raise Error, "no such file or directory: #{@media_path}" unless @media_path.nil? || File.exist?(@media_path)
269
- raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
287
+ @media_path = arg if @media_path.nil?
288
+ @tune_paths << arg
270
289
  end
271
290
  end
272
291
  end
data/lib/badline/sdl.rb CHANGED
@@ -15,6 +15,7 @@ module Badline
15
15
  ffi_func :SDL_SetHint, %i[str str], :int
16
16
  ffi_func :SDL_CreateWindow, %i[str int int int int uint32], :ptr
17
17
  ffi_func :SDL_SetWindowTitle, %i[ptr str], :void
18
+ ffi_func :SDL_SetWindowSize, %i[ptr int int], :void
18
19
  ffi_func :SDL_DestroyWindow, [:ptr], :void
19
20
  ffi_func :SDL_CreateRenderer, %i[ptr int uint32], :ptr
20
21
  ffi_func :SDL_GetWindowDisplayMode, %i[ptr ptr], :int
@@ -26,6 +27,9 @@ module Badline
26
27
  ffi_func :SDL_RenderClear, [:ptr], :int
27
28
  ffi_func :SDL_SetRenderDrawColor, %i[ptr int int int int], :int
28
29
  ffi_func :SDL_RenderFillRect, %i[ptr ptr], :int
30
+ ffi_func :SDL_RenderDrawLines, %i[ptr int_array int], :int
31
+ ffi_func :SDL_SetTextureBlendMode, %i[ptr int], :int
32
+ ffi_func :SDL_SetTextureColorMod, %i[ptr int int int], :int
29
33
  ffi_func :SDL_RenderCopy, %i[ptr ptr ptr ptr], :int
30
34
  ffi_func :SDL_RenderPresent, [:ptr], :void
31
35
  ffi_func :SDL_PollEvent, [:ptr], :int
@@ -52,9 +56,6 @@ module Badline
52
56
  ffi_func :SDL_GameControllerGetButton, %i[ptr int], :uint8
53
57
  ffi_func :SDL_GameControllerGetAxis, %i[ptr int], :int16
54
58
  ffi_func :SDL_ClearQueuedAudio, [:uint32], :void
55
- ffi_func :SDL_GetCurrentDisplayMode, %i[int ptr], :int
56
- ffi_func :SDL_CreateRGBSurfaceFrom, %i[ptr int int int int uint32 uint32 uint32 uint32], :ptr
57
- ffi_func :SDL_CreateTextureFromSurface, %i[ptr ptr], :ptr
58
59
  ffi_func :SDL_PushEvent, [:ptr], :int
59
60
  ffi_func :SDL_GetKeyName, [:int], :str
60
61
 
@@ -69,6 +70,11 @@ module Badline
69
70
  ffi_read_i32 :event_xrel, 28
70
71
  ffi_read_i32 :event_yrel, 32
71
72
  ffi_read_u8 :event_button, 16
73
+ # Where the pointer is, in a motion or button event.
74
+ ffi_read_i32 :event_x, 20
75
+ ffi_read_i32 :event_y, 24
76
+ # SDL_DropEvent's file, which the receiver frees.
77
+ ffi_read_ptr :event_file, 8
72
78
 
73
79
  ffi_buffer :rect, 16
74
80
  ffi_write_i32 :rect_x, 0
@@ -77,6 +83,8 @@ module Badline
77
83
  ffi_write_i32 :rect_h, 12
78
84
  # Where the drive LED goes.
79
85
  ffi_buffer :led_rect, 16
86
+ # A glyph's place in the SID player's font, drawn at SDL.rect.
87
+ ffi_buffer :glyph_rect, 16
80
88
 
81
89
  # SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
82
90
  ffi_buffer :display_mode, 24
@@ -104,6 +112,8 @@ module Badline
104
112
  ffi_const :RENDERER_ACCELERATED, 0x02
105
113
  ffi_const :RENDERER_PRESENTVSYNC, 0x04
106
114
  ffi_const :PIXELFORMAT_RGB888, 0x16161804
115
+ ffi_const :PIXELFORMAT_ARGB8888, 0x16362004
116
+ ffi_const :BLENDMODE_BLEND, 0x01
107
117
  ffi_const :TEXTUREACCESS_STREAMING, 1
108
118
  ffi_const :QUIT, 0x100
109
119
  ffi_const :KEYDOWN, 0x300
@@ -114,13 +124,10 @@ module Badline
114
124
  ffi_const :KMOD_SHIFT, 0x0003
115
125
  ffi_const :CONTROLLERDEVICEADDED, 0x653
116
126
  ffi_const :CONTROLLERDEVICEREMOVED, 0x654
117
- ffi_const :CONTROLLERDEVICEREMAPPED, 0x655
118
127
  ffi_const :AUDIO_S16LSB, 0x8010
119
128
  ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
120
129
  ffi_const :KEY_TAB, 0x09
121
130
  ffi_const :KEY_F10, 0x4000_0043
122
- ffi_const :KEY_F11, 0x4000_0044
123
- ffi_const :KEY_F12, 0x4000_0045
124
131
  end
125
132
 
126
133
  # The few C library calls the native front end makes.
@@ -128,6 +135,9 @@ module Badline
128
135
  ffi_func :malloc, [:size_t], :ptr
129
136
  ffi_func :free, [:ptr], :void
130
137
  ffi_func :poll, %i[ptr size_t int], :int
138
+ # Called with an empty needle, it returns the C string it's given as a
139
+ # String.
140
+ ffi_func :strstr, %i[ptr str], :str
131
141
 
132
142
  ffi_const :POLLIN, 0x01
133
143