GqrxRawAudioPlot(wxPerl, GD, UDP client, UDP server, & memory file)
James Lynes <[email protected]> Sat, 9 Apr 2016 13:17:26 -0400
| Newsgroups | gmane.comp.lang.perl.wxperl |
|---|---|
| Message-ID | <CABPysejnM=LGYZ8gQne4s0xvzv5dK3XX4YBoRiEK6QU+O703gQ@mail.gmail.com> |
For those that haven't been on PM lately, here's a repost. ---------------------------------------------------------------------------------- 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. James
GqrxRawAudioPlot.pl
(application/x-perl, 16.1 KB)
#! /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 4, 2016
# Enviroment: Perl 5.18.2, wxPerl .9924, wxWidgets 3.0.1, Ubuntu 14.04LTS 64bit, HP 15 Quad Core
# Change Log: 2/27/2016 - Program Created
# 2/28/2016 - Remove all but random plot code, merge in socket code
# 3/6/2016 - Create an array(fifo) to hold sample data values - 600 values
# 3/13/2016 - Implement updating of sample array and unpacking of complete packet
# 3/14/2016 - Add scale factor selection. Correct offset calculation.
# 3/23/2016 - Add writing of image updates to memory file instead 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 loops,
# Add screen snapshot to diskfile function
# 4/2/2016 - Add display of received packet length
# 4/4/2016 - Add Connect & Plot button color highlighting
#
# Description: wxPerl Proof of Concept for display of raw audio from Gqrx
# Read & unpack the first 600 samples to fill the display array
# Read following packets, unpack, and update the display 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 the audio
# in Gqrx and seeing the curve on the O-scope.
# App will hang up(go unresponsve) if Gqrx is not sending packets.
#
# 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]); # Fit to o-scope size
# Application Initialization
my $oscope = {}; # Blank oscope data structure
$oscope->{maxw} = 600; # width/x axis
$oscope->{maxh} = 600; # height/y axis
# Must be square for appearance purposes
$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 available, 1->full
$oscope->{connectstatus} = 0; # 0->disconnected, 1->connected
$oscope->{plotstatus} = 0; # 0->plot stopped, 1->plot started
$oscope->{scalefactor} = 10; # Sample scale factor(1, 10, 100)
$oscope->{memoryfile} = []; # Memory File to 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 label
$oscope->{slabel} = "Scale Factor:"; # Screen label
$oscope->{snapcounter} = 0; # Screen snapshot counter
# Create initial blank o-scope screen background
oscopeinit($oscope); # Draw a blank O-Scope screen
saveaspngfile($oscope); # Save image to a PNG 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", wxDefaultPosition, wxDefaultSize);
my $exitButton = Wx::Button->new($self, wxID_EXIT, "", wxDefaultPosition, wxDefaultSize);
my $scaleButton = Wx::Button->new($self, -1, "Change Scale Factor", wxDefaultPosition, wxDefaultSize);
my $snapButton = Wx::Button->new($self, -1, "Snapshot", wxDefaultPosition, 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-scope 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) { # Disconnected ?
udpconnect($oscope); # Connect to the Gqrx UDP port
$oscope->{connectstatus} = 1; # set Connected
$connectButton->SetLabel("Connected");
$connectButton->SetBackgroundColour(wxGREEN);
}
});
Wx::Event::EVT_BUTTON($self, $disconnectButton, sub {
my ($self, $event) = @_;
if($oscope->{connectstatus} == 1) { # Connected ?
$oscope->{plotstatus} = 0; # Stop plotting
$self->{timer}->Stop; # Stop the screen update
udpdisconnect($oscope); # Disconnect from the Gqrx UDP port
$oscope->{connectstatus} = 0; # Set Disconnected
$connectButton->SetLabel("Connect to Gqrx");
$connectButton->SetBackgroundColour(wxWHITE);
$plotButton->SetBackgroundColour(wxWHITE);
}
});
Wx::Event::EVT_BUTTON($self, $plotButton, sub { # Read/Plot first 600 samples
my ($self, $event) = @_; # (fill the fifo)
$plotButton->SetBackgroundColour(wxGREEN);
oscopeinit($oscope); # Redraw the screen background
$oscope->{color} = $oscope->{blue}; # Plot data in blue
$oscope->{fifostatus} = 0; # Reset fifo status
$oscope->{fifo} = []; # Reset fifo array
$oscope->{packetcounter} = 0; # Reset packet counter
fillqueue($oscope); # Read 600 samples from the Gqrx UDP port
drawcurve($oscope); # Plot the fifo array
saveaspngmemoryfile($oscope); # Save GD Image as PNG memory file
open my $fh, '<', \$oscope->{memoryfile}; # Rebuild the screen bitmap
$self->{bmp} = Wx::Bitmap->new(Wx::Image->new($fh, wxBITMAP_TYPE_PNG));
close $fh;
$self->{sbm}->SetBitmap($self->{bmp}); # Refresh screen
$oscope->{plotstatus} = 1; # Set Plotting started
$self->{timer}->Start(45); # 45 msec refresh timer
});
Wx::Event::EVT_BUTTON($self, $scaleButton, sub { # Change 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 { # Snapshot 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 { # Display update timer
my ($self, $event) = @_; # (all following packets)
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->{packet}); # Convert from 16bit "Network" format
$oscope->{packetcounter}++; # Update received 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); # Oldest sample out, newest sample in
}
oscopeinit($oscope); # Redraw the screen background
$oscope->{color} = $oscope->{blue}; # Plot data in blue
drawcurve($oscope); # Plot the fifo array
saveaspngmemoryfile($oscope); # Save GD Image as PNG memory file
open my $fh, '<', \$oscope->{memoryfile}; # Rebuild the screen bitmap
$self->{bmp} = Wx::Bitmap->new(Wx::Image->new($fh, wxBITMAP_TYPE_PNG));
close $fh;
$self->{sbm}->SetBitmap($self->{bmp}); # Refresh 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 green 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 filename
}
#
# 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, $color); # 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), $color); # 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->{packetcounter}, $color);
$oscope->{image}->string(gdSmallFont, 10, 20, $oscope->{slabel}, $color);
$oscope->{image}->string(gdSmallFont, 120, 20, $oscope->{scalefactor}, $color);
$oscope->{image}->string(gdSmallFont, 10, 30, $oscope->{plenlabel}, $color);
$oscope->{image}->string(gdSmallFont, 120, 30, $oscope->{packetlength}, $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 sample width
if($oscope->{fifo}[$i] > 0) { # Scale/offset the sample
$y = 300 - ($oscope->{fifo}[$i] / $oscope->{scalefactor});
}
elsif($oscope->{fifo}[$i] < 0) {
$y = 300 + ((abs($oscope->{fifo}[$i])) / $oscope->{scalefactor});
}
else {
$y = 300; # Default to 300("zero" value)
}
$oscope->{image}->line($lastx, $lasty, $i, $y, $color);
$lastx = $i;
$lasty = $y;
}
}
sub udpconnect {
my ($oscope) = @_;
$oscope->{socket} = IO::Socket::INET->new( # Open udp 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 most recent 600 samples
# enqueue and fillqueue fill the fifo for the first time(600 samples)
# shiftqueue handles all following samples
# topvalue is discarded and the new value is entered at the bottom
#
# 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 push in newest
my ($fifo, $sample) = @_;
shift $fifo;
push $fifo, $sample
}
sub fillqueue { # Read packets until 600 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 received 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
(application/x-perl, 2.6 KB)
#! /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 waveforms
# 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 waveform
$oscope->{socket} = IO::Socket::INET->new( # Open udp socket to GqrxRawAudioPlot.pl
PeerAddr => 'localhost',
PeerPort => 7355,
Proto => 'udp'
);
$oscope->{packet} = (); # Create a blank UDP 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 connection
# 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 waveform
my($oscope) = @_;
for(my $i=0; $i<360; $i++) {
my $rads = $i/57.32;
my $s = (sin($rads) * 100) + rand(50); # Sin wave 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;
}
}
rawaudio3.png
(image/png, 3.3 KB) - not displayed