sidtool 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/sidtool +82 -0
- data/lib/sidtool.rb +15 -0
- data/lib/sidtool/file_reader.rb +70 -0
- data/lib/sidtool/sid.rb +48 -0
- data/lib/sidtool/state.rb +11 -0
- data/lib/sidtool/synth.rb +64 -0
- data/lib/sidtool/version.rb +3 -0
- data/lib/sidtool/voice.rb +120 -0
- data/sidtool.gemspec +26 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ee52c07a7f399012cf4f38f0be369570784e54086f7bc0c1bb52664b8cbe01b6
|
4
|
+
data.tar.gz: e5652d2922c9a8b52df256ac1a45074849c81ff256bfdf672bdbdef3c16113ff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8695db8b9e68e83c550bf4eba49eafbe4f4eef09c2c409eea568365a708d8d7446da573b854c126c2b88fe84d6711c5448ad8b1441272d47c205ef467ec3480b
|
7
|
+
data.tar.gz: 0f828663808f36e37f20866699fe0abd7c015a87d803c19a994a9589318ad5ba34a91ba0cdbe821d0cae058f7639f5e96f650a8fdbad718f932ec0a1eb7f1f2f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sidtool (0.0.1)
|
5
|
+
mos6510 (~> 0.1.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
coderay (1.1.2)
|
11
|
+
diff-lcs (1.3)
|
12
|
+
libv8 (7.3.492.27.1-x86_64-darwin-18)
|
13
|
+
method_source (0.9.2)
|
14
|
+
mini_racer (0.2.6)
|
15
|
+
libv8 (>= 6.9.411)
|
16
|
+
mos6510 (0.1.0)
|
17
|
+
mini_racer (~> 0.2.6)
|
18
|
+
pry (0.12.2)
|
19
|
+
coderay (~> 1.1.0)
|
20
|
+
method_source (~> 0.9.0)
|
21
|
+
rake (10.5.0)
|
22
|
+
rspec (3.8.0)
|
23
|
+
rspec-core (~> 3.8.0)
|
24
|
+
rspec-expectations (~> 3.8.0)
|
25
|
+
rspec-mocks (~> 3.8.0)
|
26
|
+
rspec-core (3.8.2)
|
27
|
+
rspec-support (~> 3.8.0)
|
28
|
+
rspec-expectations (3.8.4)
|
29
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
+
rspec-support (~> 3.8.0)
|
31
|
+
rspec-mocks (3.8.1)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.8.0)
|
34
|
+
rspec-support (3.8.2)
|
35
|
+
|
36
|
+
PLATFORMS
|
37
|
+
ruby
|
38
|
+
|
39
|
+
DEPENDENCIES
|
40
|
+
bundler (~> 2.0)
|
41
|
+
pry (~> 0.12.2)
|
42
|
+
rake (~> 10.0)
|
43
|
+
rspec (~> 3.0)
|
44
|
+
sidtool!
|
45
|
+
|
46
|
+
BUNDLED WITH
|
47
|
+
2.0.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Ole Friis Østergaard
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Sidtool
|
2
|
+
|
3
|
+
Convert Commodore 64 SID music in the form of `.sid` files into other formats! This is still VERY
|
4
|
+
much work in progress... the code is ugly and will probably create horrible results for you :-)
|
5
|
+
|
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.
|
10
|
+
|
11
|
+
`.sid` files contain actual Commodore 64 machine code that writes to registers corresponding to the
|
12
|
+
Commodore 64 sound chip, the SID (Sound Interface Device). Which means that in order to play back a
|
13
|
+
`.sid` file like it would sound on an actual Commodore 64, you will have to simulate both the
|
14
|
+
processor and the sound chip.
|
15
|
+
|
16
|
+
This project does not attempt to simulate the SID chip to produce authentic sounds - lots of those
|
17
|
+
players already exist - but instead has a very simple implementation that produces data that can be
|
18
|
+
used to play back the songs. You know, almost as notes.
|
19
|
+
|
20
|
+
Currently the only output format is a Ruby file which defines a list of synths to play at certain
|
21
|
+
points in time. This can be used to play back the music in [Sonic Pi](https://sonic-pi.net).
|
22
|
+
|
23
|
+
## Limitations
|
24
|
+
|
25
|
+
Only a subset of the so-called `PSID` format is supported (a few `.sid` files use the `RSID` format
|
26
|
+
which requires a more complete Commodore 64 environment to run), and maybe not all shortcomings of
|
27
|
+
the support is handled well.
|
28
|
+
|
29
|
+
Only PAL (50 frames per second) is supported. No CIA timers or other fanciness is supported.
|
30
|
+
|
31
|
+
The conversion runs a specified number of frames (default is 1500 - this can be changed on the
|
32
|
+
command line). Ideally it should be able to run until the song finishes.
|
33
|
+
|
34
|
+
For these and other limitations, please consult [the issues](https://github.com/olefriis/sidtool/issues).
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
gem install sidtool
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
You can find lots of `.sid` files (and a super nice list of players for a wide range of platforms)
|
43
|
+
at the [High Voltage SID Collection](https://www.hvsc.c64.org) homepage.
|
44
|
+
|
45
|
+
Show information, like the author and number of songs in a file:
|
46
|
+
|
47
|
+
$ sidtool --info <input file>
|
48
|
+
|
49
|
+
Convert the default song from a file to a Ruby list:
|
50
|
+
|
51
|
+
$ sidtool --out <output file> <input file>
|
52
|
+
|
53
|
+
The output can then be used to play back the music, for example in Sonic Pi:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
load '<path to your output file from before>'
|
57
|
+
|
58
|
+
previous_frame = 1
|
59
|
+
::SYNTHS.each do |synth|
|
60
|
+
current_frame = synth[0]
|
61
|
+
frames_to_sleep = current_frame - previous_frame
|
62
|
+
previous_frame = current_frame
|
63
|
+
sleep frames_to_sleep/50.0 if frames_to_sleep > 0
|
64
|
+
|
65
|
+
in_thread do
|
66
|
+
use_synth synth[2]
|
67
|
+
played_synth = play synth[1], attack: synth[3], decay: synth[4], sustain: synth[5], release: synth[6]
|
68
|
+
|
69
|
+
this_frame = current_frame
|
70
|
+
controls = synth[7]
|
71
|
+
controls.each do |c|
|
72
|
+
sleep (c[0] - this_frame) / 50.0
|
73
|
+
this_frame = c[0]
|
74
|
+
control played_synth, note: c[1]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
It's a bit hacky, I know. Part of the issue is that Sonic Pi has a limit on the size of the edit buffer,
|
81
|
+
so paste the above into the buffer and edit the first line so it loads the (probably rather large)
|
82
|
+
output file from `sidtool`.
|
83
|
+
|
84
|
+
## Development
|
85
|
+
|
86
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to
|
87
|
+
run the tests. You can also run `bin/console` for an interactive prompt that will allow you to
|
88
|
+
experiment.
|
89
|
+
|
90
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new
|
91
|
+
version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
92
|
+
which will create a git tag for the version, push git commits and tags, and push the `.gem` file
|
93
|
+
to [rubygems.org](https://rubygems.org).
|
94
|
+
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/olefriis/sidtool.
|
98
|
+
|
99
|
+
## License
|
100
|
+
|
101
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'sidtool'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/sidtool
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'sidtool'
|
3
|
+
require 'mos6510'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
params = {}
|
7
|
+
OptionParser.new do |parser|
|
8
|
+
parser.banner = 'Usage: sidtool [options] <intputfile.sid>'
|
9
|
+
|
10
|
+
parser.on('-i', '--info', 'Show file information')
|
11
|
+
parser.on('-o', '--out FILENAME', 'Output file (Ruby array)')
|
12
|
+
parser.on('-s', '--song NUMBER', Integer, 'Song number to process')
|
13
|
+
parser.on('-f', '--frames NUMBER', Integer, 'Number of frames to process')
|
14
|
+
parser.on_tail('-h', '--help', 'Show this message') do
|
15
|
+
puts parser
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
parser.on_tail('--version', 'Show version') do
|
19
|
+
puts Sidtool::Version
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
end.parse!(into: params)
|
23
|
+
|
24
|
+
raise 'Missing input file' if ARGV.empty?
|
25
|
+
raise 'Too many arguments' if ARGV.length > 1
|
26
|
+
input_file = ARGV.pop
|
27
|
+
sid_file = Sidtool::FileReader.read(input_file)
|
28
|
+
|
29
|
+
output_file = params[:out]
|
30
|
+
show_info = !!params[:info]
|
31
|
+
raise 'Either provide -i or -o, or I have nothing to do!' unless output_file || show_info
|
32
|
+
|
33
|
+
song = params[:song] || sid_file.start_song
|
34
|
+
raise 'Song must be at least 1' if song < 1
|
35
|
+
raise "File only has #{sid_file.songs} songs" if song > sid_file.songs
|
36
|
+
|
37
|
+
frames = params[:frames] || 1500
|
38
|
+
|
39
|
+
if show_info
|
40
|
+
puts "Read #{sid_file.format} version #{sid_file.version} file."
|
41
|
+
puts "Name: #{sid_file.name}"
|
42
|
+
puts "Author: #{sid_file.author}"
|
43
|
+
puts "Released: #{sid_file.released}"
|
44
|
+
puts "Songs: #{sid_file.songs} (start song: #{sid_file.start_song})"
|
45
|
+
end
|
46
|
+
|
47
|
+
if output_file
|
48
|
+
load_address = sid_file.data[0] + (sid_file.data[1] << 8)
|
49
|
+
|
50
|
+
sid = Sidtool::Sid.new
|
51
|
+
cpu = Mos6510::Cpu.new(sid: sid)
|
52
|
+
|
53
|
+
cpu.load(sid_file.data[2..-1], from: load_address)
|
54
|
+
cpu.start
|
55
|
+
|
56
|
+
play_address = sid_file.play_address
|
57
|
+
if play_address == 0
|
58
|
+
cpu.jsr sid_file.init_address
|
59
|
+
play_address = (cpu.peek(0x0315) << 8) + cpu.peek(0x0314)
|
60
|
+
STDERR.puts "New play address #{play_address}"
|
61
|
+
end
|
62
|
+
|
63
|
+
cpu.jsr sid_file.init_address, song - 1
|
64
|
+
|
65
|
+
frames.times do
|
66
|
+
cpu.jsr play_address
|
67
|
+
sid.finish_frame
|
68
|
+
Sidtool::STATE.current_frame += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
sid.stop!
|
72
|
+
|
73
|
+
STDERR.puts("Processed #{frames} frames")
|
74
|
+
|
75
|
+
File.open(output_file, 'w') do |file|
|
76
|
+
file.puts '::SYNTHS = ['
|
77
|
+
Sidtool::STATE.synths.each do |synth|
|
78
|
+
file.puts synth.to_a.inspect + ','
|
79
|
+
end
|
80
|
+
file.puts ']'
|
81
|
+
end
|
82
|
+
end
|
data/lib/sidtool.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'sidtool/version'
|
2
|
+
|
3
|
+
module Sidtool
|
4
|
+
require 'sidtool/file_reader'
|
5
|
+
require 'sidtool/synth'
|
6
|
+
require 'sidtool/voice'
|
7
|
+
require 'sidtool/sid'
|
8
|
+
require 'sidtool/state'
|
9
|
+
|
10
|
+
# PAL properties
|
11
|
+
FRAMES_PER_SECOND = 50.0
|
12
|
+
CLOCK_FREQUENCY = 985248.0
|
13
|
+
|
14
|
+
STATE = State.new
|
15
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class FileReader
|
3
|
+
attr_reader :format, :version, :init_address, :play_address, :songs, :start_song
|
4
|
+
attr_reader :name, :author, :released
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
def self.read(path)
|
8
|
+
contents = File.open(path, 'rb', encoding: 'ascii-8bit') { |file| file.read }
|
9
|
+
|
10
|
+
expected_data_offset = 0x7C
|
11
|
+
minimum_file_size = expected_data_offset
|
12
|
+
|
13
|
+
raise "File is too small - it should be at least #{minimum_file_size} bytes. The file may be corrupt." unless contents.length >= minimum_file_size
|
14
|
+
|
15
|
+
format = contents[0..3]
|
16
|
+
raise "Unknown file format: #{format}. Only PSID is supported." unless format == 'PSID'
|
17
|
+
|
18
|
+
version = read_word(contents[4..5])
|
19
|
+
raise "Invalid version number: #{version}. Only versions 2, 3, and 4 are supported." unless version >= 2 && version <= 4
|
20
|
+
|
21
|
+
data_offset = read_word(contents[6..7])
|
22
|
+
raise "Invalid data offset: #{data_offset}. This has to be #{expected_data_offset}. The file may be corrupt." unless data_offset == expected_data_offset
|
23
|
+
|
24
|
+
load_address = read_word(contents[8..9])
|
25
|
+
raise "Unsupported load address: #{load_address}. Only 0 is supported for now." unless load_address == 0
|
26
|
+
|
27
|
+
init_address = read_word(contents[10..11])
|
28
|
+
play_address = read_word(contents[12..13])
|
29
|
+
songs = read_word(contents[14..15])
|
30
|
+
start_song = read_word(contents[16..17])
|
31
|
+
|
32
|
+
name = read_null_terminated_string(contents[22..53])
|
33
|
+
author = read_null_terminated_string(contents[54..85])
|
34
|
+
released = read_null_terminated_string(contents[86..117])
|
35
|
+
|
36
|
+
data = read_bytes(contents[data_offset..-1])
|
37
|
+
|
38
|
+
return self.new(format: format, version: version, init_address: init_address, play_address: play_address,
|
39
|
+
songs: songs, start_song: start_song, name: name, author: author, released: released,
|
40
|
+
data: data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(format:, version:, init_address:, play_address:, songs:, start_song:, name:, author:, released:, data:)
|
44
|
+
@format = format
|
45
|
+
@version = version
|
46
|
+
@init_address = init_address
|
47
|
+
@play_address = play_address
|
48
|
+
@songs = songs
|
49
|
+
@start_song = start_song
|
50
|
+
@name = name
|
51
|
+
@author = author
|
52
|
+
@released = released
|
53
|
+
@data = data
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def self.read_word(bytes)
|
58
|
+
(bytes[0].ord << 8) + bytes[1].ord
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.read_null_terminated_string(bytes)
|
62
|
+
first_null = bytes.index("\0") || 32
|
63
|
+
bytes[0..first_null-1]
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.read_bytes(bytes)
|
67
|
+
bytes.chars.map(&:ord)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/sidtool/sid.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class Sid
|
3
|
+
def initialize
|
4
|
+
@voices = [Voice.new, Voice.new, Voice.new]
|
5
|
+
@synths = []
|
6
|
+
|
7
|
+
@frequency_low = @frequency_high = 0
|
8
|
+
@pulse_low = @pulse_high = 0
|
9
|
+
@control_register = 0
|
10
|
+
@attack_decay = @sustain_release = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def poke(register, value)
|
14
|
+
if register >= 0 && register <= 6
|
15
|
+
voice = @voices[0]
|
16
|
+
elsif register >= 7 && register <=13
|
17
|
+
voice = @voices[1]
|
18
|
+
register -=7
|
19
|
+
elsif register >= 14 && register <=20
|
20
|
+
voice = @voices[2]
|
21
|
+
register -=14
|
22
|
+
end
|
23
|
+
|
24
|
+
case register
|
25
|
+
when 0 then voice.frequency_low = value
|
26
|
+
when 1 then voice.frequency_high = value
|
27
|
+
when 2 then voice.pulse_low = value
|
28
|
+
when 3 then voice.pulse_high = value
|
29
|
+
when 4 then voice.control_register = value
|
30
|
+
when 5 then voice.attack_decay = value
|
31
|
+
when 6 then voice.sustain_release = value
|
32
|
+
# 7-20 are covered by the mapping above
|
33
|
+
when 21 then @cutoff_frequency_low = value
|
34
|
+
when 22 then @cutoff_frequency_high = value
|
35
|
+
when 23 then @resonance_filter = value
|
36
|
+
when 24 then @mode_volume = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def finish_frame
|
41
|
+
@voices.each(&:finish_frame)
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop!
|
45
|
+
@voices.each(&:stop!)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class Synth
|
3
|
+
attr_writer :waveform
|
4
|
+
attr_writer :attack
|
5
|
+
attr_writer :decay
|
6
|
+
attr_writer :release
|
7
|
+
|
8
|
+
def initialize(start_frame)
|
9
|
+
@start_frame = start_frame
|
10
|
+
@controls = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def frequency=(frequency)
|
14
|
+
if @frequency
|
15
|
+
previous_midi, current_midi = sid_frequency_to_nearest_midi(@frequency), sid_frequency_to_nearest_midi(frequency)
|
16
|
+
@controls << [STATE.current_frame, current_midi] if previous_midi != current_midi
|
17
|
+
end
|
18
|
+
@frequency = frequency
|
19
|
+
end
|
20
|
+
|
21
|
+
def release!
|
22
|
+
length_of_attack_decay_sustain = (STATE.current_frame - @start_frame) / FRAMES_PER_SECOND
|
23
|
+
if length_of_attack_decay_sustain < @attack
|
24
|
+
@attack = length_of_attack_decay_sustain
|
25
|
+
@decay, @sustain_length = 0, 0
|
26
|
+
elsif length_of_attack_decay_sustain < @attack + @decay
|
27
|
+
@decay = length_of_attack_decay_sustain - @attack
|
28
|
+
@sustain_length = 0
|
29
|
+
else
|
30
|
+
@sustain_length = length_of_attack_decay_sustain - @attack - @decay
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop!
|
35
|
+
# TODO: Should also cut off any remaining release
|
36
|
+
release!
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_a
|
40
|
+
tone = sid_frequency_to_nearest_midi(@frequency)
|
41
|
+
[@start_frame, tone, @waveform, @attack.round(3), @decay.round(3), @sustain_length, @release.round(3), @controls]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def sid_frequency_to_nearest_midi(sid_frequency)
|
46
|
+
actual_frequency = sid_frequency_to_actual_frequency(sid_frequency)
|
47
|
+
nearest_tone(actual_frequency)
|
48
|
+
end
|
49
|
+
|
50
|
+
def nearest_tone(frequency)
|
51
|
+
# Stolen from Sonic Pi
|
52
|
+
midi_tone = (12 * (Math.log(frequency * 0.0022727272727) / Math.log(2))) + 69
|
53
|
+
|
54
|
+
midi_tone.round
|
55
|
+
end
|
56
|
+
|
57
|
+
def sid_frequency_to_actual_frequency(sid_frequency)
|
58
|
+
# With a standard 1 MHz clock
|
59
|
+
# (sid_frequency * 0.0596).round(2)
|
60
|
+
# PAL clock: 985248
|
61
|
+
(sid_frequency * (CLOCK_FREQUENCY / 16777216)).round(2)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Sidtool
|
2
|
+
class Voice
|
3
|
+
attr_writer :frequency_low
|
4
|
+
attr_writer :frequency_high
|
5
|
+
attr_writer :pulse_low
|
6
|
+
attr_writer :pulse_high
|
7
|
+
attr_writer :control_register
|
8
|
+
attr_writer :attack_decay
|
9
|
+
attr_writer :sustain_release
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@frequency_low = @frequency_high = 0
|
13
|
+
@pulse_low = @pulse_high = 0
|
14
|
+
@attack_decay = @sustain_release = 0
|
15
|
+
@control_register = 0
|
16
|
+
@synth = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def finish_frame
|
20
|
+
if gate
|
21
|
+
if frequency > 0
|
22
|
+
unless @synth
|
23
|
+
@synth = Synth.new(STATE.current_frame)
|
24
|
+
STATE.synths << @synth
|
25
|
+
end
|
26
|
+
@synth.frequency = frequency
|
27
|
+
@synth.waveform = waveform
|
28
|
+
@synth.attack = attack
|
29
|
+
@synth.decay = decay
|
30
|
+
@synth.release = release
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@synth&.release!
|
34
|
+
@synth = nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop!
|
39
|
+
@synth&.stop!
|
40
|
+
@synth = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def gate
|
45
|
+
@control_register & 1 == 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def frequency
|
49
|
+
(@frequency_high << 8) + @frequency_low
|
50
|
+
end
|
51
|
+
|
52
|
+
def waveform
|
53
|
+
return :tri if @control_register & 16 != 0
|
54
|
+
return :saw if @control_register & 32 != 0
|
55
|
+
return :pulse if @control_register & 64 != 0
|
56
|
+
return :noise if @control_register & 128 != 0
|
57
|
+
STDERR.puts "Unknown waveform: #{@control_register}"
|
58
|
+
return :noise
|
59
|
+
end
|
60
|
+
|
61
|
+
def attack
|
62
|
+
# Approximated... should be multiplied by 1.000.000 / clock
|
63
|
+
convert_attack(@attack_decay >> 4)
|
64
|
+
end
|
65
|
+
|
66
|
+
def decay
|
67
|
+
# Approximated... should be multiplied by 1.000.000 / clock
|
68
|
+
convert_decay_or_release(@attack_decay & 0xF)
|
69
|
+
end
|
70
|
+
|
71
|
+
def release
|
72
|
+
# Approximated... should be multiplied by 1.000.000 / clock
|
73
|
+
convert_decay_or_release(@sustain_release >> 4)
|
74
|
+
end
|
75
|
+
|
76
|
+
def convert_attack(attack)
|
77
|
+
case attack
|
78
|
+
when 0 then 0.002
|
79
|
+
when 1 then 0.008
|
80
|
+
when 2 then 0.016
|
81
|
+
when 3 then 0.024
|
82
|
+
when 4 then 0.038
|
83
|
+
when 5 then 0.056
|
84
|
+
when 6 then 0.068
|
85
|
+
when 7 then 0.08
|
86
|
+
when 8 then 0.1
|
87
|
+
when 9 then 0.25
|
88
|
+
when 10 then 0.5
|
89
|
+
when 11 then 0.8
|
90
|
+
when 12 then 1
|
91
|
+
when 13 then 3
|
92
|
+
when 14 then 5
|
93
|
+
when 15 then 8
|
94
|
+
else raise "Unknown value: #{attack}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def convert_decay_or_release(decay_or_release)
|
99
|
+
case decay_or_release
|
100
|
+
when 0 then 0.006
|
101
|
+
when 1 then 0.024
|
102
|
+
when 2 then 0.048
|
103
|
+
when 3 then 0.072
|
104
|
+
when 4 then 0.114
|
105
|
+
when 5 then 0.168
|
106
|
+
when 6 then 0.204
|
107
|
+
when 7 then 0.240
|
108
|
+
when 8 then 0.3
|
109
|
+
when 9 then 0.75
|
110
|
+
when 10 then 1.5
|
111
|
+
when 11 then 2.4
|
112
|
+
when 12 then 3
|
113
|
+
when 13 then 9
|
114
|
+
when 14 then 15
|
115
|
+
when 15 then 24
|
116
|
+
else raise "Unknown value: #{decay_or_release}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/sidtool.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "sidtool/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'sidtool'
|
7
|
+
spec.version = Sidtool::VERSION
|
8
|
+
spec.authors = ['Ole Friis Østergaard']
|
9
|
+
spec.email = ['olefriis@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Convert SID tunes to other formats'
|
12
|
+
spec.homepage = 'https://github.com/olefriis/sidtool'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.executables = 'sidtool'
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'mos6510', '~> 0.1.0'
|
22
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
|
+
spec.add_development_dependency 'pry', '~> 0.12.2'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidtool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ole Friis Østergaard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mos6510
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.12.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.12.2
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- olefriis@gmail.com
|
86
|
+
executables:
|
87
|
+
- sidtool
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- Gemfile
|
94
|
+
- Gemfile.lock
|
95
|
+
- LICENSE.txt
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- bin/sidtool
|
101
|
+
- lib/sidtool.rb
|
102
|
+
- lib/sidtool/file_reader.rb
|
103
|
+
- lib/sidtool/sid.rb
|
104
|
+
- lib/sidtool/state.rb
|
105
|
+
- lib/sidtool/synth.rb
|
106
|
+
- lib/sidtool/version.rb
|
107
|
+
- lib/sidtool/voice.rb
|
108
|
+
- sidtool.gemspec
|
109
|
+
homepage: https://github.com/olefriis/sidtool
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.0.1
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Convert SID tunes to other formats
|
132
|
+
test_files: []
|