GqrxRawAudioPlot(wxPerl, GD, UDP client, UDP server, & memory file)

AKA - Another wxPerl example.

Back in June I was looking into the purchase of Digilent's Analog Discovery USB O-scope(still looking). I was considering wrapping the SDK shared library until kovacslattila pointed out that a Linux version of the Waveforms software was under development(now available). The result was that I dropped my plan to wrap the SDK shared library. What I had put together at that point was a simulated O-scope display based on wxPerl and GD. Reference: Digilent Inc. Analog Discovery

Fast forward to October... I had been playing with the Gqrx Software Defined Radio package and had developed a wxPerl script that used Telnet and threads to implement a scanner function on top of Gqrx. Reference: SDR Scanner(aka Threaded wxPerl Example)

Last month, a user on the Gqrx google group asked if there was a way to display the raw audio produced by Gqrx. As it turns out, one channel of the demodulated audio is output to a UDP port. It sounded like a fun challenge and a reason to dig out the simulated O-scope code. Listed below is the GqrxRawAudioPlot.pl script and the udpserver.pl script(provides test UDP packets so that the plot code can be run without having to have SDR hardware and Gqrx installed). If you don't want to run these scripts, you can see a screen snapshot at jmlynesjr.

If nothing else, it's another wxPerl example along with GD graphics, memory file usage, a UDP client and a UDP server.

GqrxRawAudioPlot.pl

#! /usr/bin/perl # Name: GqrxRawAudioPlot.pl Cloned from Wx-WaveForms2.pl(a +simulated o-scope proof of concept) # Author: James M. Lynes, Jr. # Created: February 27, 2016 # Modified By: James M. Lynes, Jr. # Last Modified: April 2, 2016 # Enviroment: Perl 5.18.2, wxPerl .9924, wxWidgets 3.0.1, Ubunt +u 14.04LTS 64bit, HP 15 Quad Core # Change Log: 2/27/2016 - Program Created # 2/28/2016 - Remove all but random plot code, m +erge in socket code # 3/6/2016 - Create an array(fifo) to hold samp +le data values - 600 values # 3/13/2016 - Implement updating of sample array and unpack +ing of complete packet # 3/14/2016 - Add scale factor selection. Correct offset ca +lculation. # 3/23/2016 - Add writing of image updates to memory file i +nstead of disk file # 3/29/2016 - Add socket packet counter with display, fixed + unpacking the complete packet # 3/30/2016 - Add button to change scale factor(1, 10, 100) # 3/31/2016 - Move a few variable assignments outside of lo +ops, # Add screen snapshot to diskfile function # 4/2/2016 - Add display of received packet length # # Description: wxPerl Proof of Concept for display of raw audio + from Gqrx # Read & unpack the first 600 samples to fill the display a +rray # Read following packets, unpack, and update the displa +y array # One sample per UDP packet(localhost/port 7355) # Left Channel Value # 48KHz Sample Rate # 16bit signed, little endian # # Notes: There seems to be a 6-8 second delay between hearing t +he audio # in Gqrx and seeing the curve on the O-scope. # App will hang up(go unresponsve) if Gqrx is not sending p +ackets. # # To Do: Rewrite the o-scope specific code into module form. package main; use strict; use warnings; my $app = App->new(); $app->MainLoop; package App; use strict; use warnings; use base 'Wx::App'; sub OnInit { Wx::InitAllImageHandlers(); my $frame = Frame->new(); $frame->Show(1); } package Frame; use strict; use warnings; use Wx qw(:everything); use base qw(Wx::Frame); use IO::Socket; use GD; use Data::Dumper; sub new { my ($class, $parent) = @_; my $self = $class->SUPER::new($parent, -1, "Gqrx Raw Audio Display +", wxDefaultPosition, [610, 680]); # F +it to o-scope size # Application Initialization my $oscope = {}; # Blank oscope data st +ructure $oscope->{maxw} = 600; # width/x axis $oscope->{maxh} = 600; # height/y axis # Must be square for appearance pu +rposes $oscope->{packet} = 0; # Blank received UDP + packet $oscope->{fifo} = []; # Blank sample array( +display array) $oscope->{audiosamples} = (); # Blank unpacked +sample list $oscope->{fifostatus} = 0; # 0->space avail +able, 1->full $oscope->{connectstatus} = 0; # 0->disconnected +, 1->connected $oscope->{plotstatus} = 0; # 0->plot stoppe +d, 1->plot started $oscope->{scalefactor} = 10; # Sample scale fac +tor(1, 10, 100) $oscope->{memoryfile} = []; # Memory File t +o hold PNG image $oscope->{packetcounter} = 0; # Received packet + counter $oscope->{packetlength} = 0; # Received packet +length $oscope->{pctrlabel} = "Packet Counter:"; # Screen +label $oscope->{plenlabel} = "Packet Length:"; # Screen l +abel $oscope->{slabel} = "Scale Factor:"; # Screen label $oscope->{snapcounter} = 0; # Screen snapsh +ot counter # Create initial blank o-scope screen background oscopeinit($oscope); # Draw a blank O-Scope + screen saveaspngfile($oscope); # Save image to a P +NG disk file $self->{bmp} = Wx::Bitmap->new("rawaudio.png", wxBITMAP_TYPE_PNG); + # Reload disk file into a bitmap $self->{sbm} = Wx::StaticBitmap->new($self, wxID_ANY, $self->{bmp} +, wxDefaultPosition, [600,600]); # Display bitmap # Create Buttons my $connectButton = Wx::Button->new($self, -1, "Connect to Gqrx", +wxDefaultPosition, wxDefaultSize); my $disconnectButton = Wx::Button->new($self, -1, "Disconnect from + Gqrx", wxDefaultPosition, wxDefaultSize); my $plotButton = Wx::Button->new($self, -1, "Plot UDP Data", wxDef +aultPosition, wxDefaultSize); my $exitButton = Wx::Button->new($self, wxID_EXIT, "", wxDefaultPo +sition, wxDefaultSize); my $scaleButton = Wx::Button->new($self, -1, "Change Scale Factor" +, wxDefaultPosition, wxDefaultSize); my $snapButton = Wx::Button->new($self, -1, "Snapshot", wxDefaultP +osition, wxDefaultSize); # Define Timer $self->{timer} = Wx::Timer->new($self); # Create sizers. my $verticalSizerFrame = Wx::BoxSizer->new(wxVERTICAL); $self->SetSizer($verticalSizerFrame); my $verticalSizerControls = Wx::BoxSizer->new(wxVERTICAL); my $horizontalSizerButtons1 = Wx::BoxSizer->new(wxHORIZONTAL); my $horizontalSizerButtons2 = Wx::BoxSizer->new(wxHORIZONTAL); # Layout Sizers $verticalSizerFrame->Add($verticalSizerControls,0,0,0); $verticalSizerFrame->Add($horizontalSizerButtons1,0,0,0); $verticalSizerFrame->Add($horizontalSizerButtons2,0,0,0); $verticalSizerControls->Add($self->{sbm},0,0,0); # O-sc +ope Screen Display $verticalSizerControls->AddSpacer(15); $horizontalSizerButtons1->Add($connectButton,0,0,0); $horizontalSizerButtons1->AddSpacer(10); $horizontalSizerButtons1->Add($disconnectButton,0,0,0); $horizontalSizerButtons1->AddSpacer(10); $horizontalSizerButtons1->Add($plotButton,0,0,0); $horizontalSizerButtons1->AddSpacer(10); $horizontalSizerButtons1->Add($scaleButton,0,0,0); $horizontalSizerButtons2->Add($snapButton,0,0,0); $horizontalSizerButtons2->AddSpacer(45); $horizontalSizerButtons2->Add($exitButton,0,0,0); # Event handlers Wx::Event::EVT_BUTTON($self, $connectButton, sub { my ($self, $event) = @_; if($oscope->{connectstatus} == 0) { # Disconnec +ted ? udpconnect($oscope); # Connect to the G +qrx UDP port $oscope->{connectstatus} = 1; # set Connect +ed $connectButton->SetLabel("Connected"); } }); Wx::Event::EVT_BUTTON($self, $disconnectButton, sub { my ($self, $event) = @_; if($oscope->{connectstatus} == 1) { # Connected + ? $oscope->{plotstatus} = 0; # Stop plott +ing $self->{timer}->Stop; # Stop the screen + update udpdisconnect($oscope); # Disconnect fr +om the Gqrx UDP port $oscope->{connectstatus} = 0; # Set Disconn +ected $connectButton->SetLabel("Connect to Gqrx"); } }); Wx::Event::EVT_BUTTON($self, $plotButton, sub { # Read/ +Plot first 600 samples my ($self, $event) = @_; # (fill the fifo) oscopeinit($oscope); # Redraw the scr +een background $oscope->{color} = $oscope->{blue}; # Plot data + in blue $oscope->{fifostatus} = 0; # Reset fifo sta +tus $oscope->{fifo} = []; # Reset fifo arra +y $oscope->{packetcounter} = 0; # Reset packe +t counter fillqueue($oscope); # Read 600 samples +from the Gqrx UDP port drawcurve($oscope); # Plot the fifo arr +ay saveaspngmemoryfile($oscope); # Save GD Ima +ge as PNG memory file open my $fh, '<', \$oscope->{memoryfile}; # Rebuild + the screen bitmap $self->{bmp} = Wx::Bitmap->new(Wx::Image->new($fh, wxBITMAP_TY +PE_PNG)); close $fh; $self->{sbm}->SetBitmap($self->{bmp}); # Refres +h screen $oscope->{plotstatus} = 1; # Set Plotting s +tarted $self->{timer}->Start(45); # 45 msec refres +h timer }); Wx::Event::EVT_BUTTON($self, $scaleButton, sub { # Chan +ge scale factor(1, 10, 100) my ($self, $event) = @_; $oscope->{scalefactor} *= 10; if($oscope->{scalefactor} > 100) { $oscope->{scalefactor} = 1; } }); Wx::Event::EVT_BUTTON($self, $snapButton, sub { # Snaps +hot the screen to disk my ($self, $event) = @_; snapaspngfile($oscope); }); Wx::Event::EVT_BUTTON($self, $exitButton, sub { # Exit +script my ($self, $event) = @_; $self->Close; }); Wx::Event::EVT_TIMER($self, $self->{timer}, sub { # Dis +play update timer my ($self, $event) = @_; # (all following p +ackets) if($oscope->{plotstatus} == 1) { # Ignore timer + until fifo has been filled $oscope->{socket}->recv($oscope->{packet},2000); # +Wait for a packet - maxlength 2000 @{$oscope->{audiosamples}} = unpack('s<*', $oscope->{packe +t}); # Convert from 16bit "Network" format $oscope->{packetcounter}++; # Update re +ceived packet count $oscope->{packetlength} = length($oscope->{packet}); # +Update received packet length foreach my $sample(@{$oscope->{audiosamples}}) { # +Move the samples to the fifo shiftqueue($oscope->{fifo}, $sample); # Olde +st sample out, newest sample in } oscopeinit($oscope); # Redraw the scree +n background $oscope->{color} = $oscope->{blue}; # Plot +data in blue drawcurve($oscope); # Plot the fifo + array saveaspngmemoryfile($oscope); # Save GD Ima +ge as PNG memory file open my $fh, '<', \$oscope->{memoryfile}; # Reb +uild the screen bitmap $self->{bmp} = Wx::Bitmap->new(Wx::Image->new($fh, wxBITMA +P_TYPE_PNG)); close $fh; $self->{sbm}->SetBitmap($self->{bmp}); # Refres +h screen }; }); $verticalSizerFrame->Layout(); return $self; } # ---------------------------------------------- Subroutines --------- +------------------------------------------------------ # # Initialize/redraw the O-Scope Image background # sub oscopeinit { my($oscope) = @_; $oscope->{image} = GD::Image->new($oscope->{maxw}, $oscope->{maxh} +) || die; $oscope->{white} = $oscope->{image}->colorAllocate(255,255,255); + # 1st allocate defines the background color - White # $oscope->{black} = $oscope->{image}->colorAllocate(0,0,0); + # Unused color $oscope->{green} = $oscope->{image}->colorAllocate(0,255,0); $oscope->{blue} = $oscope->{image}->colorAllocate(0,0,255); # $oscope->{yellow} = $oscope->{image}->colorAllocate(255,255,0); + # Unused color # $oscope->{red} = $oscope->{image}->colorAllocate(255,0,0); + # Unused color $oscope->{color} = $oscope->{green}; # Set screen g +reen on white drawgrid($oscope); } # # Save the Image as a PNG Disk File(initial screen background) # sub saveaspngfile { my($oscope) = @_; my $png_data = $oscope->{image}->png; # Write image + to a file open OUTFILE, ">", "rawaudio.png" || die; binmode OUTFILE; print OUTFILE $png_data; close OUTFILE; } # # Save the Image as a PNG Memory File(updated screen image) # sub saveaspngmemoryfile { my($oscope) = @_; $oscope->{memoryfile} = $oscope->{image}->png; # Write +image to memory } # # Save the Image to a PNG Disk file with incrementing filename # sub snapaspngfile { my($oscope) = @_; my $png_data = $oscope->{image}->png; # Write image + to a disk file open OUTFILE, ">", "rawaudio$oscope->{snapcounter}.png" || die; + # Build the filename binmode OUTFILE; print OUTFILE $png_data; close OUTFILE; $oscope->{snapcounter}++; # Increment file +name } # # Draw a simulated O-Scope Screen # sub drawgrid { my($oscope) = @_; my $maxw = $oscope->{maxw}; my $maxh = $oscope->{maxh}; my $color = $oscope->{color}; # Draw Border $oscope->{image}->setThickness(3); $oscope->{image}->rectangle(1, 2, $maxw-2, $maxh-3, $color); # +Fudge box coord for best appearance # Draw horizontal lines $oscope->{image}->setThickness(1); for(my $i=0; $i<$maxw; $i=$i+50) { # 50 pixels +per major division $oscope->{image}->line(0, $i, $maxw, $i, $color); } # Draw vertical lines for(my $i=0; $i<$maxh; $i=$i+50) { # 50 pixels +per major division $oscope->{image}->line($i, 0, $i, $maxh, $color); } # Draw Axis $oscope->{image}->setThickness(3); $oscope->{image}->line($maxw/2, 0, $maxw/2, $maxh, $color); + # Vertical Axis $oscope->{image}->line(0, $maxh/2, $maxw, $maxh/2, $color); + # Horizontal Axis # Draw Axis tic marks $oscope->{image}->setThickness(1); # Vertical Axis tic marks for(my $i=0; $i<$maxh; $i=$i+10) { # 10 pixels +per minor division $oscope->{image}->line(($maxw/2)-3, $i, ($maxw/2)+3, $i, $colo +r); # 6 pixel wide tic mark } # Horizontal Axis tic marks for(my $i=0; $i<$maxw; $i=$i+10) { # 10 pixels +per minor division $oscope->{image}->line($i, ($maxh/2)+3, $i, ($maxh/2-3), $colo +r); # 6 pixel wide tic mark } # Screen Label(s) - Static text for now $oscope->{image}->string(gdSmallFont, 10, 10, $oscope->{pctrlabel +}, $color); $oscope->{image}->string(gdSmallFont, 120, 10, $oscope->{packetcou +nter}, $color); $oscope->{image}->string(gdSmallFont, 10, 20, $oscope->{slabel}, + $color); $oscope->{image}->string(gdSmallFont, 120, 20, $oscope->{scalefact +or}, $color); $oscope->{image}->string(gdSmallFont, 10, 30, $oscope->{plenlabel +}, $color); $oscope->{image}->string(gdSmallFont, 120, 30, $oscope->{packetlen +gth}, $color); } sub drawcurve { # Draw the current fifo my($oscope) = @_; my $maxw = $oscope->{maxw}; my $maxh = $oscope->{maxh}; my $color = $oscope->{color}; $oscope->{image}->setThickness(1); my $lastx = 0; my $lasty = 0; my $y = 0; for(my $i=0; $i<$maxw; $i=$i+1) { # 1 pixel sam +ple width if($oscope->{fifo}[$i] > 0) { # Scale/offse +t the sample $y = 300 - ($oscope->{fifo}[$i] / $oscope->{scalefactor}); } elsif($oscope->{fifo}[$i] < 0) { $y = 300 + ((abs($oscope->{fifo}[$i])) / $oscope->{scalefa +ctor}); } else { $y = 300; # Default to 300("zero" v +alue) } $oscope->{image}->line($lastx, $lasty, $i, $y, $color); $lastx = $i; $lasty = $y; } } sub udpconnect { my ($oscope) = @_; $oscope->{socket} = IO::Socket::INET->new( # Open u +dp socket to Gqrx LocalAddr => 'localhost', LocalPort => 7355, Proto => 'udp' ); } sub udpdisconnect { # Disconnect udp socket + from Gqrx my ($oscope) = @_; close($oscope->{socket}); } # enqueue, shiftqueue and fillqueue implement a fifo that keeps the mo +st recent 600 samples # enqueue and fillqueue fill the fifo for the first time(600 sample +s) # shiftqueue handles all following samples # topvalue is discarded and the new value is entered at the bot +tom # # note: $#{$fifo} is the highest array index(runs 0-599) sub enqueue { # Insert a sample at the +bottom of the array my ($fifo, $sample) = @_; if($#{$fifo} < 599) { push @$fifo, $sample; return 0; # Space still available } return 1; # Full } sub shiftqueue { # Shift out oldest and pus +h in newest my ($fifo, $sample) = @_; shift $fifo; push $fifo, $sample } sub fillqueue { # Read packets until 60 +0 slot fifo is filled my ($oscope) = @_; while (1) { $oscope->{socket}->recv($oscope->{packet},2000); # Wait + for a packet - maxlength 2000 @{$oscope->{audiosamples}} = unpack('s<*', $oscope->{packet}); + # Convert from 16bit "Network" format $oscope->{packetcounter}++; # Update receiv +ed packet counter $oscope->{packetlength} = length($oscope->{packet}); # +Update received packet length foreach my $sample(@{$oscope->{audiosamples}}) { # Move + the samples to the fifo $oscope->{fifostatus} = enqueue($oscope->{fifo}, $sample); + # 0-not full, 1-full if($oscope->{fifostatus}) {return}; # Loop +until 600 samples are queued } } } 1;

udpserver.pl

#! /usr/bin/perl # Gqrx UDP Server Test Program - udpserver.pl # James M. Lynes, Jr. April 2,2016 # Send test UDP packets to the GqrxRawAudioPlot.pl program # 16bit signed, little endian # # To Run: ./udpserver.pl & # Start the server # ./GqrxRawAudioServer.pl # Display wavefor +ms # kill %1 # Kill the server use strict; use warnings; use IO::Socket; use Time::HiRes qw(sleep); use Data::Dumper; # Application Initialization my $oscope = {}; # Create a blank o-scope $oscope->{waveform} = (); # Create a blank wave +form $oscope->{socket} = IO::Socket::INET->new( # Open udp s +ocket to GqrxRawAudioPlot.pl PeerAddr => 'localhost', PeerPort => 7355, Proto => 'udp' ); $oscope->{packet} = (); # Create a blank UD +P packet to send # Build and Send the transmit packets - a variety of wave types for(my $i=0; $i<3000; $i++) { for(my $j=0; $j<1; $j++) { sinwaveform($oscope); # Sin wave packpacket($oscope); sendpacket($oscope); sleep(.03); } for(my $j=0; $j<5; $j++) { clearwaveform($oscope); # Zero wave packpacket($oscope); sendpacket($oscope); sleep(.03); } for(my $j=0; $j<1; $j++) { randomwaveform($oscope); # Random wave packpacket($oscope); sendpacket($oscope); sleep(.03); } for(my $j=0; $j<5; $j++) { clearwaveform($oscope); # Zero wave packpacket($oscope); sendpacket($oscope); sleep(.03); } } #exit close($oscope->{socket}); # Close the UDP conne +ction # Subroutines -------------------------------------------------------- +------------------------------------------- sub packpacket { my($oscope) = @_; $oscope->{packet} = pack('s<*', @{$oscope->{waveform}}); # +Convert to 16bit "Network" format } sub sendpacket { my($oscope) = @_; $oscope->{socket}->send($oscope->{packet}); } sub sinwaveform { # Build one cycle of a wa +veform my($oscope) = @_; for(my $i=0; $i<360; $i++) { my $rads = $i/57.32; my $s = (sin($rads) * 100) + rand(50); # Sin wa +ve plus noise $oscope->{waveform}[$i] = $s; } } sub clearwaveform { # Zero out the waveform my($oscope) = @_; for(my $i=0; $i<360; $i++) { $oscope->{waveform}[$i] = 0; } } sub randomwaveform { # Random waveform my ($oscope) = @_; for(my $i=0; $i<360; $i++) { $oscope->{waveform}[$i] = rand(100) - 50; } }

James

There's never enough time to do it right, but always enough time to do it over...