sidtool 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -14
- data/bin/sidtool +10 -7
- data/lib/sidtool.rb +2 -0
- data/lib/sidtool/midi_file_writer.rb +198 -0
- data/lib/sidtool/ruby_file_writer.rb +17 -0
- data/lib/sidtool/sid.rb +4 -1
- data/lib/sidtool/state.rb +0 -2
- data/lib/sidtool/synth.rb +11 -5
- data/lib/sidtool/version.rb +1 -1
- data/lib/sidtool/voice.rb +17 -15
- data/sidtool.gemspec +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9827680bd9ec8f3babcce7dae70a7dfe3c17c4b2fa851a87035b6a8393a8661
|
4
|
+
data.tar.gz: f242e65a3926abce13400f3ae1ab38a2ab40d5416827a702429ded7f3de2f171
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2747811196f61ef4492ffab664a1ce68c716274d8ed9d74dd4868d4f07d78f04f4604aa50a376830d6daf4d18dde403ea3376947f24002e159acb72fd7343bc5
|
7
|
+
data.tar.gz: b473d4c29a792bfe39cf7e17e8f425b60544e78b38a6487b60d793132112fd06e2a9905fca19ebf93d6394c03665410b90e3e7625269176cabcb74e274b6acbe
|
data/README.md
CHANGED
@@ -1,24 +1,36 @@
|
|
1
1
|
# Sidtool
|
2
2
|
|
3
|
-
Convert Commodore 64 SID music in the form of `.sid` files into other formats!
|
4
|
-
much work in progress... the code is ugly and will probably create horrible results for you :-)
|
3
|
+
Convert Commodore 64 SID music in the form of `.sid` files into other formats!
|
5
4
|
|
6
|
-
Basically, it's a massive hack made for fun and no profit.
|
7
|
-
|
8
|
-
The vision, though, is to extract the actual information from `.sid` files, which are files storing
|
9
|
-
music for the Commodore 64. This sounds like an easy task.
|
5
|
+
Basically, it's a massive hack made for fun and no profit. The vision, though, is to extract the
|
6
|
+
actual information from `.sid` files, which are files storing music for the Commodore 64.
|
10
7
|
|
11
8
|
`.sid` files contain actual Commodore 64 machine code that writes to registers corresponding to the
|
12
9
|
Commodore 64 sound chip, the SID (Sound Interface Device). Which means that in order to play back a
|
13
10
|
`.sid` file like it would sound on an actual Commodore 64, you will have to simulate both the
|
14
11
|
processor and the sound chip.
|
15
12
|
|
16
|
-
This project does not attempt to
|
17
|
-
players already exist - but instead
|
18
|
-
|
13
|
+
This project does not attempt to produce an authentic playback of the sounds - lots of those
|
14
|
+
players already exist - but instead lets you export a Commodore 64 song into a format that lets
|
15
|
+
you edit and experiment with the song. Want to change the instruments? Go ahead. Want to take out
|
16
|
+
parts of the song and use in other projects? You can do that. Want to just listen to your favourite
|
17
|
+
Commodore 64 song played back by a piano? Definitely do that!
|
18
|
+
|
19
|
+
## Supported Output Formats
|
20
|
+
|
21
|
+
### Ruby
|
22
|
+
|
23
|
+
You can get a simple Ruby file which defines a list of synths to play at certain points in time.
|
24
|
+
This can be used to play back the music in [Sonic Pi](https://sonic-pi.net) (see below), or you
|
25
|
+
can write a Ruby script to do your own post-processing.
|
19
26
|
|
20
|
-
|
21
|
-
|
27
|
+
### Midi
|
28
|
+
|
29
|
+
If you just want to listen to a `.sid` file, the easiest way is to export to midi file format and
|
30
|
+
open the file in a player such as [VLC](https://www.videolan.org/vlc/index.html). However, if you
|
31
|
+
want to further edit the result, import the file in a music editor such as GarageBand on a Mac.
|
32
|
+
Then you can use all of the tools provided by your music editor to change instruments and rearrange
|
33
|
+
the song.
|
22
34
|
|
23
35
|
## Limitations
|
24
36
|
|
@@ -46,16 +58,20 @@ Show information, like the author and number of songs in a file:
|
|
46
58
|
|
47
59
|
$ sidtool --info <input file>
|
48
60
|
|
49
|
-
Convert the default song from a file to a
|
61
|
+
Convert the default song from a `.sid` file to a midi file:
|
62
|
+
|
63
|
+
$ sidtool --out <output file> --format midi <input file>
|
64
|
+
|
65
|
+
Convert the default song from a file to a Ruby list (`--format ruby` is the default):
|
50
66
|
|
51
67
|
$ sidtool --out <output file> <input file>
|
52
68
|
|
53
|
-
The output can then be used to play back the music, for example in Sonic Pi:
|
69
|
+
The Ruby output can then be used to play back the music, for example in Sonic Pi:
|
54
70
|
|
55
71
|
```ruby
|
56
72
|
load '<path to your output file from before>'
|
57
73
|
|
58
|
-
previous_frame =
|
74
|
+
previous_frame = 0
|
59
75
|
::SYNTHS.each do |synth|
|
60
76
|
current_frame = synth[0]
|
61
77
|
frames_to_sleep = current_frame - previous_frame
|
data/bin/sidtool
CHANGED
@@ -4,12 +4,17 @@ require 'mos6510'
|
|
4
4
|
require 'optparse'
|
5
5
|
|
6
6
|
DEFAULT_FRAMES_TO_PROCESS = 15000
|
7
|
+
EXPORTERS = {
|
8
|
+
'ruby' => Sidtool::RubyFileWriter,
|
9
|
+
'midi' => Sidtool::MidiFileWriter
|
10
|
+
}
|
7
11
|
|
8
12
|
params = {}
|
9
13
|
OptionParser.new do |parser|
|
10
14
|
parser.banner = 'Usage: sidtool [options] <intputfile.sid>'
|
11
15
|
|
12
16
|
parser.on('-i', '--info', 'Show file information')
|
17
|
+
parser.on('--format FORMAT', 'Output format, "ruby" (default) or "midi"')
|
13
18
|
parser.on('-o', '--out FILENAME', 'Output file (Ruby array)')
|
14
19
|
parser.on('-s', '--song NUMBER', Integer, 'Song number to process (defaults to the start song in the file)')
|
15
20
|
parser.on('-f', '--frames NUMBER', Integer, "Number of frames to process (default #{DEFAULT_FRAMES_TO_PROCESS})")
|
@@ -32,6 +37,10 @@ output_file = params[:out]
|
|
32
37
|
show_info = !!params[:info]
|
33
38
|
raise 'Either provide -i or -o, or I have nothing to do!' unless output_file || show_info
|
34
39
|
|
40
|
+
format = params[:format] || EXPORTERS.keys.first
|
41
|
+
exporter_class = EXPORTERS[format]
|
42
|
+
raise "Invalid format: #{format}. Valid formats: #{EXPORTERS.keys.join(', ')}" unless exporter_class
|
43
|
+
|
35
44
|
song = params[:song] || sid_file.start_song
|
36
45
|
raise 'Song must be at least 1' if song < 1
|
37
46
|
raise "File only has #{sid_file.songs} songs" if song > sid_file.songs
|
@@ -74,11 +83,5 @@ if output_file
|
|
74
83
|
|
75
84
|
STDERR.puts("Processed #{frames} frames")
|
76
85
|
|
77
|
-
|
78
|
-
file.puts '::SYNTHS = ['
|
79
|
-
Sidtool::STATE.synths.each do |synth|
|
80
|
-
file.puts synth.to_a.inspect + ','
|
81
|
-
end
|
82
|
-
file.puts ']'
|
83
|
-
end
|
86
|
+
exporter_class.new(sid.synths_for_voices).write_to(output_file)
|
84
87
|
end
|
data/lib/sidtool.rb
CHANGED
@@ -0,0 +1,198 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class MidiFileWriter
|
3
|
+
DeltaTime = Struct.new(:time) do
|
4
|
+
def bytes
|
5
|
+
quantity = time
|
6
|
+
seven_bit_segments = []
|
7
|
+
while true
|
8
|
+
seven_bit_segments << (quantity & 127)
|
9
|
+
quantity = quantity >> 7
|
10
|
+
break if quantity == 0
|
11
|
+
end
|
12
|
+
result = seven_bit_segments.reverse.map { |segment| segment | 128 }
|
13
|
+
result[-1] &= 127
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
TrackName = Struct.new(:name) do
|
19
|
+
def bytes
|
20
|
+
[
|
21
|
+
0xFF, 0x03,
|
22
|
+
name.length,
|
23
|
+
*name.bytes
|
24
|
+
]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
TimeSignature = Struct.new(:numerator, :denominator_power_of_two, :clocks_per_metronome_click, :number_of_32th_nodes_per_24_clocks) do
|
29
|
+
def bytes
|
30
|
+
[
|
31
|
+
0xFF, 0x58, 0x04,
|
32
|
+
numerator,
|
33
|
+
denominator_power_of_two,
|
34
|
+
clocks_per_metronome_click,
|
35
|
+
number_of_32th_nodes_per_24_clocks
|
36
|
+
]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
KeySignature = Struct.new(:sharps_or_flats, :is_major) do
|
41
|
+
def bytes
|
42
|
+
[
|
43
|
+
0xFF, 0x59, 0x02,
|
44
|
+
sharps_or_flats,
|
45
|
+
is_major ? 0 : 1
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
EndOfTrack = Struct.new(:nothing) do
|
51
|
+
def bytes
|
52
|
+
[
|
53
|
+
0xFF, 0x2F, 0x00
|
54
|
+
]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
ProgramChange = Struct.new(:channel, :program_number) do
|
59
|
+
def bytes
|
60
|
+
raise "Channel too big: #{channel}" if channel > 15
|
61
|
+
raise "Program number is too big: #{program_number}" if program_number > 255
|
62
|
+
[
|
63
|
+
0xC0 + channel,
|
64
|
+
program_number
|
65
|
+
]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
NoteOn = Struct.new(:channel, :key) do
|
70
|
+
def bytes
|
71
|
+
raise "Channel too big: #{channel}" if channel > 15
|
72
|
+
raise "Key is too big: #{key}" if key > 255
|
73
|
+
[
|
74
|
+
0x90 + channel,
|
75
|
+
key,
|
76
|
+
40 # Default velocity
|
77
|
+
]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
NoteOff = Struct.new(:channel, :key) do
|
82
|
+
def bytes
|
83
|
+
raise "Channel too big: #{channel}" if channel > 15
|
84
|
+
raise "Key is too big: #{key}" if key > 255
|
85
|
+
[
|
86
|
+
0x80 + channel,
|
87
|
+
key,
|
88
|
+
40 # Default velocity
|
89
|
+
]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize(synths_for_voices)
|
94
|
+
@synths_for_voices = synths_for_voices
|
95
|
+
end
|
96
|
+
|
97
|
+
def write_to(path)
|
98
|
+
tracks = @synths_for_voices.map { |synths| build_track(synths) }
|
99
|
+
|
100
|
+
File.open(path, 'wb') do |file|
|
101
|
+
write_header(file)
|
102
|
+
tracks.each_with_index { |track, index| write_track(file, track, "Voice #{index + 1}") }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_track(synths)
|
107
|
+
waveforms = [:tri, :saw, :pulse, :noise]
|
108
|
+
|
109
|
+
track = []
|
110
|
+
current_frame = 0
|
111
|
+
synths.each do |synth|
|
112
|
+
channel = waveforms.index(synth.waveform) || raise("Unknown waveform #{synth.waveform}")
|
113
|
+
track << DeltaTime[synth.start_frame - current_frame]
|
114
|
+
track << NoteOn[channel, synth.tone]
|
115
|
+
current_frame = synth.start_frame
|
116
|
+
|
117
|
+
current_tone = synth.tone
|
118
|
+
synth.controls.each do |start_frame, tone|
|
119
|
+
track << DeltaTime[start_frame - current_frame]
|
120
|
+
track << NoteOff[channel, current_tone]
|
121
|
+
track << DeltaTime[0]
|
122
|
+
track << NoteOn[channel, tone]
|
123
|
+
current_tone = tone
|
124
|
+
current_frame = start_frame
|
125
|
+
end
|
126
|
+
|
127
|
+
end_frame = [current_frame, synth.start_frame + (FRAMES_PER_SECOND * (synth.attack + synth.decay + synth.sustain_length)).to_i].max
|
128
|
+
track << DeltaTime[end_frame - current_frame]
|
129
|
+
track << NoteOff[channel, current_tone]
|
130
|
+
|
131
|
+
current_frame = end_frame
|
132
|
+
end
|
133
|
+
|
134
|
+
track
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
def write_header(file)
|
139
|
+
# Type
|
140
|
+
file << 'MThd'
|
141
|
+
|
142
|
+
# Length
|
143
|
+
write_uint32(file, 6)
|
144
|
+
|
145
|
+
# Format
|
146
|
+
write_uint16(file, 1)
|
147
|
+
|
148
|
+
# Number of tracks
|
149
|
+
write_uint16(file, 3)
|
150
|
+
|
151
|
+
# Division
|
152
|
+
# Default tempo is 120 BPM - 120 quarter-notes per minute. Which is 2 quarter-notes per second. If we then define
|
153
|
+
# 25 ticks per quarter-note, we end up with a timing of 50 ticks per second.
|
154
|
+
write_uint16(file, 25)
|
155
|
+
end
|
156
|
+
|
157
|
+
def write_track(file, track, name)
|
158
|
+
track_with_metadata = [
|
159
|
+
DeltaTime[0], TrackName[name],
|
160
|
+
DeltaTime[0], TimeSignature[4, 2, 24, 8],
|
161
|
+
DeltaTime[0], KeySignature[0, 0],
|
162
|
+
|
163
|
+
DeltaTime[0], ProgramChange[0, 1], # Triangular - maps to piano
|
164
|
+
DeltaTime[0], ProgramChange[1, 25], # Saw - maps to guitar
|
165
|
+
DeltaTime[0], ProgramChange[2, 33], # Pulse - maps to bass
|
166
|
+
DeltaTime[0], ProgramChange[3, 41] # Noise - maps to strings
|
167
|
+
] +
|
168
|
+
track +
|
169
|
+
[
|
170
|
+
DeltaTime[0], EndOfTrack[]
|
171
|
+
]
|
172
|
+
track_bytes = track_with_metadata.flat_map(&:bytes)
|
173
|
+
|
174
|
+
# Type
|
175
|
+
file << 'MTrk'
|
176
|
+
|
177
|
+
# Length
|
178
|
+
write_uint32(file, track_bytes.length)
|
179
|
+
|
180
|
+
file << track_bytes.pack('c' * track_bytes.length)
|
181
|
+
end
|
182
|
+
|
183
|
+
def write_uint32(file, value)
|
184
|
+
bytes = [(value >> 24) & 255, (value >> 16) & 255, (value >> 8) & 255, value & 255]
|
185
|
+
file << bytes.pack('cccc')
|
186
|
+
end
|
187
|
+
|
188
|
+
def write_uint16(file, value)
|
189
|
+
bytes = [(value >> 8) & 255, value & 255]
|
190
|
+
file << bytes.pack('cc')
|
191
|
+
end
|
192
|
+
|
193
|
+
def write_byte(file, value)
|
194
|
+
bytes = [value & 255]
|
195
|
+
file << bytes.pack('c')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class RubyFileWriter
|
3
|
+
def initialize(synths_for_voices)
|
4
|
+
@synths_for_voices = synths_for_voices
|
5
|
+
end
|
6
|
+
|
7
|
+
def write_to(path)
|
8
|
+
File.open(path, 'w') do |file|
|
9
|
+
file.puts '::SYNTHS = ['
|
10
|
+
@synths_for_voices.flatten.sort_by(&:start_frame).each do |synth|
|
11
|
+
file.puts synth.to_a.inspect + ','
|
12
|
+
end
|
13
|
+
file.puts ']'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/sidtool/sid.rb
CHANGED
@@ -2,7 +2,6 @@ module Sidtool
|
|
2
2
|
class Sid
|
3
3
|
def initialize
|
4
4
|
@voices = [Voice.new, Voice.new, Voice.new]
|
5
|
-
@synths = []
|
6
5
|
|
7
6
|
@frequency_low = @frequency_high = 0
|
8
7
|
@pulse_low = @pulse_high = 0
|
@@ -44,5 +43,9 @@ module Sidtool
|
|
44
43
|
def stop!
|
45
44
|
@voices.each(&:stop!)
|
46
45
|
end
|
46
|
+
|
47
|
+
def synths_for_voices
|
48
|
+
@voices.map(&:synths)
|
49
|
+
end
|
47
50
|
end
|
48
51
|
end
|
data/lib/sidtool/state.rb
CHANGED
data/lib/sidtool/synth.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
module Sidtool
|
2
2
|
class Synth
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
attr_reader :start_frame
|
4
|
+
attr_accessor :waveform
|
5
|
+
attr_accessor :attack
|
6
|
+
attr_accessor :decay
|
7
|
+
attr_reader :sustain_length
|
8
|
+
attr_accessor :release
|
9
|
+
attr_reader :controls
|
7
10
|
|
8
11
|
def initialize(start_frame)
|
9
12
|
@start_frame = start_frame
|
@@ -48,10 +51,13 @@ module Sidtool
|
|
48
51
|
end
|
49
52
|
|
50
53
|
def to_a
|
51
|
-
tone = sid_frequency_to_nearest_midi(@frequency)
|
52
54
|
[@start_frame, tone, @waveform, @attack.round(3), @decay.round(3), @sustain_length.round(3), @release.round(3), @controls]
|
53
55
|
end
|
54
56
|
|
57
|
+
def tone
|
58
|
+
sid_frequency_to_nearest_midi(@frequency)
|
59
|
+
end
|
60
|
+
|
55
61
|
private
|
56
62
|
def sid_frequency_to_nearest_midi(sid_frequency)
|
57
63
|
actual_frequency = sid_frequency_to_actual_frequency(sid_frequency)
|
data/lib/sidtool/version.rb
CHANGED
data/lib/sidtool/voice.rb
CHANGED
@@ -7,41 +7,43 @@ module Sidtool
|
|
7
7
|
attr_writer :control_register
|
8
8
|
attr_writer :attack_decay
|
9
9
|
attr_writer :sustain_release
|
10
|
+
attr_reader :synths
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@frequency_low = @frequency_high = 0
|
13
14
|
@pulse_low = @pulse_high = 0
|
14
15
|
@attack_decay = @sustain_release = 0
|
15
16
|
@control_register = 0
|
16
|
-
@
|
17
|
+
@current_synth = nil
|
18
|
+
@synths = []
|
17
19
|
end
|
18
20
|
|
19
21
|
def finish_frame
|
20
22
|
if gate
|
21
|
-
if @
|
22
|
-
@
|
23
|
-
@
|
23
|
+
if @current_synth&.released?
|
24
|
+
@current_synth.stop!
|
25
|
+
@current_synth = nil
|
24
26
|
end
|
25
27
|
|
26
28
|
if frequency > 0
|
27
|
-
if !@
|
28
|
-
@
|
29
|
-
|
29
|
+
if !@current_synth
|
30
|
+
@current_synth = Synth.new(STATE.current_frame)
|
31
|
+
@synths << @current_synth
|
30
32
|
end
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@
|
33
|
+
@current_synth.frequency = frequency
|
34
|
+
@current_synth.waveform = waveform
|
35
|
+
@current_synth.attack = attack
|
36
|
+
@current_synth.decay = decay
|
37
|
+
@current_synth.release = release
|
36
38
|
end
|
37
39
|
else
|
38
|
-
@
|
40
|
+
@current_synth&.release!
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
42
44
|
def stop!
|
43
|
-
@
|
44
|
-
@
|
45
|
+
@current_synth&.stop!
|
46
|
+
@current_synth = nil
|
45
47
|
end
|
46
48
|
|
47
49
|
private
|
data/sidtool.gemspec
CHANGED
@@ -12,6 +12,8 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.homepage = 'https://github.com/olefriis/sidtool'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
15
|
+
spec.required_ruby_version = '>= 2.3'
|
16
|
+
|
15
17
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
18
|
f.match(%r{^(test|spec|features)/})
|
17
19
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidtool
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ole Friis Østergaard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mos6510
|
@@ -101,6 +101,8 @@ files:
|
|
101
101
|
- bin/sidtool
|
102
102
|
- lib/sidtool.rb
|
103
103
|
- lib/sidtool/file_reader.rb
|
104
|
+
- lib/sidtool/midi_file_writer.rb
|
105
|
+
- lib/sidtool/ruby_file_writer.rb
|
104
106
|
- lib/sidtool/sid.rb
|
105
107
|
- lib/sidtool/state.rb
|
106
108
|
- lib/sidtool/synth.rb
|
@@ -119,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
121
|
requirements:
|
120
122
|
- - ">="
|
121
123
|
- !ruby/object:Gem::Version
|
122
|
-
version: '
|
124
|
+
version: '2.3'
|
123
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
126
|
requirements:
|
125
127
|
- - ">="
|