wav2cas 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 +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: []
|