radio 0.0.2 → 0.0.3

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.
@@ -15,31 +15,33 @@
15
15
 
16
16
 
17
17
  class Radio
18
- module Input
18
+ module Signal
19
19
  class File
20
20
  class WAV
21
21
 
22
22
  attr_reader :rate
23
23
 
24
- def initialize id, rate, channel_i, channel_q
25
- @file = ::File.new id
24
+ def initialize options
25
+ @file = ::File.new options[:id]
26
26
  #TODO validate header instead?
27
27
  @file.read 12 # discard header
28
- @rate = rate
29
- @channel_i = channel_i
30
- @channel_q = channel_q
28
+ @rate = options[:rate].to_i
29
+ if input = options[:input]
30
+ @channel_i = input[0]
31
+ @channel_q = input[1]
32
+ end
31
33
  @data = [next_data]
32
34
  @time = Time.now
33
35
  end
34
36
 
35
- def call samples
37
+ def in samples
36
38
  sample_size = @channels * (@bit_sample/8)
37
- @time += 1.0/(rate/samples)
39
+ @time += 1.0/(rate.to_f/samples)
38
40
  sleep [0,@time-Time.now].max
39
41
  while @data.reduce(0){|a,b|a+b.size} < samples * sample_size
40
42
  @data.push next_data
41
43
  end
42
- if channels > 1
44
+ if input_channels > 1
43
45
  out = NArray.scomplex samples
44
46
  else
45
47
  out = NArray.sfloat samples
@@ -59,11 +61,15 @@ class Radio
59
61
  out
60
62
  end
61
63
 
62
- def channels
64
+ def input_channels
63
65
  return 2 if @channel_q and @channels > 1
64
66
  1
65
67
  end
66
68
 
69
+ def output_channels
70
+ 0
71
+ end
72
+
67
73
  def stop
68
74
  @file.close
69
75
  end
@@ -78,9 +84,10 @@ class Radio
78
84
  else
79
85
  raise "Unsupported sample size: #{@bit_sample}"
80
86
  end
81
- return out if channels == 1 and @channels == 1
87
+ return out if input_channels == 1 and @channels == 1
88
+ #TODO buggy wav files with bad endings raise the next line
82
89
  out.reshape! @channels, out.size/@channels
83
- if channels == 1
90
+ if input_channels == 1
84
91
  out[@channel_i,true]
85
92
  else
86
93
  c_out = NArray.scomplex out[0,true].size
@@ -90,26 +97,32 @@ class Radio
90
97
  end
91
98
  end
92
99
 
93
- #TODO read data in chunks smaller than size (which is often the whole file)
94
100
  def next_data
95
101
  loop do
96
102
  until @file.eof?
97
- type = @file.read(4)
98
- size = @file.read(4).unpack("V")[0].to_i
99
- case type
100
- when 'fmt '
101
- fmt = @file.read(size)
102
- @id = fmt.slice(0,2).unpack('c')[0]
103
- @channels = fmt.slice(2,2).unpack('c')[0]
104
- @rate = fmt.slice(4,4).unpack('V').join.to_i
105
- @byte_sec = fmt.slice(8,4).unpack('V').join.to_i
106
- @block_size = fmt.slice(12,2).unpack('c')[0]
107
- @bit_sample = fmt.slice(14,2).unpack('c')[0]
108
- next
109
- when 'data'
110
- return @file.read size
103
+ if @data_size
104
+ actual = [@data_size, 4096].min
105
+ return @file.read actual
106
+ @data_size -= actual
107
+ @data_size = nil if @data_size == 0
111
108
  else
112
- raise "Unknown GIF type: #{type}"
109
+ type = @file.read(4)
110
+ size = @file.read(4).unpack("V")[0].to_i
111
+ case type
112
+ when 'fmt '
113
+ fmt = @file.read(size)
114
+ @id = fmt.slice(0,2).unpack('c')[0]
115
+ @channels = fmt.slice(2,2).unpack('c')[0]
116
+ @rate = fmt.slice(4,4).unpack('V').join.to_i
117
+ @byte_sec = fmt.slice(8,4).unpack('V').join.to_i
118
+ @block_size = fmt.slice(12,2).unpack('c')[0]
119
+ @bit_sample = fmt.slice(14,2).unpack('c')[0]
120
+ next
121
+ when 'data'
122
+ @data_size = size
123
+ else
124
+ raise "Unknown type: #{type}"
125
+ end
113
126
  end
114
127
  end
115
128
  @file.rewind
@@ -0,0 +1,395 @@
1
+ # Copyright 2012 The ham21/radio Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ # This uses similar variable names and similar logic as a
17
+ # popular C implementation of the Parks McClellan algorithm
18
+ # which is based on an old FORTRAN implementation.
19
+ # I did this Ruby version in a hurry so for comments, see the C:
20
+ # http://www.janovetz.com/ Jake Janovetz (janovetz@uiuc.edu)
21
+
22
+
23
+ class Radio
24
+ module Utils
25
+
26
+ FIRPM_CACHE_VERSION = 1
27
+ FIRPM_CACHE_FILENAME = File.expand_path '~/.radio_firpm_cache'
28
+ @@firpm_cache = {}
29
+ @@firpm_mtime = nil
30
+
31
+ def firpm options
32
+ out = nil
33
+ File.open(FIRPM_CACHE_FILENAME, File::CREAT|File::RDWR) do |f|
34
+ f.flock(File::LOCK_EX)
35
+ if f.mtime != @@firpm_mtime
36
+ @@firpm_cache = YAML::load(f)
37
+ unless @@firpm_cache and @@firpm_cache[:version] == FIRPM_CACHE_VERSION
38
+ @@firpm_cache = {version:FIRPM_CACHE_VERSION}
39
+ end
40
+ f.seek 0
41
+ @@firpm_mtime = f.mtime
42
+ end
43
+ firpm = FirPM.new(options)
44
+ out = @@firpm_cache[firpm.options]
45
+ unless out
46
+ out = firpm.to_a
47
+ @@firpm_cache[firpm.options] = out
48
+ f.truncate 0
49
+ YAML::dump @@firpm_cache, f
50
+ end
51
+ end
52
+ out
53
+ end
54
+ module_function :firpm
55
+
56
+ # Instances of FirPM will act as the result array. The result
57
+ # is lazily generated just-in-time for the first use. This
58
+ # allows for CONSTANT=FirPM.new assignments without the huge
59
+ # penalty to application startup. It also let's us normalize
60
+ # the options at initialization for use as a cache key.
61
+ # It is strongly recommended to use the caching mixin.
62
+ class FirPM
63
+
64
+ # :type may be in [:bandpass, :differentiator, :hilbert]
65
+ # :maxiterations won't raise error when negative
66
+ def initialize options
67
+ # normalized and frozen to be suitable for a hash key
68
+ @options = {
69
+ type: options[:type].to_sym,
70
+ numtaps: options[:numtaps].to_i,
71
+ bands: options[:bands].flatten.collect(&:to_f).freeze,
72
+ desired: options[:desired].collect(&:to_f).freeze,
73
+ weights: options[:weights].collect(&:to_f).freeze,
74
+ griddensity: options[:griddensity] || 16,
75
+ maxiterations: options[:maxiterations] || 40
76
+ }.freeze
77
+ end
78
+ attr_reader :options
79
+
80
+ def method_missing name, *opts, &block
81
+ firpm unless @h
82
+ @h.send name, *opts, &block
83
+ end
84
+
85
+ private
86
+
87
+ def firpm
88
+ numtaps = @options[:numtaps]
89
+ bands = @options[:bands]
90
+ des = @options[:desired]
91
+ weight = @options[:weights]
92
+ type = @options[:type]
93
+ griddensity = @options[:griddensity]
94
+ maxiterations = @options[:maxiterations]
95
+
96
+ numband = weight.size
97
+ unless bands.size == numband * 2 and des.size == numband * 2
98
+ raise 'size mismatch in bands, desired, or weights'
99
+ end
100
+
101
+ symmetry = (type == :bandpass) ? :positive : :negative
102
+
103
+ @r = numtaps / 2
104
+ @r += 1 if numtaps.odd? and symmetry == :positive
105
+
106
+ @gridsize = 0
107
+ numband.times do |i|
108
+ increment = 2.0 * @r * griddensity * (bands[2*i+1] - bands[2*i])
109
+ @gridsize += increment.round
110
+ end
111
+ @gridsize -= 1 if symmetry == :negative
112
+
113
+ taps = Array.new @r+1, 0.0
114
+ @h = Array.new numtaps, 0.0
115
+ @grid = Array.new @gridsize, 0.0
116
+ @d = Array.new @gridsize, 0.0
117
+ @w = Array.new @gridsize, 0.0
118
+ @e = Array.new @gridsize, 0.0
119
+ @ext = Array.new @r+1, 0
120
+ @x = Array.new @r+1, 0.0
121
+ @y = Array.new @r+1, 0.0
122
+ @ad = Array.new @r+1, 0.0
123
+ @foundExt = Array.new @r*2, 0
124
+
125
+ lowf = delf = 0.5/(griddensity*@r)
126
+ lowf = bands[0] unless symmetry == :negative and delf > bands[0]
127
+ j=0
128
+ numband.times do |band|
129
+ @grid[j] = bands[2*band]
130
+ lowf = bands[2*band] unless band == 0
131
+ highf = bands[2*band + 1]
132
+ k = ((highf - lowf)/delf).round
133
+ k.times do |i|
134
+ @d[j] = des[2*band] + i*(des[2*band+1]-des[2*band])/(k-1)
135
+ @w[j] = weight[band]
136
+ @grid[j] = lowf
137
+ lowf += delf
138
+ j += 1
139
+ end
140
+ @grid[j-1] = highf
141
+ end
142
+ @grid[@gridsize-1] = 0.5-delf if (
143
+ (symmetry == :negative) &&
144
+ (@grid[@gridsize-1] > (0.5 - delf)) &&
145
+ numtaps.odd?
146
+ )
147
+
148
+ (0..@r).each do |i|
149
+ @ext[i] = i * (@gridsize-1) / @r
150
+ end
151
+
152
+ if type == :differentiator
153
+ @gridsize.times do |i|
154
+ @w[i] = @w[i]/@grid[i] if @d[i] > 0.0001
155
+ end
156
+ end
157
+
158
+ if symmetry == :positive
159
+ if numtaps.even?
160
+ @gridsize.times do |i|
161
+ c = Math.cos(PI * @grid[i])
162
+ @d[i] /= c
163
+ @w[i] *= c
164
+ end
165
+ end
166
+ else
167
+ if numtaps.odd?
168
+ @gridsize.times do |i|
169
+ c = Math.sin(PI2 * @grid[i])
170
+ @d[i] /= c
171
+ @w[i] *= c
172
+ end
173
+ else
174
+ @gridsize.times do |i|
175
+ c = Math.sin(PI * @grid[i])
176
+ @d[i] /= c
177
+ @w[i] *= c
178
+ end
179
+ end
180
+ end
181
+
182
+ if maxiterations > 0
183
+ maxiter_error = true
184
+ else
185
+ maxiterations = -maxiterations
186
+ maxiter_error = false
187
+ end
188
+ iter = 0
189
+ while iter < maxiterations
190
+ calc_params
191
+ @gridsize.times do |i|
192
+ @e[i] = @w[i] * (@d[i] - compute_a(@grid[i]))
193
+ end
194
+ search
195
+ break if done?
196
+ iter += 1
197
+ end
198
+ raise "Maximum iterations exceeded" if maxiter_error && iter == maxiterations
199
+ calc_params
200
+
201
+ (0..numtaps/2).each do |i|
202
+ if symmetry == :positive
203
+ if numtaps.odd?
204
+ c = 1
205
+ else
206
+ c = Math.cos(PI * i / numtaps)
207
+ end
208
+ else
209
+ if numtaps.odd?
210
+ c = Math.sin(PI2 * i / numtaps)
211
+ else
212
+ c = Math.sin(PI * i / numtaps)
213
+ end
214
+ end
215
+ taps[i] = compute_a(i.to_f / numtaps) * c
216
+ end
217
+
218
+ m = (numtaps.to_f-1)/2
219
+ if symmetry == :positive
220
+ if numtaps.odd?
221
+ (0...numtaps).each do |n|
222
+ val = taps[0]
223
+ x = PI2 * (n - m)/numtaps
224
+ (1..m).each do |k|
225
+ val += 2.0 * taps[k] * Math.cos(x*k)
226
+ end
227
+ @h[n] = val/numtaps
228
+ end
229
+ else
230
+ (0...numtaps).each do |n|
231
+ val = taps[0]
232
+ x = PI2 * (n - m)/numtaps
233
+ (1..numtaps/2-1).each do |k|
234
+ val += 2.0 * taps[k] * Math.cos(x*k)
235
+ end
236
+ @h[n] = val/numtaps
237
+ end
238
+ end
239
+ else
240
+ if numtaps.odd?
241
+ (0...numtaps).each do |n|
242
+ val = 0
243
+ x = PI2 * (n - m)/numtaps
244
+ (1..m).each do |k|
245
+ val += 2.0 * taps[k] * Math.sin(x*k)
246
+ end
247
+ @h[n] = val/numtaps
248
+ end
249
+ else
250
+ (0...numtaps).each do |n|
251
+ val = taps[numtaps/2] * Math.sin(PI * (n - m))
252
+ x = PI2 * (n - m) / numtaps
253
+ (1..numtaps/2-1).each do |k|
254
+ val += 2.0 * taps[k] * Math.sin(x*k)
255
+ end
256
+ @h[n] = val/numtaps
257
+ end
258
+ end
259
+ end
260
+
261
+ @gridsize = nil
262
+ @r = nil
263
+ @grid = nil
264
+ @d = nil
265
+ @w = nil
266
+ @e = nil
267
+ @ext = nil
268
+ @x = nil
269
+ @y = nil
270
+ @ad = nil
271
+ @foundExt = nil
272
+ @h.freeze
273
+ end
274
+
275
+
276
+ def calc_params
277
+ (0..@r).each do |i|
278
+ @x[i] = Math.cos(PI2 * @grid[@ext[i]])
279
+ end
280
+ ld = (@r-1)/15 + 1
281
+ (0..@r).each do |i|
282
+ denom = 1.0
283
+ xi = @x[i]
284
+ (0...ld).each do |j|
285
+ k=j
286
+ while k <= @r
287
+ denom *= 2.0*(xi - @x[k]) if k != i
288
+ k += ld
289
+ end
290
+ end
291
+ denom = 0.00001 if denom.abs < 0.00001
292
+ @ad[i] = 1.0/denom
293
+ end
294
+ numer = denom = 0
295
+ sign = 1
296
+ (0..@r).each do |i|
297
+ numer += @ad[i] * @d[@ext[i]]
298
+ denom += sign * @ad[i]/@w[@ext[i]]
299
+ sign = -sign
300
+ end
301
+ delta = numer/denom
302
+ sign = 1
303
+ (0..@r).each do |i|
304
+ @y[i] = @d[@ext[i]] - sign * delta / @w[@ext[i]]
305
+ sign = -sign
306
+ end
307
+ end
308
+
309
+
310
+ def compute_a freq
311
+ denom = numer = 0
312
+ xc = Math.cos(PI2 * freq)
313
+ (0..@r).each do |i|
314
+ c = xc - @x[i]
315
+ if c.abs < 1.0e-7
316
+ numer = @y[i]
317
+ denom = 1
318
+ break
319
+ end
320
+ c = @ad[i]/c
321
+ denom += c
322
+ numer += c*@y[i]
323
+ end
324
+ numer/denom
325
+ end
326
+
327
+
328
+ def search
329
+ @foundExt.fill 0
330
+ k = 0
331
+ if ((@e[0]>0.0) && (@e[0]>@e[1])) || ((@e[0]<0.0) && (@e[0]<@e[1]))
332
+ @foundExt[k] = 0
333
+ k += 1
334
+ end
335
+ (1...@gridsize-1).each do |i|
336
+ if (((@e[i]>=@e[i-1]) && (@e[i]>@e[i+1]) && (@e[i]>0.0)) ||
337
+ ((@e[i]<=@e[i-1]) && (@e[i]<@e[i+1]) && (@e[i]<0.0)))
338
+ @foundExt[k] = i
339
+ k += 1
340
+ end
341
+ end
342
+ j = @gridsize-1
343
+ if (((@e[j]>0.0) && (@e[j]>@e[j-1])) ||
344
+ ((@e[j]<0.0) && (@e[j]<@e[j-1])))
345
+ @foundExt[k] = j
346
+ k += 1
347
+ end
348
+ extra = k - (@r+1)
349
+ while (extra > 0)
350
+ up = @e[@foundExt[0]] > 0.0
351
+ l = 0
352
+ alt = true
353
+ (1...k).each do |j|
354
+ l = j if (@e[@foundExt[j]].abs < @e[@foundExt[l]].abs)
355
+ if up && (@e[@foundExt[j]] < 0.0)
356
+ up = false
357
+ elsif !up && (@e[@foundExt[j]] > 0.0)
358
+ up = true
359
+ else
360
+ alt = false
361
+ break
362
+ end
363
+ end
364
+ if alt && (extra == 1)
365
+ if (@e[@foundExt[k-1]].abs < @e[@foundExt[0]].abs)
366
+ l = @foundExt[k-1]
367
+ else
368
+ l = @foundExt[0]
369
+ end
370
+ end
371
+ (l...k).each do |j|
372
+ @foundExt[j] = @foundExt[j+1]
373
+ end
374
+ k -= 1
375
+ extra -= 1
376
+ end
377
+ (0..@r).each do |i|
378
+ @ext[i] = @foundExt[i]
379
+ end
380
+ end
381
+
382
+
383
+ def done?
384
+ min = max = @e[@ext[0]].abs
385
+ (1..@r).each do |i|
386
+ current = @e[@ext[i]].abs
387
+ min = current if current < min
388
+ max = current if current > max
389
+ end
390
+ ((max-min)/max) < 0.0001
391
+ end
392
+
393
+ end
394
+ end
395
+ end