├── pifm ├── README.md ├── rasplayerfm.conf ├── RasplayerFM.py └── pifm.c /pifm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/rasplayer-fm/master/pifm -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rasplayer-FM 2 | 3 | https://raymii.org/s/articles/Raspberry_Pi_FM_Radio_With_Buttons.html 4 | 5 | 6 | -------------------------------------------------------------------------------- /rasplayerfm.conf: -------------------------------------------------------------------------------- 1 | [rasplayerfm] 2 | frequency = 101.0 3 | shuffle = True 4 | repeat_all = True 5 | stereo_playback = True 6 | music_dir = /rasplayerfm 7 | -------------------------------------------------------------------------------- /RasplayerFM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Rasplayer-FM, 3 | # By Remy van Elst, https://raymii.org 4 | # License: GNU GPLv3 5 | # https://raymii.org/s/articles/Raspberry_Pi_FM_Radio_With_Buttons.html 6 | # Inspired by Pirate Radio by Wynter Woods (Make Magazine) 7 | 8 | try: 9 | import configparser 10 | except: 11 | import ConfigParser as configparser 12 | finally: 13 | import re 14 | import random 15 | import sys 16 | import os 17 | import threading 18 | import time 19 | import subprocess 20 | import RPi.GPIO as GPIO 21 | 22 | cur_song = 0 23 | files_list = [] 24 | playing = 0 25 | stop_press = 0 26 | 27 | fm_process = None 28 | ffmpeg_process = None 29 | on_off = ["off", "on"] 30 | config_location = "/rasplayerfm/rasplayerfm.conf" 31 | 32 | frequency = 108.0 33 | shuffle = False 34 | repeat_all = False 35 | merge_audio_in = False 36 | play_stereo = True 37 | music_dir = "/rasplayerfm" 38 | 39 | music_pipe_r,music_pipe_w = os.pipe() 40 | 41 | def callback_startstop(channel): 42 | global files_list 43 | global cur_song 44 | global playing 45 | global stop_press 46 | if playing: 47 | if not stop_press: 48 | print("Stop playback") 49 | cur_song = cur_song - 1 50 | kill_ffmpeg_and_pifm() 51 | stop_press = 1 52 | playing = 0 53 | else: 54 | print("Start playback") 55 | kill_ffmpeg_and_pifm() 56 | stop_press = 0 57 | 58 | 59 | def callback_previous_next(channel): 60 | global files_list 61 | global cur_song 62 | if playing: 63 | cur_song = cur_song - 1 64 | kill_ffmpeg_and_pifm() 65 | if channel == 18: 66 | if stop_press: 67 | subprocess.call(["/sbin/shutdown", "-h", "now"]) 68 | cur_song = cur_song - 1 69 | print("Previous song") 70 | elif channel == 24: 71 | cur_song = cur_song + 1 72 | print("Next song") 73 | 74 | def add_callbacks(): 75 | GPIO.add_event_detect(18, GPIO.FALLING, callback=callback_previous_next, bouncetime=300) 76 | GPIO.add_event_detect(23, GPIO.FALLING, callback=callback_startstop, bouncetime=300) 77 | GPIO.add_event_detect(24, GPIO.FALLING, callback=callback_previous_next, bouncetime=300) 78 | 79 | def setup_gpio(): 80 | GPIO.setmode(GPIO.BCM) 81 | # button 1, previous 82 | GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP) 83 | # button 2, start/stop 84 | GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP) 85 | # button 3, next 86 | GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_UP) 87 | 88 | def main(): 89 | global files_list 90 | global cur_song 91 | check_requirements() 92 | setup_gpio() 93 | add_callbacks() 94 | setup() 95 | if not files_list: 96 | files_list = build_file_list() 97 | if shuffle == True: 98 | random.shuffle(files_list) 99 | while(True): 100 | print("main") 101 | play_song(cur_song) 102 | time.sleep(1) 103 | 104 | def build_file_list(): 105 | file_list = [] 106 | for root, folders, files in os.walk(music_dir): 107 | folders.sort() 108 | files.sort() 109 | for filename in files: 110 | if re.search(".(aac|mp3|wav|flac|m4a|ogg)$", filename) != None: 111 | file_list.append(os.path.join(root, filename)) 112 | if len(file_list) < 1: 113 | print("Error: no songs found. Add them to %s." % music_dir) 114 | sys.exit(1) 115 | return file_list 116 | 117 | def kill_ffmpeg_and_pifm(): 118 | global fm_process 119 | global ffmpeg_process 120 | global stop_press 121 | try: 122 | fm_process.terminate() 123 | os.wait() 124 | except Exception as e: 125 | pass 126 | 127 | try: 128 | ffmpeg_process.kill() 129 | except Exception as e: 130 | pass 131 | playing = 0 132 | return True 133 | 134 | def play_song(song): 135 | global files_list 136 | global cur_song 137 | global playing 138 | global ffmpeg_process 139 | kill_ffmpeg_and_pifm() 140 | if not stop_press: 141 | print("Number: %i - Name: %s" % (song, files_list[song])) 142 | playing = 1 143 | run_pifm() 144 | with open(os.devnull, "w") as dev_null: 145 | ffmpeg_process = subprocess.Popen(["ffmpeg","-i",files_list[song],"-f","s16le","-acodec","pcm_s16le","-ac", "2" if play_stereo else "1" ,"-ar","44100","-"],stdout=music_pipe_w, stderr=dev_null) 146 | ffmpeg_process.wait() 147 | playing = 0 148 | cur_song = cur_song + 1 149 | 150 | def read_config(): 151 | global frequency 152 | global shuffle 153 | global repeat_all 154 | global play_stereo 155 | global music_dir 156 | try: 157 | config = configparser.ConfigParser() 158 | config.read(config_location) 159 | 160 | except: 161 | print("Error reading from config file.") 162 | else: 163 | play_stereo = config.get("rasplayerfm", 'stereo_playback', fallback=True) 164 | frequency = config.get("rasplayerfm",'frequency') 165 | shuffle = config.getboolean("rasplayerfm",'shuffle',fallback=False) 166 | repeat_all = config.getboolean("rasplayerfm",'repeat_all', fallback=False) 167 | music_dir = config.get("rasplayerfm", 'music_dir', fallback="/rasplayerfm") 168 | print("Playing songs to frequency ", str(frequency)) 169 | print("Shuffle is " + on_off[shuffle]) 170 | print("Repeat All is " + on_off[repeat_all]) 171 | 172 | def setup(): 173 | global frequency 174 | read_config() 175 | 176 | def run_pifm(use_audio_in=False): 177 | global fm_process 178 | with open(os.devnull, "w") as dev_null: 179 | fm_process = subprocess.Popen(["/rasplayerfm/pifm","-",str(frequency),"44100", "stereo" if play_stereo else "mono"], stdin=music_pipe_r, stdout=dev_null) 180 | 181 | def check_requirements(): 182 | try: 183 | ffmpeg_check = subprocess.Popen(["ffmpeg", "-v"]) 184 | ffmpeg_check.kill() 185 | except Exception as e: 186 | print("Error, please install ffmpeg. I failed it: %s" % e) 187 | sys.exit(1) 188 | if not os.path.isfile("/rasplayerfm/pifm"): 189 | print("Error: pifm binary not found. Please place it in /rasplayerfm/pifm") 190 | sys.exit(1) 191 | if not os.path.isfile("/rasplayerfm/rasplayerfm.conf"): 192 | print("Error: config not found. Please place it in /rasplayerfm/rasplayerfm.conf") 193 | sys.exit(1) 194 | 195 | main() 196 | 197 | -------------------------------------------------------------------------------- /pifm.c: -------------------------------------------------------------------------------- 1 | // To run: 2 | // g++ -O3 -o pifm pifm.c 3 | // ./pifm left_right.wav 103.3 22050 stereo 4 | // ./pifm sound.wav 5 | 6 | // Created by Oliver Mattos and Oskar Weigl. 7 | // Code quality = Totally hacked together. 8 | 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #define PAGE_SIZE (4*1024) 26 | #define BLOCK_SIZE (4*1024) 27 | 28 | #define PI 3.14159265 29 | 30 | int mem_fd; 31 | char *gpio_mem, *gpio_map; 32 | char *spi0_mem, *spi0_map; 33 | 34 | 35 | // I/O access 36 | volatile unsigned *gpio; 37 | volatile unsigned *allof7e; 38 | 39 | // GPIO setup macros. Always use INP_GPIO(x) before using OUT_GPIO(x) or SET_GPIO_ALT(x,y) 40 | #define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3)) 41 | #define OUT_GPIO(g) *(gpio+((g)/10)) |= (1<<(((g)%10)*3)) 42 | #define SET_GPIO_ALT(g,a) *(gpio+(((g)/10))) |= (((a)<=3?(a)+4:(a)==4?3:2)<<(((g)%10)*3)) 43 | 44 | #define GPIO_SET *(gpio+7) // sets bits which are 1 ignores bits which are 0 45 | #define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0 46 | #define GPIO_GET *(gpio+13) // sets bits which are 1 ignores bits which are 0 47 | 48 | #define ACCESS(base) *(volatile int*)((int)allof7e+base-0x7e000000) 49 | #define SETBIT(base, bit) ACCESS(base) |= 1<SOURCE_AD = (int)constPage.p + 2048 + intval*4 - 4 ; 238 | bufPtr++; 239 | 240 | // Create DMA command to delay using serializer module for suitable time. 241 | ((struct CB*)(instrs[bufPtr].v))->TXFR_LEN = (int)timeErr-fracval; 242 | bufPtr++; 243 | 244 | // Create DMA command to set clock controller to output FM signal for PWM "HIGH" time. 245 | ((struct CB*)(instrs[bufPtr].v))->SOURCE_AD = (int)constPage.p + 2048 + intval*4 + 4; 246 | bufPtr++; 247 | 248 | // Create DMA command for more delay. 249 | ((struct CB*)(instrs[bufPtr].v))->TXFR_LEN = fracval; 250 | bufPtr=(bufPtr+1) % (BUFFERINSTRUCTIONS); 251 | } 252 | } 253 | }; 254 | 255 | class PreEmp : public SampleSink { 256 | public: 257 | float fmconstant; 258 | float dataold; 259 | SampleSink* next; 260 | 261 | // this isn't the right filter... But it's close... 262 | // Something todo with a bilinear transform not being right... 263 | PreEmp(float rate, SampleSink* next): 264 | fmconstant(rate * 75.0e-6), // for pre-emphisis filter. 75us time constant 265 | dataold(0), 266 | next(next) { }; 267 | 268 | 269 | void consume(float* data, int num) { 270 | for (int i=0; iconsume(&sample, 1); 276 | 277 | dataold = value; 278 | } 279 | } 280 | }; 281 | 282 | 283 | class Resamp : public SampleSink { 284 | public: 285 | static const int QUALITY = 5; // comp. complexity goes up linearly with this. 286 | static const int SQUALITY = 10; // start time quality (defines max phase error of filter vs ram used & cache thrashing) 287 | static const int BUFSIZE = 1000; 288 | float dataOld[QUALITY]; 289 | float sincLUT[SQUALITY][QUALITY]; // [startime][samplenum] 290 | float ratio; 291 | float outTimeLeft; 292 | float outBuffer[BUFSIZE]; 293 | int outBufPtr; 294 | SampleSink* next; 295 | 296 | Resamp(float rateIn, float rateOut, SampleSink* next): 297 | outTimeLeft(1.0), 298 | outBufPtr(0), 299 | ratio((float)rateIn/(float)rateOut), 300 | next(next) { 301 | 302 | for(int i=0; i= BUFSIZE) { 338 | next->consume(outBuffer, outBufPtr); 339 | outBufPtr = 0; 340 | } 341 | } 342 | } 343 | } 344 | }; 345 | 346 | class NullSink: public SampleSink { 347 | public: 348 | 349 | NullSink() { } 350 | 351 | void consume(float* data, int num) {} // throws away data 352 | }; 353 | 354 | // decodes a mono wav file 355 | class Mono: public SampleSink { 356 | public: 357 | SampleSink* next; 358 | 359 | Mono(SampleSink* next): next(next) { } 360 | 361 | void consume(void* data, int num) { // expects num%2 == 0 362 | for (int i=0; iconsume( &l, 1); 365 | } 366 | } 367 | }; 368 | 369 | class StereoSplitter: public SampleSink { 370 | public: 371 | SampleSink* nextLeft; 372 | SampleSink* nextRight; 373 | 374 | StereoSplitter(SampleSink* nextLeft, SampleSink* nextRight): 375 | nextLeft(nextLeft), nextRight(nextRight) { } 376 | 377 | 378 | void consume(void* data, int num) { // expects num%4 == 0 379 | for (int i=0; iconsume( &l, 1); 382 | 383 | float r = (float)(((short*)data)[i+1]) / 32768.0; 384 | nextRight->consume( &r, 1); 385 | } 386 | } 387 | }; 388 | 389 | 390 | const unsigned char RDSDATA[] = { 391 | // RDS data. Send MSB first. Google search gr_rds_data_encoder.cc to make your own data. 392 | 0x50, 0xFF, 0xA9, 0x01, 0x02, 0x1E, 0xB0, 0x00, 0x05, 0xA1, 0x41, 0xA4, 0x12, 393 | 0x50, 0xFF, 0xA9, 0x01, 0x02, 0x45, 0x20, 0x00, 0x05, 0xA1, 0x19, 0xB6, 0x8C, 394 | 0x50, 0xFF, 0xA9, 0x01, 0x02, 0xA9, 0x90, 0x00, 0x05, 0xA0, 0x80, 0x80, 0xDC, 395 | 0x50, 0xFF, 0xA9, 0x01, 0x03, 0xC7, 0xD0, 0x00, 0x05, 0xA0, 0x80, 0x80, 0xDC, 396 | 0x50, 0xFF, 0xA9, 0x09, 0x00, 0x14, 0x75, 0x47, 0x51, 0x7D, 0xB9, 0x95, 0x79, 397 | 0x50, 0xFF, 0xA9, 0x09, 0x00, 0x4F, 0xE7, 0x32, 0x02, 0x21, 0x99, 0xC8, 0x09, 398 | 0x50, 0xFF, 0xA9, 0x09, 0x00, 0xA3, 0x56, 0xF6, 0xD9, 0xE8, 0x81, 0xE5, 0xEE, 399 | 0x50, 0xFF, 0xA9, 0x09, 0x00, 0xF8, 0xC6, 0xF7, 0x5B, 0x19, 0xC8, 0x80, 0x88, 400 | 0x50, 0xFF, 0xA9, 0x09, 0x01, 0x21, 0xA5, 0x26, 0x19, 0xD5, 0xCD, 0xC3, 0xDC, 401 | 0x50, 0xFF, 0xA9, 0x09, 0x01, 0x7A, 0x36, 0x26, 0x56, 0x31, 0xC9, 0xC8, 0x72, 402 | 0x50, 0xFF, 0xA9, 0x09, 0x01, 0x96, 0x87, 0x92, 0x09, 0xA5, 0x41, 0xA4, 0x12, 403 | 0x50, 0xFF, 0xA9, 0x09, 0x01, 0xCD, 0x12, 0x02, 0x8C, 0x0D, 0xBD, 0xB6, 0xA6, 404 | 0x50, 0xFF, 0xA9, 0x09, 0x02, 0x24, 0x46, 0x17, 0x4B, 0xB9, 0xD1, 0xBC, 0xE2, 405 | 0x50, 0xFF, 0xA9, 0x09, 0x02, 0x7F, 0xD7, 0x34, 0x09, 0xE1, 0x9D, 0xB5, 0xFF, 406 | 0x50, 0xFF, 0xA9, 0x09, 0x02, 0x93, 0x66, 0x16, 0x92, 0xD9, 0xB0, 0xB9, 0x3E, 407 | 0x50, 0xFF, 0xA9, 0x09, 0x02, 0xC8, 0xF6, 0x36, 0xF4, 0x85, 0xB4, 0xA4, 0x74, 408 | 0x50, 0xFF, 0xA9, 0x09, 0x03, 0x11, 0x92, 0x02, 0x00, 0x00, 0x80, 0x80, 0xDC, 409 | 0x50, 0xFF, 0xA9, 0x09, 0x03, 0x4A, 0x02, 0x02, 0x00, 0x00, 0x80, 0x80, 0xDC, 410 | 0x50, 0xFF, 0xA9, 0x09, 0x03, 0xA6, 0xB2, 0x02, 0x00, 0x00, 0x80, 0x80, 0xDC, 411 | 0x50, 0xFF, 0xA9, 0x09, 0x03, 0xFD, 0x22, 0x02, 0x00, 0x00, 0x80, 0x80, 0xDC 412 | }; 413 | 414 | class RDSEncoder: public SampleSink { 415 | public: 416 | float sinLut[8]; 417 | SampleSink* next; 418 | int bitNum; 419 | int lastBit; 420 | int time; 421 | float lastValue; 422 | 423 | RDSEncoder(SampleSink* next): 424 | next(next), bitNum(0), lastBit(0), time(0), lastValue(0) { 425 | for (int i=0; i<8; i++) { 426 | sinLut[i] = sin((float)i*2.0*PI*3/8); 427 | } 428 | } 429 | 430 | void consume(float* data, int num) { 431 | for (int i=0; i>(7-(bitNum%8)))&1; 435 | lastBit = lastBit^newBit; // differential encoding 436 | 437 | bitNum = (bitNum+1)%(20*13*8); 438 | } 439 | 440 | int outputBit = (time<192)?lastBit:1-lastBit; // manchester encoding 441 | 442 | lastValue = lastValue*0.99 + (((float)outputBit)*2-1)*0.01; // very simple IIR filter to hopefully reduce sidebands. 443 | data[i] += lastValue*sinLut[time%8]*0.05; 444 | 445 | time = (time+1)%384; 446 | } 447 | next->consume(data, num); 448 | } 449 | }; 450 | 451 | // Takes 2 input signals at 152kHz and stereo modulates it. 452 | class StereoModulator: public SampleSink { 453 | public: 454 | 455 | // Helper to make two input interfaces for the stereomodulator. Feels like I'm reimplementing a closure here... :-( 456 | class ModulatorInput: public SampleSink { 457 | public: 458 | StereoModulator* mod; 459 | int channel; 460 | 461 | ModulatorInput(StereoModulator* mod, int channel): 462 | mod(mod), 463 | channel(channel) { } 464 | 465 | void consume(float* data, int num) { 466 | mod->consume(data, num, channel); 467 | } 468 | }; 469 | 470 | float buffer[1024]; 471 | int bufferOwner; 472 | int bufferLen; 473 | int state; // 8 state state machine. 474 | float sinLut[16]; 475 | 476 | SampleSink* next; 477 | 478 | StereoModulator(SampleSink* next): 479 | next(next), bufferOwner(0), bufferLen(0), state(0) { 480 | for (int i=0; i<16; i++) { 481 | sinLut[i] = sin((float)i*2.0*PI/8); 482 | } 483 | } 484 | 485 | SampleSink* getChannel(int channel) { 486 | return new ModulatorInput(this, channel); // never freed, cos I'm a rebel... 487 | } 488 | 489 | void consume(float* data, int num, int channel) { 490 | if (channel==bufferOwner || bufferLen==0) { 491 | bufferOwner=channel; 492 | // append to buffer 493 | while(num && bufferLen<1024) { 494 | buffer[bufferLen++] = data[0]; 495 | data++; 496 | num--; 497 | } 498 | } else { 499 | int consumable = (bufferLenconsume(buffer, consumable); 508 | 509 | // move stuff along buffer 510 | for (int i=consumable; igetChannel(0))), 538 | 539 | // Right 540 | new PreEmp(samplerate, new Resamp(samplerate, 152000, sm->getChannel(1))) 541 | ); 542 | } else { 543 | ss = new Mono(new PreEmp(samplerate, new Outputter(samplerate))); 544 | } 545 | 546 | for (int i=0; i<22; i++) 547 | read(fp, &data, 2); // read past header 548 | 549 | int readBytes; 550 | while (readBytes = read(fp, &data, 1024)) { 551 | 552 | ss->consume(data, readBytes); 553 | } 554 | close(fp); 555 | } 556 | 557 | void unSetupDMA(){ 558 | printf("exiting\n"); 559 | struct DMAregs* DMA0 = (struct DMAregs*)&(ACCESS(DMABASE)); 560 | DMA0->CS =1<<31; // reset dma controller 561 | 562 | } 563 | 564 | void handSig(int dunno) { 565 | exit(0); 566 | } 567 | void setupDMA( float centerFreq ){ 568 | 569 | atexit(unSetupDMA); 570 | signal (SIGINT, handSig); 571 | signal (SIGTERM, handSig); 572 | signal (SIGHUP, handSig); 573 | signal (SIGQUIT, handSig); 574 | 575 | // allocate a few pages of ram 576 | getRealMemPage(&constPage.v, &constPage.p); 577 | 578 | int centerFreqDivider = (int)((500.0 / centerFreq) * (float)(1<<12) + 0.5); 579 | 580 | // make data page contents - it's essientially 1024 different commands for the 581 | // DMA controller to send to the clock module at the correct time. 582 | for (int i=0; i<1024; i++) 583 | ((int*)(constPage.v))[i] = (0x5a << 24) + centerFreqDivider - 512 + i; 584 | 585 | 586 | int instrCnt = 0; 587 | 588 | while (instrCntSOURCE_AD = (unsigned int)constPage.p+2048; 598 | instr0->DEST_AD = PWMBASE+0x18 /* FIF1 */; 599 | instr0->TXFR_LEN = 4; 600 | instr0->STRIDE = 0; 601 | //instr0->NEXTCONBK = (int)instrPage.p + sizeof(struct CB)*(i+1); 602 | instr0->TI = (1/* DREQ */<<6) | (5 /* PWM */<<16) | (1<<26/* no wide*/) ; 603 | instr0->RES1 = 0; 604 | instr0->RES2 = 0; 605 | 606 | if (!(i%2)) { 607 | instr0->DEST_AD = CM_GP0DIV; 608 | instr0->STRIDE = 4; 609 | instr0->TI = (1<<26/* no wide*/) ; 610 | } 611 | 612 | if (instrCnt!=0) ((struct CB*)(instrs[instrCnt-1].v))->NEXTCONBK = (int)instrs[instrCnt].p; 613 | instr0++; 614 | instrCnt++; 615 | } 616 | } 617 | ((struct CB*)(instrs[BUFFERINSTRUCTIONS-1].v))->NEXTCONBK = (int)instrs[0].p; 618 | 619 | // set up a clock for the PWM 620 | ACCESS(CLKBASE + 40*4 /*PWMCLK_CNTL*/) = 0x5A000026; 621 | usleep(1000); 622 | ACCESS(CLKBASE + 41*4 /*PWMCLK_DIV*/) = 0x5A002800; 623 | ACCESS(CLKBASE + 40*4 /*PWMCLK_CNTL*/) = 0x5A000016; 624 | usleep(1000); 625 | 626 | // set up pwm 627 | ACCESS(PWMBASE + 0x0 /* CTRL*/) = 0; 628 | usleep(1000); 629 | ACCESS(PWMBASE + 0x4 /* status*/) = -1; // clear errors 630 | usleep(1000); 631 | ACCESS(PWMBASE + 0x0 /* CTRL*/) = -1; //(1<<13 /* Use fifo */) | (1<<10 /* repeat */) | (1<<9 /* serializer */) | (1<<8 /* enable ch */) ; 632 | usleep(1000); 633 | ACCESS(PWMBASE + 0x8 /* DMAC*/) = (1<<31 /* DMA enable */) | 0x0707; 634 | 635 | //activate dma 636 | struct DMAregs* DMA0 = (struct DMAregs*)&(ACCESS(DMABASE)); 637 | DMA0->CS =1<<31; // reset 638 | DMA0->CONBLK_AD=0; 639 | DMA0->TI=0; 640 | DMA0->CONBLK_AD = (unsigned int)(instrPage.p); 641 | DMA0->CS =(1<<0)|(255 <<16); // enable bit = 0, clear end flag = 1, prio=19-16 642 | } 643 | 644 | 645 | 646 | int main(int argc, char **argv) 647 | { 648 | 649 | if (argc>1) { 650 | setup_fm(); 651 | setupDMA(argc>2?atof(argv[2]):103.3); 652 | playWav(argv[1], argc>3?atof(argv[3]):22050, argc>4); 653 | } else 654 | fprintf(stderr, "Usage: program wavfile.wav [freq] [sample rate] [stereo]\n\nWhere wavfile is 16 bit 22.5kHz Stereo. Set wavfile to '-' to use stdin.\nfreq is in Mhz (default 103.3)\nsample rate of wav file in Hz\n\nPlay an empty file to transmit silence\n"); 655 | 656 | return 0; 657 | 658 | } // main 659 | 660 | --------------------------------------------------------------------------------