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.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.idea/
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in wav2cas.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
@@ -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
@@ -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
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -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])
@@ -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
@@ -0,0 +1,3 @@
1
+ module Wav2cas
2
+ VERSION = "0.0.1"
3
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -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: []