wav2cas 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 +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +21 -0
- data/README.md +52 -0
- data/Rakefile +2 -0
- data/bin/wav2cas +38 -0
- data/lib/wav2cas.rb +190 -0
- data/lib/wav2cas/version.rb +3 -0
- data/samples/graph_it.cas +0 -0
- data/samples/graph_it.wav +0 -0
- data/samples/sample1.cas +0 -0
- data/samples/sample1.wav +0 -0
- data/samples/sample2.cas +0 -0
- data/samples/sample2.wav +0 -0
- data/samples/sample3.cas +0 -0
- data/samples/sample3.wav +0 -0
- data/samples/sample4.cas +0 -0
- data/samples/sample4.wav +0 -0
- data/samples/sample5.cas +0 -0
- data/samples/sample5.wav +0 -0
- data/samples/sample6.cas +0 -0
- data/samples/sample6.wav +0 -0
- data/wav2cas.gemspec +29 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9aca0a7e45d36e903629c80c3ba705053c6a9164f5c9fb4ea5f11016231adb37
|
4
|
+
data.tar.gz: ec81b02b64021ff014a013b559d78a204db96d639b149512f94d4f86c067a92d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e63b83b62123249e0276db8261877661c84f6b9affea0a460bc1ec04db18ec1b5c27afd2b9098d8c8f13a1b1516692af41de8074ab4f41eb5aaf7f120cf46ac0
|
7
|
+
data.tar.gz: c1a831aa0b9aff1b208edb5a5df3c5497a15c93ff74a4ea5168e9202277904fb8acba4a57f99b8e941f036cfcf637807e336c47fda2966b536f927d0c2dee68c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
wav2cas (0.0.1)
|
5
|
+
wavefile (~> 1.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
rake (12.3.3)
|
11
|
+
wavefile (1.1.1)
|
12
|
+
|
13
|
+
PLATFORMS
|
14
|
+
ruby
|
15
|
+
|
16
|
+
DEPENDENCIES
|
17
|
+
rake (~> 12.0)
|
18
|
+
wav2cas!
|
19
|
+
|
20
|
+
BUNDLED WITH
|
21
|
+
2.1.4
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# wav2cas
|
2
|
+
|
3
|
+
This utility can be used to convert audio records (in WAV format) saved with TRS-80 (Model I/III) to a CAS file for use by TRS-80 emulators.
|
4
|
+
|
5
|
+
It was created as a result of an attempt to understand what format of data stored on some old tapes found on my bookshelf :-)
|
6
|
+
|
7
|
+
It's HIGHLY experimental, so if you got here, please use other well-known utility for conversion: http://knut.one/wav2cas.htm
|
8
|
+
|
9
|
+
If you can't restore records in mentioned utility (as I did) so you can try this one. It contains some options which allow you to restore data even without lead tone.
|
10
|
+
|
11
|
+
It accepts WAV files with any sample rate (11025 / 22050 / 44100) and format (float / 8-bit / 16-bit / stereo / mono), uses auto-detection mechanism to get clock frequency which depends on baud rate (model I level2 500baud, level1 250baud or model III highspeed 1500 baud).
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
You should have Ruby >= 2.3.0 prior installed. Then type command to install wav2cas:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem install wav2cas
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```
|
24
|
+
Usage: wav2cas [options] <input.wav>
|
25
|
+
-d, --double Double density
|
26
|
+
-o, --output FILENAME Output file
|
27
|
+
-s, --skip N Skip N seconds from the beginning of file
|
28
|
+
-l, --no-lead-tone Audio doesn't start from lead tone (use to recover corrupted records)
|
29
|
+
-a, --auto-align Try to auto align (could fix some records)
|
30
|
+
-t, --threshold THRESHOLD Peak detection threshold (5-15). Default: 10
|
31
|
+
```
|
32
|
+
|
33
|
+
## Examples
|
34
|
+
|
35
|
+
All samples except of `sample4` and `sample6` are processed successfully without additional options.
|
36
|
+
|
37
|
+
```
|
38
|
+
wav2cas samples/graph_it.wav
|
39
|
+
wav2cas samples/sample1.wav
|
40
|
+
wav2cas samples/sample2.wav
|
41
|
+
wav2cas samples/sample3.wav
|
42
|
+
wav2cas -a -t 20 samples/sample4.wav
|
43
|
+
wav2cas samples/sample5.wav
|
44
|
+
wav2cas -a -t 20 samples/sample6.wav
|
45
|
+
```
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
If you have audio records which can not be recognized with this utility, send it to <anton.argirov@gmail.com>, I will try to improve.
|
50
|
+
|
51
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/anteo/wav2cas.
|
52
|
+
|
data/Rakefile
ADDED
data/bin/wav2cas
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'wav2cas'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
|
8
|
+
optparse = OptionParser.new do |opt|
|
9
|
+
opt.banner = "Usage: #{__FILE__} [options] <input.wav>"
|
10
|
+
opt.on("-o FILENAME", "--output FILENAME", "Output file") { |v| options[:output] = v }
|
11
|
+
opt.on("-s N", "--skip N", Float, "Skip N seconds from the beginning of file") { |v| options[:skip] = v }
|
12
|
+
opt.on("-l", "--no-lead-tone", "Audio doesn't start from lead tone (use to recover corrupted records)") { options[:no_lead_tone] = true }
|
13
|
+
opt.on("-a", "--auto-align", "Try to align when clock frequency is lost (could fix some records)") { |v| options[:auto_align] = true }
|
14
|
+
opt.on("-t THRESHOLD", "--threshold THRESHOLD", Integer, "Peak detection threshold (5-30). Default: 10") { |v| options[:peak_threshold] = v }
|
15
|
+
opt.on("-d", "--debug", "Print out some debug information") { |v| options[:debug] = v }
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
optparse.parse!
|
20
|
+
raise OptionParser::MissingArgument, "input file" if ARGV.empty?
|
21
|
+
file_name = ARGV[0]
|
22
|
+
raise OptionParser::InvalidArgument, "input file doesn't exist" unless File.exists?(file_name)
|
23
|
+
rescue OptionParser::ParseError
|
24
|
+
puts $!.to_s
|
25
|
+
puts optparse
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
|
29
|
+
options[:output] ||= File.join(File.dirname(file_name), File.basename(file_name, ".wav") + ".cas")
|
30
|
+
|
31
|
+
converter = Wav2Cas.new(file_name,
|
32
|
+
skip_seconds: options[:skip],
|
33
|
+
has_lead_tone: !options[:no_lead_tone],
|
34
|
+
auto_align: options[:auto_align],
|
35
|
+
peak_threshold: options[:peak_threshold] || 10,
|
36
|
+
debug: options[:debug])
|
37
|
+
|
38
|
+
converter.convert_to(options[:output])
|
data/lib/wav2cas.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'wav2cas/version'
|
2
|
+
require 'wavefile'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
class Wav2Cas
|
6
|
+
Error = Class.new(StandardError)
|
7
|
+
|
8
|
+
def initialize(wav_file, skip_seconds: 0, has_lead_tone: true, auto_align: false, peak_threshold: 10, debug: false)
|
9
|
+
@wav_file = wav_file
|
10
|
+
@skip_seconds = skip_seconds
|
11
|
+
@has_lead_tone = has_lead_tone
|
12
|
+
@auto_align = auto_align
|
13
|
+
@peak_threshold = peak_threshold
|
14
|
+
@debug = debug
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert_to(cas_file)
|
18
|
+
load_samples
|
19
|
+
detect_pulse_distance
|
20
|
+
proof_pulse_distance
|
21
|
+
|
22
|
+
load_data
|
23
|
+
write_cas cas_file
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def debug(message)
|
29
|
+
puts message if @debug
|
30
|
+
end
|
31
|
+
|
32
|
+
def message_with_position(message)
|
33
|
+
"#{message} at #{"%.3f" % current_timestamp}s, position: #{"%08X" % current_position}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_sample(pos, size = 40)
|
37
|
+
val = ((@samples[pos] / 128.0 - 1) * size).round
|
38
|
+
s = "#{"%05d:%03d" % [pos, @samples[pos]]} "
|
39
|
+
neg = [val, 0].min
|
40
|
+
pos = [0, val].max
|
41
|
+
s += " " * (size + neg) + "*" * (-neg)
|
42
|
+
s += "|"
|
43
|
+
s += "*" * pos + " " * (size - pos)
|
44
|
+
puts s
|
45
|
+
end
|
46
|
+
|
47
|
+
def print_samples(start_pos, end_pos)
|
48
|
+
(start_pos..end_pos).each { |pos| print_sample(pos) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def detect_pulse_distance
|
52
|
+
rewind!
|
53
|
+
prev_pulse = nil
|
54
|
+
@pulse_dist = 0
|
55
|
+
20.times do |i|
|
56
|
+
break unless detect_next_pulse
|
57
|
+
if prev_pulse
|
58
|
+
dist = @last_pulse.pos - prev_pulse.pos
|
59
|
+
debug "#{i}th distance between pulses: #{dist}"
|
60
|
+
if dist > @pulse_dist
|
61
|
+
@pulse_dist = dist
|
62
|
+
@start_pos = @last_pulse.pos
|
63
|
+
end
|
64
|
+
end
|
65
|
+
prev_pulse = @last_pulse
|
66
|
+
end
|
67
|
+
raise Error, 'Audio is too short or silent' unless @pulse_dist > 0
|
68
|
+
debug "Detected pulse distance: #{@pulse_dist}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def proof_pulse_distance
|
72
|
+
rewind!
|
73
|
+
20.times do
|
74
|
+
break unless forward!
|
75
|
+
unless @last_pulse
|
76
|
+
raise Error, "Can't detect clock frequency, please ensure that audio starts from lead tone!"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def rewind!
|
82
|
+
@pos = @start_pos || (@source_format.sample_rate * @skip_seconds.to_f).floor
|
83
|
+
@prev_pos = @pos
|
84
|
+
end
|
85
|
+
|
86
|
+
def detect_next_pulse(threshold = 130 + @peak_threshold)
|
87
|
+
@pos += 1 while @pos < @n_samples && @samples[@pos] < threshold
|
88
|
+
return false if @pos >= @n_samples
|
89
|
+
pulse_start = @pos
|
90
|
+
|
91
|
+
@pos += 1 while @pos < @n_samples && @samples[@pos] >= threshold
|
92
|
+
return false if @pos >= @n_samples
|
93
|
+
pulse_end = @pos - 1
|
94
|
+
|
95
|
+
pulse = @samples[pulse_start..pulse_end].each_with_index.max
|
96
|
+
@last_pulse = OpenStruct.new(val: pulse[0], pos: pulse[1] + pulse_start)
|
97
|
+
end
|
98
|
+
|
99
|
+
def detect_pulse(pos = @pos, distance = @pulse_dist / 6)
|
100
|
+
pulse_start = pos - distance
|
101
|
+
pulse_end = pos + distance
|
102
|
+
return if pulse_end >= @n_samples
|
103
|
+
|
104
|
+
samples = @samples[pulse_start..pulse_end]
|
105
|
+
max = samples.each_with_index.max
|
106
|
+
min = samples.each_with_index.min
|
107
|
+
has_pulse = max[0] - min[0] > @peak_threshold
|
108
|
+
has_pulse && OpenStruct.new(val: max[0], pos: max[1] + pulse_start)
|
109
|
+
end
|
110
|
+
|
111
|
+
def load_samples
|
112
|
+
@samples = []
|
113
|
+
|
114
|
+
reader = WaveFile::Reader.new(@wav_file)
|
115
|
+
|
116
|
+
@source_format = reader.format
|
117
|
+
@target_format = WaveFile::Format.new(:mono, :pcm_8, @source_format.sample_rate)
|
118
|
+
|
119
|
+
reader.each_buffer(10000) do |buffer|
|
120
|
+
@samples += buffer.convert(@target_format).samples
|
121
|
+
end
|
122
|
+
|
123
|
+
@n_samples = @samples.count
|
124
|
+
end
|
125
|
+
|
126
|
+
def forward!
|
127
|
+
@prev_pos = @pos
|
128
|
+
@pos += @pulse_dist
|
129
|
+
return false if @pos >= @n_samples
|
130
|
+
@last_pulse = detect_pulse
|
131
|
+
align_to_last_pulse
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def current_timestamp
|
136
|
+
@pos / @source_format.sample_rate.to_f
|
137
|
+
end
|
138
|
+
|
139
|
+
def current_position
|
140
|
+
((@bits.size - 1) / 8)
|
141
|
+
end
|
142
|
+
|
143
|
+
def align_to_last_pulse
|
144
|
+
@pos = @last_pulse.pos if @last_pulse
|
145
|
+
end
|
146
|
+
|
147
|
+
def load_data
|
148
|
+
rewind!
|
149
|
+
@bits = ""
|
150
|
+
@has_sync_byte = false
|
151
|
+
clock_lost = false
|
152
|
+
loop do
|
153
|
+
break unless forward!
|
154
|
+
middle_pos = (@pos + @prev_pos) / 2
|
155
|
+
has_pulse = detect_pulse(middle_pos)
|
156
|
+
@bits += has_pulse ? "1" : "0"
|
157
|
+
if !@has_sync_byte && @has_lead_tone && @bits[-8..-1] == '10100101'
|
158
|
+
@bits = ("0" * 256) + "10100101"
|
159
|
+
@has_sync_byte = true
|
160
|
+
unless @auto_align
|
161
|
+
@pos += @pulse_dist / 2
|
162
|
+
next
|
163
|
+
end
|
164
|
+
end
|
165
|
+
if !@last_pulse
|
166
|
+
if @auto_align
|
167
|
+
debug message_with_position("Auto-align")
|
168
|
+
detect_next_pulse
|
169
|
+
align_to_last_pulse
|
170
|
+
elsif @has_lead_tone && @has_sync_byte
|
171
|
+
clock_lost = true
|
172
|
+
end
|
173
|
+
elsif clock_lost
|
174
|
+
puts message_with_position("Possible read error")
|
175
|
+
clock_lost = false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def write_cas(file_name)
|
181
|
+
if @has_lead_tone
|
182
|
+
File.write file_name, [@bits].pack("B*")
|
183
|
+
else
|
184
|
+
8.times do |i|
|
185
|
+
File.write file_name + "_#{i + 1}", [@bits].pack("B*")
|
186
|
+
@bits.prepend "0"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
Binary file
|
Binary file
|
data/samples/sample1.cas
ADDED
Binary file
|
data/samples/sample1.wav
ADDED
Binary file
|
data/samples/sample2.cas
ADDED
Binary file
|
data/samples/sample2.wav
ADDED
Binary file
|
data/samples/sample3.cas
ADDED
Binary file
|
data/samples/sample3.wav
ADDED
Binary file
|
data/samples/sample4.cas
ADDED
Binary file
|
data/samples/sample4.wav
ADDED
Binary file
|
data/samples/sample5.cas
ADDED
Binary file
|
data/samples/sample5.wav
ADDED
Binary file
|
data/samples/sample6.cas
ADDED
Binary file
|
data/samples/sample6.wav
ADDED
Binary file
|
data/wav2cas.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'lib/wav2cas/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "wav2cas"
|
5
|
+
spec.version = Wav2cas::VERSION
|
6
|
+
spec.authors = ["Anton Argirov"]
|
7
|
+
spec.email = ["anton.argirov@gmail.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{TRS-80 (Model I/III) WAV to CAS converter}
|
10
|
+
spec.description = %q{Converts TRS-80 (Model I/III) computer audio records in WAV format to CAS file for use by TRS-80 emulators.}
|
11
|
+
spec.homepage = "https://github.com/anteo/wav2cas"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
13
|
+
|
14
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "bin"
|
25
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_runtime_dependency 'wavefile', '~>1.1'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wav2cas
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anton Argirov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: wavefile
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
description: Converts TRS-80 (Model I/III) computer audio records in WAV format to
|
28
|
+
CAS file for use by TRS-80 emulators.
|
29
|
+
email:
|
30
|
+
- anton.argirov@gmail.com
|
31
|
+
executables:
|
32
|
+
- wav2cas
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".gitignore"
|
37
|
+
- ".idea/wav2cas.iml"
|
38
|
+
- Gemfile
|
39
|
+
- Gemfile.lock
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- bin/wav2cas
|
43
|
+
- lib/wav2cas.rb
|
44
|
+
- lib/wav2cas/version.rb
|
45
|
+
- samples/graph_it.cas
|
46
|
+
- samples/graph_it.wav
|
47
|
+
- samples/sample1.cas
|
48
|
+
- samples/sample1.wav
|
49
|
+
- samples/sample2.cas
|
50
|
+
- samples/sample2.wav
|
51
|
+
- samples/sample3.cas
|
52
|
+
- samples/sample3.wav
|
53
|
+
- samples/sample4.cas
|
54
|
+
- samples/sample4.wav
|
55
|
+
- samples/sample5.cas
|
56
|
+
- samples/sample5.wav
|
57
|
+
- samples/sample6.cas
|
58
|
+
- samples/sample6.wav
|
59
|
+
- wav2cas.gemspec
|
60
|
+
homepage: https://github.com/anteo/wav2cas
|
61
|
+
licenses: []
|
62
|
+
metadata:
|
63
|
+
allowed_push_host: https://rubygems.org
|
64
|
+
homepage_uri: https://github.com/anteo/wav2cas
|
65
|
+
changelog_uri: https://github.com/anteo/wav2cas
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.3.0
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubygems_version: 3.1.2
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: TRS-80 (Model I/III) WAV to CAS converter
|
85
|
+
test_files: []
|