sidtool 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|