cw 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.cw_config +9 -18
- data/.gitignore +1 -0
- data/README.md +10 -4
- data/VERSION +1 -1
- data/calculator.rb +63 -0
- data/chart.rb +33 -0
- data/chart2.rb +35 -0
- data/chart3.rb +47 -0
- data/chart4.rb +45 -0
- data/cw.gemspec +5 -3
- data/data/callsign/callsign.yaml +78 -0
- data/data/code/code.yaml +215 -0
- data/example.rb +18 -0
- data/lib/cw.rb +3 -0
- data/lib/cw/alphabet.rb +21 -6
- data/lib/cw/audio_player.rb +5 -8
- data/lib/cw/book.rb +0 -4
- data/lib/cw/book_details.rb +19 -8
- data/lib/cw/callsign.rb +59 -0
- data/lib/cw/cl.rb +12 -11
- data/lib/cw/common_words.rb +26 -3
- data/lib/cw/config.rb +14 -3
- data/lib/cw/cw_dsl.rb +11 -0
- data/lib/cw/cw_encoding.rb +49 -53
- data/lib/cw/file_details.rb +92 -19
- data/lib/cw/print.rb +10 -0
- data/lib/cw/read.rb +341 -0
- data/lib/cw/tone_generator.rb +6 -11
- data/lib/cw/words.rb +1 -1
- data/run_script_tests.rb +1 -1
- data/test/test_config.rb +0 -20
- data/test/test_cw.rb +14 -14
- metadata +20 -11
data/lib/cw/file_details.rb
CHANGED
@@ -3,29 +3,102 @@
|
|
3
3
|
module CWG
|
4
4
|
|
5
5
|
module FileDetails
|
6
|
-
HERE
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
6
|
+
HERE = File.dirname(__FILE__)
|
7
|
+
WORK_DIR = Dir.pwd
|
8
|
+
ROOT = File.expand_path File.join(HERE,'..','..')
|
9
|
+
DATA = File.join(ROOT,'data')
|
10
|
+
AUDIO_DIR = File.join(WORK_DIR,'audio')
|
11
|
+
TEXT = File.join(DATA,'text')
|
12
|
+
CODE = File.join(DATA,'code')
|
13
|
+
CALLS = File.join(DATA,'callsign')
|
14
|
+
DOT_CW_DIR = File.join WORK_DIR, ".cw"
|
15
|
+
DOT_AUDIO_DIR = File.join DOT_CW_DIR, "audio"
|
16
|
+
DICT_FILENAME = "english.txt"
|
17
|
+
CONFIG_FILENAME = ".cw_config"
|
18
|
+
DEF_AUDIO_FILENAME = "audio_output.wav"
|
19
|
+
CODE_FILENAME = File.join CODE, "code.yaml"
|
20
|
+
CALLS_FILENAME = File.join CALLS, "callsign.yaml"
|
21
|
+
DOT_FILENAME = "dot.wav"
|
22
|
+
DASH_FILENAME = "dash.wav"
|
23
|
+
SPACE_FILENAME = "space.wav"
|
24
|
+
E_SPACE_FILENAME = "e_space.wav"
|
25
|
+
BOOKMARK_FILE = "bookmark.txt"
|
26
|
+
DICT_DIR = TEXT
|
27
|
+
ABBREVIATIONS = File.join TEXT, "abbreviations.txt"
|
28
|
+
Q_CODES = File.join TEXT, "q_codes.txt"
|
29
|
+
CONFIG_PATH = File.join ROOT, CONFIG_FILENAME
|
30
|
+
USER_CONFIG_PATH = File.join WORK_DIR, CONFIG_FILENAME
|
19
31
|
|
20
32
|
def init_filenames
|
21
|
-
@repeat_tone
|
22
|
-
@r_tone
|
23
|
-
@text_folder = TEXT
|
24
|
-
@progress_file = 'progress.txt'
|
33
|
+
@repeat_tone = File.join(AUDIO_DIR, "rpt.mp3")
|
34
|
+
@r_tone = File.join(AUDIO_DIR, "r.mp3")
|
25
35
|
end
|
26
36
|
|
27
|
-
def
|
28
|
-
File.
|
37
|
+
def process_dot_cw
|
38
|
+
Dir.mkdir(DOT_CW_DIR) unless File.exists? DOT_CW_DIR
|
39
|
+
DOT_CW_DIR
|
29
40
|
end
|
41
|
+
|
42
|
+
def dot_cw_dir
|
43
|
+
@dot_cw_dir ||= process_dot_cw
|
44
|
+
end
|
45
|
+
|
46
|
+
def process_dot_audio
|
47
|
+
Dir.mkdir(DOT_AUDIO_DIR) unless(dot_cw_dir && File.exists?(DOT_AUDIO_DIR))
|
48
|
+
DOT_AUDIO_DIR
|
49
|
+
end
|
50
|
+
|
51
|
+
def dot_audio_dir
|
52
|
+
@dot_audio_dir ||= process_dot_audio
|
53
|
+
end
|
54
|
+
|
55
|
+
def dot_path
|
56
|
+
File.join dot_audio_dir, DOT_FILENAME
|
57
|
+
end
|
58
|
+
|
59
|
+
def dash_path
|
60
|
+
File.join dot_audio_dir, DASH_FILENAME
|
61
|
+
end
|
62
|
+
|
63
|
+
def space_path
|
64
|
+
File.join dot_audio_dir, SPACE_FILENAME
|
65
|
+
end
|
66
|
+
|
67
|
+
def e_space_path
|
68
|
+
File.join dot_audio_dir, E_SPACE_FILENAME
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_audio_dir
|
72
|
+
Dir.mkdir(AUDIO_DIR) unless File.exists? AUDIO_DIR
|
73
|
+
AUDIO_DIR
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_audio_dir
|
77
|
+
Cfg.config['audio_dir'] ? user_audio_dir : default_audio_dir
|
78
|
+
end
|
79
|
+
|
80
|
+
def user_audio_dir
|
81
|
+
@user_audio_dir ||=
|
82
|
+
unless File.exists? Cfg.config['audio_dir']
|
83
|
+
Dir.mkdir Cfg.config['audio_dir']
|
84
|
+
end
|
85
|
+
Cfg.config['audio_dir']
|
86
|
+
end
|
87
|
+
|
88
|
+
def audio_dir
|
89
|
+
@audio_dir ||= process_audio_dir
|
90
|
+
end
|
91
|
+
|
92
|
+
def audio_filename
|
93
|
+
@audio_filename ||=
|
94
|
+
Cfg.config["audio_filename"] ?
|
95
|
+
Cfg.config["audio_filename"] :
|
96
|
+
DEF_AUDIO_FILENAME
|
97
|
+
end
|
98
|
+
|
99
|
+
def progress_file
|
100
|
+
File.join(dot_cw_dir, BOOKMARK_FILE)
|
101
|
+
end
|
102
|
+
|
30
103
|
end
|
31
104
|
end
|
data/lib/cw/print.rb
CHANGED
@@ -111,6 +111,16 @@ module CWG
|
|
111
111
|
print paint("#{word}", fail_colour)
|
112
112
|
end
|
113
113
|
|
114
|
+
def speculative word
|
115
|
+
print paint("#{word}", fail_colour)
|
116
|
+
end
|
117
|
+
def stable word
|
118
|
+
print paint("#{word}", :yellow)
|
119
|
+
end
|
120
|
+
def optimum word
|
121
|
+
print paint("#{word}", success_colour)
|
122
|
+
end
|
123
|
+
|
114
124
|
def list word
|
115
125
|
print paint("#{word}", list_colour)
|
116
126
|
end
|
data/lib/cw/read.rb
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#require 'wavefile'
|
4
|
+
require 'coreaudio'
|
5
|
+
|
6
|
+
module CWG
|
7
|
+
|
8
|
+
SAMPLE_RATE = 44100
|
9
|
+
MAGNITUDE_CUTOFF = 50000 # 200000
|
10
|
+
N = 441
|
11
|
+
TONE = 882
|
12
|
+
START = 0
|
13
|
+
VALUE = 1
|
14
|
+
BLANKING = 2
|
15
|
+
FILTERED = 3
|
16
|
+
PREV = 4
|
17
|
+
PERIOD = 1
|
18
|
+
AVG = 2
|
19
|
+
class Read
|
20
|
+
|
21
|
+
def n ; N ; end
|
22
|
+
def k ; (0.5 + ((n * @tone / @sample_rate))).to_i ; end
|
23
|
+
def w ; ((2 * Math::PI) / n) * k ; end
|
24
|
+
def cosine ; Math.cos(w) ; end
|
25
|
+
def coeff ; 2 * cosine ; end
|
26
|
+
def n_delay_ms ; n * 1000/SAMPLE_RATE; end
|
27
|
+
def print ; @print ; end
|
28
|
+
|
29
|
+
def initialize(filename)
|
30
|
+
@tone = TONE
|
31
|
+
@mag_max = 0
|
32
|
+
@mag_min = 999999999
|
33
|
+
@sample_rate = SAMPLE_RATE
|
34
|
+
@n_val = n
|
35
|
+
@coeff = coeff
|
36
|
+
@n_delay_ms = n_delay_ms
|
37
|
+
@filename = filename
|
38
|
+
@code = []
|
39
|
+
@q1 = 0
|
40
|
+
@q2 = 0
|
41
|
+
@magnitude_set_point = 10000
|
42
|
+
@magnitude_set_point_low = @magnitude_set_point
|
43
|
+
@wpm = 40
|
44
|
+
@noise_blanking_ms = 6
|
45
|
+
@last_start_time = 0
|
46
|
+
@state = Array.new(5)
|
47
|
+
@high = Array.new(3)
|
48
|
+
@low = Array.new(2)
|
49
|
+
@queue = Queue.new
|
50
|
+
@cw_encoding = CwEncoding.new
|
51
|
+
@print = Print.new
|
52
|
+
@state[START] = 0
|
53
|
+
@state[VALUE] = :low
|
54
|
+
@state[BLANKING] = false
|
55
|
+
@state[FILTERED] = false
|
56
|
+
@state[PREV] = false
|
57
|
+
@high[START] = 0
|
58
|
+
@high[PERIOD] = 0
|
59
|
+
@high[AVG] = 120
|
60
|
+
@low[START] = 0
|
61
|
+
@low[PERIOD] = 0
|
62
|
+
@low[AVG] = 0
|
63
|
+
@millisecs = 0
|
64
|
+
@last = 0
|
65
|
+
@need_space = false
|
66
|
+
puts "@n_val #{@n_val}"
|
67
|
+
puts "@coeff #{@coeff}"
|
68
|
+
puts "@n_delay_ms #{@n_delay_ms}"
|
69
|
+
input
|
70
|
+
end
|
71
|
+
|
72
|
+
def open_sound_device
|
73
|
+
soundflower = nil
|
74
|
+
CoreAudio.devices.each do |device|
|
75
|
+
if device.name == 'Soundflower (2ch)'
|
76
|
+
soundflower = device
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@buf_in = soundflower.input_buffer(44100)
|
80
|
+
@buf_in.start
|
81
|
+
end
|
82
|
+
|
83
|
+
def input
|
84
|
+
dly = @n_delay_ms
|
85
|
+
open_sound_device
|
86
|
+
nval = @n_val
|
87
|
+
buf = @buf_in
|
88
|
+
bufs = nil
|
89
|
+
block_size = nval * 28
|
90
|
+
thr = Thread.fork do
|
91
|
+
loop do
|
92
|
+
@queue.push buf.read(block_size)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
loop do
|
96
|
+
loop do
|
97
|
+
bufs = @queue.pop
|
98
|
+
break if @queue.empty?
|
99
|
+
sleep 0.001
|
100
|
+
end
|
101
|
+
count = 0
|
102
|
+
28.times do
|
103
|
+
nval.times do
|
104
|
+
temp = bufs[count] + bufs[count + 1]
|
105
|
+
# puts temp
|
106
|
+
calc_coeff temp
|
107
|
+
count += 2
|
108
|
+
end
|
109
|
+
@millisecs += dly
|
110
|
+
# @millisecs += dly
|
111
|
+
per_block_processing
|
112
|
+
calc_real_state
|
113
|
+
calc_filtered_state
|
114
|
+
decode_signal
|
115
|
+
end
|
116
|
+
end
|
117
|
+
@buf_in.stop
|
118
|
+
$stdout.puts "done."
|
119
|
+
end
|
120
|
+
|
121
|
+
def dbg_print message
|
122
|
+
if @millisecs > @last
|
123
|
+
@last = @millisecs + 1000
|
124
|
+
puts
|
125
|
+
puts " " + message
|
126
|
+
@mag_min = 999999999
|
127
|
+
@mag_max = 0
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def per_block_processing
|
132
|
+
@magnitude = (@q1 * @q1) + (@q2 * @q2) - @q1 * @q2 * @coeff
|
133
|
+
@magnitude = @magnitude.to_i / 1000000
|
134
|
+
@magnitude = 0 if @magnitude < MAGNITUDE_CUTOFF
|
135
|
+
# if @magnitude >= 1000000000000.0
|
136
|
+
# @magnitude = Math.sqrt(magnitude_squared).to_i
|
137
|
+
# else
|
138
|
+
# @magnitude = 0
|
139
|
+
# end
|
140
|
+
@q1, @q2 = 0, 0;
|
141
|
+
# p @magnitude
|
142
|
+
end
|
143
|
+
|
144
|
+
def magnitude_filter
|
145
|
+
if(@magnitude > @magnitude_set_point_low)
|
146
|
+
@magnitude_set_point = (@magnitude_set_point + ((@magnitude - @magnitude_set_point) / 6)) # moving average filter
|
147
|
+
else
|
148
|
+
@magnitude_set_point = @magnitude_set_point_low
|
149
|
+
end
|
150
|
+
# dbg_print "@magnitude: #{@magnitude.to_s}\n @magnitude_set_point: #{@magnitude_set_point.to_s}\n @magnitude_set_point_low = #{@magnitude_set_point_low}"
|
151
|
+
|
152
|
+
@mag_max = @magnitude if @magnitude > @mag_max
|
153
|
+
@mag_min = @magnitude if @magnitude < @mag_min
|
154
|
+
|
155
|
+
# dbg_print "@magnitude: #{@magnitude.to_s}\n" +
|
156
|
+
# " @mag_max: #{@mag_max.to_s}\n" +
|
157
|
+
# " @mag_min: #{@mag_min.to_s}\n"
|
158
|
+
end
|
159
|
+
|
160
|
+
def calc_real_state
|
161
|
+
@state[VALUE] =
|
162
|
+
(@magnitude > (@magnitude_set_point * 0.6)) ?
|
163
|
+
:high : :low
|
164
|
+
end
|
165
|
+
|
166
|
+
def calc_filtered_state
|
167
|
+
if real_state_change?
|
168
|
+
reset_noise_blanker
|
169
|
+
end
|
170
|
+
|
171
|
+
store_real_state
|
172
|
+
|
173
|
+
if @state[BLANKING]
|
174
|
+
if noise_blanked
|
175
|
+
@state[FILTERED] = true
|
176
|
+
if(@state[VALUE] == :high)
|
177
|
+
@high[START] = @millisecs
|
178
|
+
@high_mag = @magnitude
|
179
|
+
# dbg_print "high: #{@high_mag}\n low: #{@low_mag}\n set point: #{@magnitude_set_point}"
|
180
|
+
@low[PERIOD] = (@millisecs - @low[START])
|
181
|
+
else
|
182
|
+
@low[START] = @millisecs;
|
183
|
+
@low_mag = @magnitude
|
184
|
+
@high[PERIOD] = @millisecs - @high[START]
|
185
|
+
if @high[PERIOD] < (2 * @high[AVG])
|
186
|
+
@high[AVG] = ((@high[PERIOD] + @high[AVG] + @high[AVG]) / 3) # now we know avg dit time ( rolling 3 avg)
|
187
|
+
elsif(@high[PERIOD] > (5 * @high[AVG]))
|
188
|
+
@high[AVG] = @high[PERIOD] + @high[AVG]; # if speed decrease fast ..
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def decode_signal
|
196
|
+
if(@state[FILTERED])
|
197
|
+
@state[FILTERED] = false
|
198
|
+
@need_space = true
|
199
|
+
#dbg_print "wpm #{@wpm.to_s}\nhigh period: #{@high[PERIOD]}\nhigh avg: #{@high[AVG]}"
|
200
|
+
if(@state[VALUE] == :low) # we did end a HIGH
|
201
|
+
if high_avg_compare?(@high[PERIOD], 0.6, 2.0)
|
202
|
+
# 0.6 filter out false dits
|
203
|
+
@code << :dot
|
204
|
+
# $stdout.print '.'
|
205
|
+
end
|
206
|
+
if high_avg_compare?(@high[PERIOD], 2.0, 6.0)
|
207
|
+
@code << :dash
|
208
|
+
# $stdout.print '_'
|
209
|
+
if @millisecs % 10 == 0
|
210
|
+
@wpm = (@wpm + (1200 / ((@high[PERIOD]) / 3))) / 2; # the most precise we can do ;o)
|
211
|
+
# dbg_print "high #{@high[PERIOD]}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
else # we did end a LOW
|
215
|
+
@need_space = false
|
216
|
+
if high_avg_compare?(@low[PERIOD], 2.0, 4.8) # letter space
|
217
|
+
print_char
|
218
|
+
elsif(high_avg_compare?(@low[PERIOD], 4.8, 6.0)) # word space
|
219
|
+
print_char
|
220
|
+
$stdout.print ' '
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
# dbg_print "millisecs #{@millisecs}"
|
225
|
+
if @need_space
|
226
|
+
if high_avg_compare?(@millisecs - @low[START], 6.0, 10)
|
227
|
+
@need_space = false
|
228
|
+
if @state[BLANKING] = false
|
229
|
+
end
|
230
|
+
print_char
|
231
|
+
$stdout.print ' '
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def high_avg_compare?(period, avg_x_low, avg_x_high)
|
237
|
+
(period <= (@high[AVG] * avg_x_high).to_i) &&
|
238
|
+
(period >= (@high[AVG] * avg_x_low).to_i)
|
239
|
+
end
|
240
|
+
|
241
|
+
def noise_blanked
|
242
|
+
if((@millisecs - @state[START]) > @noise_blanking_ms)
|
243
|
+
@state[BLANKING] = false
|
244
|
+
return true
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def print_space
|
249
|
+
@code = [:space]
|
250
|
+
print_char
|
251
|
+
end
|
252
|
+
|
253
|
+
def calc_coeff(data)
|
254
|
+
q0 = @coeff * @q1 - @q2 + data
|
255
|
+
@q2, @q1 = @q1, q0
|
256
|
+
end
|
257
|
+
|
258
|
+
def reset_noise_blanker
|
259
|
+
@state[BLANKING] = true
|
260
|
+
@state[START] = @millisecs
|
261
|
+
end
|
262
|
+
|
263
|
+
def real_state_change?
|
264
|
+
# if @real_state != @real_state_prev
|
265
|
+
# $stdout.print 'hi' if @real_state == true
|
266
|
+
# $stdout.print 'low' if @real_state == false
|
267
|
+
# end
|
268
|
+
@state[VALUE] != @state[PREV]
|
269
|
+
end
|
270
|
+
|
271
|
+
def store_real_state
|
272
|
+
@state[PREV] = @state[VALUE]
|
273
|
+
end
|
274
|
+
|
275
|
+
# def matched_char
|
276
|
+
# @cw_encoding.fetch_char @code
|
277
|
+
# end
|
278
|
+
|
279
|
+
def print_char
|
280
|
+
char = @cw_encoding.fetch_char @code
|
281
|
+
@code = []
|
282
|
+
print.optimum char
|
283
|
+
# if char == ' '
|
284
|
+
# puts "\n high: #{@high_mag}\n"
|
285
|
+
# end
|
286
|
+
|
287
|
+
if false # char == '*'
|
288
|
+
# @wpm -= 5
|
289
|
+
@state[VALUE] = false
|
290
|
+
@state[FILTERED] = false
|
291
|
+
@state[PREV] = false
|
292
|
+
# @awaiting_space = false
|
293
|
+
@low[PERIOD] = 0
|
294
|
+
|
295
|
+
@high[PERIOD] = 0
|
296
|
+
@high[PREV] = 0
|
297
|
+
@high[AVG] = 0
|
298
|
+
@low[START] = 0
|
299
|
+
@millisecs = 0
|
300
|
+
@last = 0
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# def print_char
|
305
|
+
# index = 0
|
306
|
+
# dash_jump = 64
|
307
|
+
# if @code.length < 6
|
308
|
+
# @code.each do |ele|
|
309
|
+
## puts "ele: #{ele}"
|
310
|
+
# dash_jump = dash_jump / 2
|
311
|
+
# index = index + ((ele == :dot) ? 1 : dash_jump)
|
312
|
+
# end
|
313
|
+
# char = @char_lookup[index].to_s
|
314
|
+
## char = @cw_encoding.fetch_char(@code) if char == '*'
|
315
|
+
# # if char == ' '
|
316
|
+
# # puts "\n high: #{@high_mag}\n"
|
317
|
+
# # end
|
318
|
+
# else
|
319
|
+
# char = @cw_encoding.fetch_char(@code)
|
320
|
+
# end
|
321
|
+
|
322
|
+
# if false # char == '*'
|
323
|
+
# # @wpm -= 5
|
324
|
+
# @state[VALUE] = false
|
325
|
+
# @state[FILTERED] = false
|
326
|
+
# @state[PREV] = false
|
327
|
+
# # @awaiting_space = false
|
328
|
+
# @low[PERIOD] = 0
|
329
|
+
#
|
330
|
+
# @high[PERIOD] = 0
|
331
|
+
# @high[PREV] = 0
|
332
|
+
# @high[AVG] = 0
|
333
|
+
# @low[START] = 0
|
334
|
+
# @millisecs = 0
|
335
|
+
# @last = 0
|
336
|
+
# end
|
337
|
+
# print.optimum char
|
338
|
+
# @code = []
|
339
|
+
# end
|
340
|
+
end
|
341
|
+
end
|