Compare commits

...

4 commits

Author SHA1 Message Date
d221755ef1 Added notes and todos 2021-05-14 19:40:59 -04:00
d7100d42fd Assert positive port 2021-05-14 19:39:58 -04:00
bbad0a0a70 Added Qt pro file 2021-05-14 19:39:47 -04:00
b2dbe95d2f Added gnuradio flowgraph in case people are curious.
Added the start script which controls the flow of the server application.
Still need to add functionality to listen to audio in the client side.
They will need to listen to serverip:6003
2019-06-02 10:46:00 -04:00
6 changed files with 2001 additions and 51 deletions

113
Radio Tuner Notes.md Normal file
View file

@ -0,0 +1,113 @@
# Radio Tuner Notes
This is how frequencies in the FM spectrum are delegated
| Label | Frequency |
| --------- | --------- |
| Min | 87.5 MHz |
| Max | 108.0 MHz |
| Step Size | 100 KHz |
Since we cannot represent these values in the UI (using the dial element), we will instead use these values and multiply by $10^5$
| Label | Frequency |
| --------- | --------- |
| Min | 875 |
| Max | 1080 |
| Step Size | 1 |
## Message Format
I think I would like to implement the GNU-Radio Script as a client server application that binds to some `localhost` port and can operate with the following messages
| Message Format | Description |
| -------------- | ---------------------------------------------------------- |
| ?VER; | Outputs the current version of the server |
| ?STATUS; | Gets the status of the program |
| ?FREQ; | Queries the frequency that the program is currently set at |
| :FREQ=%f; | Sets the frequency according to the float %f |
| :QUIT; | Shuts down the server |
To test out the server
```bash
telnet localhost 65432
```
## BUGS/ENHANCEMENTS`
**Bug Fix:** Investigate different event handlers because the one that's currently used will send too many frequency set messages over to the server. (Maybe one that's like "when you release the dial...")
**Feature Request:** Favorite radio stations section
**Enhancement:** Listen again once the client closes the connection **[FIXED]**
**Enhancement:** Allow multiple connections to the socket and have the server handle it appropriately
**Enhancement:** Probably should more intelligently receive the number of bytes as opposed to setting it to an arbitrarily high number.
**Enhancement:** Maybe use local sockets? (Nah, this will prevent me from having the Pi contain the radio and controlling from my laptop) **[INVALID]**
**Bug Fix:** Send out full number instead of scientific notation. **[Not Needed]**
**Bug Fix:** Include appropriate error messages for when the client can't connect.
**Bug Fix:** Kick the client back to the connect screen when it disconnects.
## Connect Screen
In this screen the user can input the `IP address` and `Port` that the SDR server is hosted on.
From there, it will confirm if it can connect successfully and if so, it will present the dial screen.
There should be some sort of notification if the client loses connection. Maybe popping them back up into the connect screen.
## Sending Audio Over the Network
This requires the UDP Sink box in GNU Radio. Make sure you set the type to `float`.
To hear the output do the following command:
```bash
netcat -ul address port | aplay -c num_channels -t raw -r sample_rate -f FLOAT_LE -
```
This tells netcat to listen to UDP on localhost:port and play it with ALSA with the above settings
My current configuration
| Name | Value |
| ------------------ | ------------- |
| Address | x.x.x.x |
| Port | 7654 |
| Number of Channels | 1 |
| Sample Rate | 48000 |
| Sample Type | Float |
| Endiness | Little Endian |
### Compressing Audio
Command to convert the raw input stream into a ogg stream in standard output with a bitrate limited to 32 kb/s
```bash
ffmpeg -f f32le -ar 48k -ac 1 -i pipe:0 -f ogg -ab 32k pipe:1
```
## TODO
So we should have the GNURadio program still act as a server to be able to change channels, but this time write the raw audio to a file descriptor from which ffmpeg grabs and compresses it to an ogg stream that gets transmitted over the network.
Current command:
```bash
ffmpeg -f f32le -ar 48k -ac 1 -i pipe:0 -f ogg -ab 32k pipe:1 < raw_audio_pipe | nc -l 127.0.0.1 6003
```
Grab audio from named pipe that gnuradio is outputting to, then convert it to ogg and compress it with a 32k bitrate and then send that audio over the network.
**TODO:** Convert the gnuradio blocks program to be a server again

16
RadioTuner.pro Normal file
View file

@ -0,0 +1,16 @@
TEMPLATE = app
TARGET = RadioTuner
INCLUDEPATH += .
# The following define makes your compiler warn you if you use any
# feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# Input
HEADERS += client.h
SOURCES += client.cpp main.cpp
RESOURCES += qml.qrc
QT += network qml quick

View file

@ -14,7 +14,6 @@ Client::~Client() {
}
void Client::connect(QString hostname, int port) {
assert (port > 0);
socket->connectToHost(hostname, port);
socket->waitForConnected();
std::cout << "Connected" << std::endl;

1774
radio_tuner.grc Normal file

File diff suppressed because it is too large Load diff

24
start Executable file
View file

@ -0,0 +1,24 @@
#!/bin/bash
compress=1
function cleanup() {
echo "Shutting Down"
rm ./raw_audio_pipe
kill $child_pid
}
mkfifo ./raw_audio_pipe
# Run Radio Server Program in Background
./tuner.py &
child_pid=$!
if [ $compress -eq 1 ]
then
ffmpeg -f f32le -ar 48k -ac 1 -i pipe:0 -f ogg -ab 32k pipe:1 < ./raw_audio_pipe | nc -kl 127.0.0.1 6003
else
tail -f raw_audio_pipe | nc -kl 127.0.0.1 6003
fi
cleanup

122
tuner.py
View file

@ -3,12 +3,11 @@
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Sun May 12 22:10:45 2019
# Generated: Sat Jun 1 12:54:09 2019
##################################################
from gnuradio import analog
from gnuradio import audio
from gnuradio import blocks
from gnuradio import eng_notation
from gnuradio import filter
@ -21,6 +20,7 @@ import time
import socket
import re
class top_block(gr.top_block):
def __init__(self):
@ -29,15 +29,17 @@ class top_block(gr.top_block):
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 1536000
self.frequency = frequency = 96900000
self.samp_rate = samp_rate = 2.2e6
self.freq = freq = 101.5e6
self.audio_rate = audio_rate = 48000
self.audio_interp = audio_interp = 4
##################################################
# Blocks
##################################################
self.rtlsdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + '' )
self.rtlsdr_source_0.set_sample_rate(samp_rate)
self.rtlsdr_source_0.set_center_freq(frequency, 0)
self.rtlsdr_source_0.set_center_freq(freq, 0)
self.rtlsdr_source_0.set_freq_corr(0, 0)
self.rtlsdr_source_0.set_dc_offset_mode(2, 0)
self.rtlsdr_source_0.set_iq_balance_mode(2, 0)
@ -48,22 +50,25 @@ class top_block(gr.top_block):
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
self.low_pass_filter_0 = filter.fir_filter_ccf(4, firdes.low_pass(
1, samp_rate, 500000, 60000, firdes.WIN_KAISER, 6.76))
self.blocks_udp_sink_0 = blocks.udp_sink(gr.sizeof_float*1, '127.0.0.1', 7654, 1472, True)
self.blocks_throttle_0 = blocks.throttle(gr.sizeof_gr_complex*1, samp_rate,True)
self.rational_resampler_xxx_0 = filter.rational_resampler_ccc(
interpolation=audio_rate * audio_interp,
decimation=int(samp_rate),
taps=None,
fractional_bw=None,
)
self.blocks_file_sink_0 = blocks.file_sink(gr.sizeof_float*1, 'raw_audio_pipe', False)
self.blocks_file_sink_0.set_unbuffered(False)
self.analog_wfm_rcv_0 = analog.wfm_rcv(
quad_rate=384000,
audio_decimation=8,
quad_rate=audio_rate * audio_interp,
audio_decimation=audio_interp,
)
##################################################
# Connections
##################################################
self.connect((self.analog_wfm_rcv_0, 0), (self.blocks_udp_sink_0, 0))
self.connect((self.blocks_throttle_0, 0), (self.low_pass_filter_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.analog_wfm_rcv_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_throttle_0, 0))
self.connect((self.analog_wfm_rcv_0, 0), (self.blocks_file_sink_0, 0))
self.connect((self.rational_resampler_xxx_0, 0), (self.analog_wfm_rcv_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.rational_resampler_xxx_0, 0))
def get_samp_rate(self):
return self.samp_rate
@ -71,20 +76,64 @@ class top_block(gr.top_block):
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate, 500000, 60000, firdes.WIN_KAISER, 6.76))
self.blocks_throttle_0.set_sample_rate(self.samp_rate)
def get_frequency(self):
return self.frequency
def get_freq(self):
return self.freq
def set_frequency(self, frequency):
self.frequency = frequency
self.rtlsdr_source_0.set_center_freq(self.frequency, 0)
def set_freq(self, freq):
self.freq = freq
self.rtlsdr_source_0.set_center_freq(self.freq, 0)
def get_audio_rate(self):
return self.audio_rate
def set_audio_rate(self, audio_rate):
self.audio_rate = audio_rate
def get_audio_interp(self):
return self.audio_interp
def set_audio_interp(self, audio_interp):
self.audio_interp = audio_interp
def is_valid_frequency(frequency):
return frequency >= 87500000 and frequency <= 108000000
def parse_commands(conn, tb, commands):
quit = False
for command in commands:
if command == "":
continue
elif command[0] == '?': # Query Section
if command[1:] == "VER":
conn.sendall("Version 0.1\n")
elif command[1:] == "FREQ":
conn.sendall(str(tb.get_freq()) + '\n')
else:
print("UNKNOWN COMMAND: ", command)
elif command[0] == ':': # Setter Section
if command[1:5] == "FREQ":
print("FREQUENCY SET")
frequency = float(command[6:])
if is_valid_frequency(frequency):
tb.set_freq(frequency)
else:
print("ERROR: Invalid Frequency ", frequency)
elif command[1:] == "QUIT":
print("QUITTING")
quit = True
else:
print("UNKNOWN COMMAND: ", command)
else:
print("UNKNOWN COMMAND", command)
# Return state back to application
return {'quit': quit}
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.start()
HOST = '127.0.0.1' # Loopback Address (Localhost)
PORT = 65432 # Semi-Random Port that is non-priviledged (> 1023)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -103,34 +152,9 @@ def main(top_block_cls=top_block, options=None):
commands = re.split(";", data)
if data == "":
break
for command in commands:
if command == "":
continue
elif command[0] == '?':
# Query Section
if command[1:] == "VER":
conn.sendall("Version 0.1\n")
elif command[1:] == "FREQ":
conn.sendall(str(tb.get_frequency()) + '\n')
else:
print("UNKNOWN COMMAND: ", command)
elif command[0] == ':':
# Setter Section
if command[1:5] == "FREQ":
print("FREQUENCY SET")
frequency = float(command[6:])
if frequency >= 87500000 and frequency <= 108000000:
tb.set_frequency(frequency)
else:
print("ERROR: Invalid Frequency ", frequency)
elif command[1:] == "QUIT":
print("QUITTING")
state = parse_commands(conn, tb, commands)
if state['quit']:
quit = True
else:
print("UNKNOWN COMMAND: ", command)
else:
print("UNKNOWN COMMAND", command)
conn.close()
s.close()