├── LICENSE ├── README.md ├── ctcss-pll.grc └── top_block.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 jazzkutya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | grctcssdec 2 | ========== 3 | 4 | CTCSS decoder with gnuradio 5 | 6 | very early stage, only a flow graph yet but it's usable with a CTCSS frequency chart (try wikipedia). 7 | 8 | Minimal docs here 9 | 10 | Most cpu hungry part is the complex bandpass filter which seems needed 11 | by the pll block to lock on the ctcss frequency accurately. 12 | The pll is also disturbed by DC which proved to be pretty heavy with linrad nbfm. 13 | I've loosened up the settings already to eat less cpu (most important is the transition width). 14 | I need to refresh my memory (actually re-study :)) about the windowing stuff. 15 | 16 | The pll block needs about this much sample rate for signals in the range 60-270Hz because 17 | it has a limited bandwidth (max pi/50 in radians/samples). 18 | 19 | The keep 1 in N block also helps reduce cpu load, no need to do moving average at full sample rate. 20 | -------------------------------------------------------------------------------- /ctcss-pll.grc: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tue Nov 19 23:15:56 2013 4 | 5 | options 6 | 7 | id 8 | top_block 9 | 10 | 11 | _enabled 12 | True 13 | 14 | 15 | title 16 | 17 | 18 | 19 | author 20 | 21 | 22 | 23 | description 24 | 25 | 26 | 27 | window_size 28 | 1280, 1024 29 | 30 | 31 | generate_options 32 | wx_gui 33 | 34 | 35 | category 36 | Custom 37 | 38 | 39 | run_options 40 | prompt 41 | 42 | 43 | run 44 | True 45 | 46 | 47 | max_nouts 48 | 0 49 | 50 | 51 | realtime_scheduling 52 | 53 | 54 | 55 | _coordinate 56 | (10, 11) 57 | 58 | 59 | _rotation 60 | 0 61 | 62 | 63 | 64 | variable 65 | 66 | id 67 | samp_rate 68 | 69 | 70 | _enabled 71 | True 72 | 73 | 74 | value 75 | 44100 76 | 77 | 78 | _coordinate 79 | (312, 19) 80 | 81 | 82 | _rotation 83 | 0 84 | 85 | 86 | 87 | import 88 | 89 | id 90 | import_0 91 | 92 | 93 | _enabled 94 | True 95 | 96 | 97 | import 98 | from math import pi 99 | 100 | 101 | _coordinate 102 | (209, 22) 103 | 104 | 105 | _rotation 106 | 0 107 | 108 | 109 | 110 | audio_source 111 | 112 | id 113 | audio_source_0 114 | 115 | 116 | _enabled 117 | True 118 | 119 | 120 | samp_rate 121 | samp_rate 122 | 123 | 124 | device_name 125 | default 126 | 127 | 128 | ok_to_block 129 | True 130 | 131 | 132 | num_outputs 133 | 1 134 | 135 | 136 | affinity 137 | 138 | 139 | 140 | minoutbuf 141 | 0 142 | 143 | 144 | _coordinate 145 | (41, 114) 146 | 147 | 148 | _rotation 149 | 0 150 | 151 | 152 | 153 | blocks_float_to_complex 154 | 155 | id 156 | blocks_float_to_complex_0 157 | 158 | 159 | _enabled 160 | True 161 | 162 | 163 | vlen 164 | 1 165 | 166 | 167 | affinity 168 | 169 | 170 | 171 | minoutbuf 172 | 0 173 | 174 | 175 | _coordinate 176 | (273, 109) 177 | 178 | 179 | _rotation 180 | 0 181 | 182 | 183 | 184 | band_pass_filter 185 | 186 | id 187 | band_pass_filter_0 188 | 189 | 190 | _enabled 191 | True 192 | 193 | 194 | type 195 | fir_filter_ccc 196 | 197 | 198 | decim 199 | 1 200 | 201 | 202 | interp 203 | 1 204 | 205 | 206 | gain 207 | 1 208 | 209 | 210 | samp_rate 211 | samp_rate 212 | 213 | 214 | low_cutoff_freq 215 | 60 216 | 217 | 218 | high_cutoff_freq 219 | 255 220 | 221 | 222 | width 223 | 100 224 | 225 | 226 | win 227 | firdes.WIN_BLACKMAN 228 | 229 | 230 | beta 231 | 6.76 232 | 233 | 234 | affinity 235 | 236 | 237 | 238 | minoutbuf 239 | 0 240 | 241 | 242 | _coordinate 243 | (482, 50) 244 | 245 | 246 | _rotation 247 | 0 248 | 249 | 250 | 251 | blocks_null_sink 252 | 253 | id 254 | blocks_null_sink_0 255 | 256 | 257 | _enabled 258 | False 259 | 260 | 261 | type 262 | float 263 | 264 | 265 | vlen 266 | 1 267 | 268 | 269 | affinity 270 | 271 | 272 | 273 | _coordinate 274 | (711, 444) 275 | 276 | 277 | _rotation 278 | 0 279 | 280 | 281 | 282 | wxgui_numbersink2 283 | 284 | id 285 | wxgui_numbersink2_0 286 | 287 | 288 | _enabled 289 | True 290 | 291 | 292 | type 293 | float 294 | 295 | 296 | title 297 | Number Plot 298 | 299 | 300 | units 301 | Hz 302 | 303 | 304 | samp_rate 305 | samp_rate 306 | 307 | 308 | min_value 309 | 50 310 | 311 | 312 | max_value 313 | 280 314 | 315 | 316 | factor 317 | 1 318 | 319 | 320 | decimal_places 321 | 2 322 | 323 | 324 | ref_level 325 | 0 326 | 327 | 328 | number_rate 329 | 15 330 | 331 | 332 | peak_hold 333 | False 334 | 335 | 336 | average 337 | False 338 | 339 | 340 | avg_alpha 341 | 0 342 | 343 | 344 | show_gauge 345 | True 346 | 347 | 348 | win_size 349 | 350 | 351 | 352 | grid_pos 353 | 354 | 355 | 356 | notebook 357 | 358 | 359 | 360 | affinity 361 | 362 | 363 | 364 | _coordinate 365 | (239, 213) 366 | 367 | 368 | _rotation 369 | 180 370 | 371 | 372 | 373 | wxgui_fftsink2 374 | 375 | id 376 | wxgui_fftsink2_0 377 | 378 | 379 | _enabled 380 | False 381 | 382 | 383 | type 384 | complex 385 | 386 | 387 | title 388 | FFT Plot 389 | 390 | 391 | samp_rate 392 | 600 393 | 394 | 395 | baseband_freq 396 | 0 397 | 398 | 399 | y_per_div 400 | 10 401 | 402 | 403 | y_divs 404 | 10 405 | 406 | 407 | ref_level 408 | 0 409 | 410 | 411 | ref_scale 412 | 2.0 413 | 414 | 415 | fft_size 416 | 1024 417 | 418 | 419 | fft_rate 420 | 15 421 | 422 | 423 | peak_hold 424 | False 425 | 426 | 427 | average 428 | False 429 | 430 | 431 | avg_alpha 432 | 0 433 | 434 | 435 | win 436 | None 437 | 438 | 439 | win_size 440 | 441 | 442 | 443 | grid_pos 444 | 445 | 446 | 447 | notebook 448 | 449 | 450 | 451 | freqvar 452 | None 453 | 454 | 455 | affinity 456 | 457 | 458 | 459 | _coordinate 460 | (1060, 7) 461 | 462 | 463 | _rotation 464 | 0 465 | 466 | 467 | 468 | analog_pll_freqdet_cf 469 | 470 | id 471 | analog_pll_freqdet_cf_0 472 | 473 | 474 | _enabled 475 | True 476 | 477 | 478 | w 479 | 200*2.0*pi/samp_rate 480 | 481 | 482 | max_freq 483 | 260*2.0*pi/samp_rate 484 | 485 | 486 | min_freq 487 | 60*2.0*pi/samp_rate 488 | 489 | 490 | affinity 491 | 492 | 493 | 494 | minoutbuf 495 | 0 496 | 497 | 498 | _coordinate 499 | (688, 249) 500 | 501 | 502 | _rotation 503 | 0 504 | 505 | 506 | 507 | blocks_keep_one_in_n 508 | 509 | id 510 | blocks_keep_one_in_n_0 511 | 512 | 513 | _enabled 514 | True 515 | 516 | 517 | type 518 | float 519 | 520 | 521 | n 522 | 10 523 | 524 | 525 | vlen 526 | 1 527 | 528 | 529 | affinity 530 | 531 | 532 | 533 | minoutbuf 534 | 0 535 | 536 | 537 | _coordinate 538 | (928, 267) 539 | 540 | 541 | _rotation 542 | 0 543 | 544 | 545 | 546 | blocks_moving_average_xx 547 | 548 | id 549 | blocks_moving_average_xx_0 550 | 551 | 552 | _enabled 553 | True 554 | 555 | 556 | type 557 | float 558 | 559 | 560 | length 561 | samp_rate/50 562 | 563 | 564 | scale 565 | 25/pi 566 | 567 | 568 | max_iter 569 | 200 570 | 571 | 572 | affinity 573 | 574 | 575 | 576 | minoutbuf 577 | 0 578 | 579 | 580 | _coordinate 581 | (709, 349) 582 | 583 | 584 | _rotation 585 | 180 586 | 587 | 588 | 589 | fractional_resampler_xx 590 | 591 | id 592 | fractional_resampler_xx_0 593 | 594 | 595 | _enabled 596 | False 597 | 598 | 599 | type 600 | complex 601 | 602 | 603 | phase_shift 604 | 0 605 | 606 | 607 | resamp_ratio 608 | 73.5 609 | 610 | 611 | affinity 612 | 613 | 614 | 615 | minoutbuf 616 | 0 617 | 618 | 619 | _coordinate 620 | (797, 72) 621 | 622 | 623 | _rotation 624 | 0 625 | 626 | 627 | 628 | blocks_moving_average_xx_0 629 | wxgui_numbersink2_0 630 | 0 631 | 0 632 | 633 | 634 | blocks_float_to_complex_0 635 | band_pass_filter_0 636 | 0 637 | 0 638 | 639 | 640 | band_pass_filter_0 641 | analog_pll_freqdet_cf_0 642 | 0 643 | 0 644 | 645 | 646 | audio_source_0 647 | blocks_float_to_complex_0 648 | 0 649 | 0 650 | 651 | 652 | band_pass_filter_0 653 | fractional_resampler_xx_0 654 | 0 655 | 0 656 | 657 | 658 | fractional_resampler_xx_0 659 | wxgui_fftsink2_0 660 | 0 661 | 0 662 | 663 | 664 | blocks_moving_average_xx_0 665 | blocks_null_sink_0 666 | 0 667 | 0 668 | 669 | 670 | analog_pll_freqdet_cf_0 671 | blocks_keep_one_in_n_0 672 | 0 673 | 0 674 | 675 | 676 | blocks_keep_one_in_n_0 677 | blocks_moving_average_xx_0 678 | 0 679 | 0 680 | 681 | 682 | -------------------------------------------------------------------------------- /top_block.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################## 3 | # Gnuradio Python Flow Graph 4 | # Title: Top Block 5 | # Generated: Tue Nov 19 23:21:43 2013 6 | ################################################## 7 | 8 | from gnuradio import analog 9 | from gnuradio import audio 10 | from gnuradio import blocks 11 | from gnuradio import eng_notation 12 | from gnuradio import filter 13 | from gnuradio import gr 14 | from gnuradio import wxgui 15 | from gnuradio.eng_option import eng_option 16 | from gnuradio.filter import firdes 17 | from gnuradio.wxgui import numbersink2 18 | from grc_gnuradio import wxgui as grc_wxgui 19 | from math import pi 20 | from optparse import OptionParser 21 | import wx 22 | 23 | class top_block(grc_wxgui.top_block_gui): 24 | 25 | def __init__(self): 26 | grc_wxgui.top_block_gui.__init__(self, title="Top Block") 27 | _icon_path = "/usr/share/icons/hicolor/32x32/apps/gnuradio-grc.png" 28 | self.SetIcon(wx.Icon(_icon_path, wx.BITMAP_TYPE_ANY)) 29 | 30 | ################################################## 31 | # Variables 32 | ################################################## 33 | self.samp_rate = samp_rate = 44100 34 | 35 | ################################################## 36 | # Blocks 37 | ################################################## 38 | self.wxgui_numbersink2_0 = numbersink2.number_sink_f( 39 | self.GetWin(), 40 | unit="Hz", 41 | minval=50, 42 | maxval=280, 43 | factor=1, 44 | decimal_places=2, 45 | ref_level=0, 46 | sample_rate=samp_rate, 47 | number_rate=15, 48 | average=False, 49 | avg_alpha=None, 50 | label="Number Plot", 51 | peak_hold=False, 52 | show_gauge=True, 53 | ) 54 | self.Add(self.wxgui_numbersink2_0.win) 55 | self.blocks_moving_average_xx_0 = blocks.moving_average_ff(samp_rate/50, 25/pi, 200) 56 | self.blocks_keep_one_in_n_0 = blocks.keep_one_in_n(gr.sizeof_float*1, 10) 57 | self.blocks_float_to_complex_0 = blocks.float_to_complex(1) 58 | self.band_pass_filter_0 = filter.fir_filter_ccc(1, firdes.complex_band_pass( 59 | 1, samp_rate, 60, 255, 100, firdes.WIN_BLACKMAN, 6.76)) 60 | self.audio_source_0 = audio.source(samp_rate, "default", True) 61 | self.analog_pll_freqdet_cf_0 = analog.pll_freqdet_cf(200*2.0*pi/samp_rate, 260*2.0*pi/samp_rate, 60*2.0*pi/samp_rate) 62 | 63 | ################################################## 64 | # Connections 65 | ################################################## 66 | self.connect((self.blocks_moving_average_xx_0, 0), (self.wxgui_numbersink2_0, 0)) 67 | self.connect((self.blocks_float_to_complex_0, 0), (self.band_pass_filter_0, 0)) 68 | self.connect((self.band_pass_filter_0, 0), (self.analog_pll_freqdet_cf_0, 0)) 69 | self.connect((self.audio_source_0, 0), (self.blocks_float_to_complex_0, 0)) 70 | self.connect((self.analog_pll_freqdet_cf_0, 0), (self.blocks_keep_one_in_n_0, 0)) 71 | self.connect((self.blocks_keep_one_in_n_0, 0), (self.blocks_moving_average_xx_0, 0)) 72 | 73 | 74 | # QT sink close method reimplementation 75 | 76 | def get_samp_rate(self): 77 | return self.samp_rate 78 | 79 | def set_samp_rate(self, samp_rate): 80 | self.samp_rate = samp_rate 81 | self.band_pass_filter_0.set_taps(firdes.complex_band_pass(1, self.samp_rate, 60, 255, 100, firdes.WIN_BLACKMAN, 6.76)) 82 | self.analog_pll_freqdet_cf_0.set_loop_bandwidth(200*2.0*pi/self.samp_rate) 83 | self.analog_pll_freqdet_cf_0.set_max_freq(260*2.0*pi/self.samp_rate) 84 | self.analog_pll_freqdet_cf_0.set_min_freq(60*2.0*pi/self.samp_rate) 85 | self.blocks_moving_average_xx_0.set_length_and_scale(self.samp_rate/50, 25/pi) 86 | 87 | if __name__ == '__main__': 88 | import ctypes 89 | import os 90 | if os.name == 'posix': 91 | try: 92 | x11 = ctypes.cdll.LoadLibrary('libX11.so') 93 | x11.XInitThreads() 94 | except: 95 | print "Warning: failed to XInitThreads()" 96 | parser = OptionParser(option_class=eng_option, usage="%prog: [options]") 97 | (options, args) = parser.parse_args() 98 | tb = top_block() 99 | tb.Start(True) 100 | tb.Wait() 101 | 102 | --------------------------------------------------------------------------------