wav2cas 0.0.1

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