Compare commits
	
		
			11 Commits
		
	
	
		
			ec8fc6bee3
			...
			5f6c5794cc
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						5f6c5794cc | |
| 
							
							
								
									
								
								 | 
						d5dd2d9603 | |
| 
							
							
								 | 
						e9fe05c4ea | |
| 
							
							
								
									
								
								 | 
						fc30717198 | |
| 
							
							
								
									
								
								 | 
						c4c6c79765 | |
| 
							
							
								
									
								
								 | 
						6a5ccbb818 | |
| 
							
							
								
									
								
								 | 
						844d0c1684 | |
| 
							
							
								
									
								
								 | 
						640c75038b | |
| 
							
							
								
									
								
								 | 
						ca411c57e1 | |
| 
							
							
								
									
								
								 | 
						5b547da40d | |
| 
							
							
								
									
								
								 | 
						4f90345a52 | 
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
cmake_minimum_required(VERSION 2.4)
 | 
					cmake_minimum_required(VERSION 2.4)
 | 
				
			||||||
project(SoftFM)
 | 
					project(SoftFM)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					find_package(Threads)
 | 
				
			||||||
find_package(PkgConfig)
 | 
					find_package(PkgConfig)
 | 
				
			||||||
find_package(ALSA REQUIRED)
 | 
					find_package(ALSA REQUIRED)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,6 +50,7 @@ include_directories(
 | 
				
			||||||
    ${EXTRA_INCLUDES} )
 | 
					    ${EXTRA_INCLUDES} )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
target_link_libraries(softfm
 | 
					target_link_libraries(softfm
 | 
				
			||||||
 | 
					    ${CMAKE_THREAD_LIBS_INIT}
 | 
				
			||||||
    ${RTLSDR_LIBRARIES}
 | 
					    ${RTLSDR_LIBRARIES}
 | 
				
			||||||
    ${ALSA_LIBRARIES}
 | 
					    ${ALSA_LIBRARIES}
 | 
				
			||||||
    ${EXTRA_LIBS} )
 | 
					    ${EXTRA_LIBS} )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -104,7 +104,7 @@ PilotPhaseLock::PilotPhaseLock(double freq, double bandwidth, double minsignal)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set valid signal threshold.
 | 
					    // Set valid signal threshold.
 | 
				
			||||||
    m_minsignal  = minsignal;
 | 
					    m_minsignal  = minsignal;
 | 
				
			||||||
    m_lock_delay = int(10.0 / bandwidth);
 | 
					    m_lock_delay = int(20.0 / bandwidth);
 | 
				
			||||||
    m_lock_cnt   = 0;
 | 
					    m_lock_cnt   = 0;
 | 
				
			||||||
    m_pilot_level = 0;
 | 
					    m_pilot_level = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										26
									
								
								NOTES.txt
								
								
								
								
							
							
						
						
									
										26
									
								
								NOTES.txt
								
								
								
								
							| 
						 | 
					@ -200,7 +200,31 @@ Radio4 reception improves from very bad to almost good.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
However, strong stations (radio3) sound slightly worse with the DIY antenna
 | 
					However, strong stations (radio3) sound slightly worse with the DIY antenna
 | 
				
			||||||
than with the basic DVB antenna.
 | 
					than with the basic DVB antenna.
 | 
				
			||||||
May be caused by clipping due to too strong signal from antenna.
 | 
					Theory: Distortion caused by clipping I/Q samples due to strong antenna signal.
 | 
				
			||||||
 | 
					No, that's not it. Reducing LNA gain or IF gain does not help much;
 | 
				
			||||||
 | 
					small DVB antenna still sounds better than DIY antenna.
 | 
				
			||||||
 | 
					Difference only clear in stereo mode.
 | 
				
			||||||
 | 
					Don't know what's going on here, maybe the DIY antenna is just not good.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IF processing
 | 
				
			||||||
 | 
					-------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Idea: Filter I/Q samples with 3rd order Butterworth filter
 | 
				
			||||||
 | 
					instead of 10th order FIR filter.
 | 
				
			||||||
 | 
					Implemented in branch "iirfilter".
 | 
				
			||||||
 | 
					Speed is unchanged.
 | 
				
			||||||
 | 
					Sound quality is not much changed for strong stations.
 | 
				
			||||||
 | 
					Sound quality is a bit worse for weak stations (at 1 MS/s).
 | 
				
			||||||
 | 
					Conclusion: not worthwhile.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Idea: Downsample I/Q samples to ~ 250 kS/s BEFORE quadrature detection
 | 
				
			||||||
 | 
					instead of immediately after detection. Would reduce amount of work in
 | 
				
			||||||
 | 
					I/Q filtering for same or higher FIR filter order.
 | 
				
			||||||
 | 
					Implemented in branch "filterif".
 | 
				
			||||||
 | 
					CPU time reduced by ~ 25%.
 | 
				
			||||||
 | 
					Sound quality very slightly worse.
 | 
				
			||||||
 | 
					Conclusion: not worthwhile.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Local radio stations
 | 
					Local radio stations
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								README
								
								
								
								
							
							
						
						
									
										1
									
								
								README
								
								
								
								
							| 
						 | 
					@ -38,6 +38,7 @@ For the latest version, see https://github.com/jorisvr/SoftFM
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The Osmocom RTL-SDR library must be installed before you can build SoftFM.
 | 
					The Osmocom RTL-SDR library must be installed before you can build SoftFM.
 | 
				
			||||||
See http://sdr.osmocom.org/trac/wiki/rtl-sdr for more information.
 | 
					See http://sdr.osmocom.org/trac/wiki/rtl-sdr for more information.
 | 
				
			||||||
 | 
					SoftFM has been tested successfully with RTL-SDR 0.5.3.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To install SoftFM, download and unpack the source code and go to the
 | 
					To install SoftFM, download and unpack the source code and go to the
 | 
				
			||||||
top level directory. Then do like this:
 | 
					top level directory. Then do like this:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								TODO.txt
								
								
								
								
							
							
						
						
									
										6
									
								
								TODO.txt
								
								
								
								
							| 
						 | 
					@ -1,8 +1,2 @@
 | 
				
			||||||
* (quality) solve issues with bad sound due to strong antenna signal
 | 
					 | 
				
			||||||
            (experiment with adaptive LNA gain or adaptive IF gain)
 | 
					 | 
				
			||||||
* (speedup) maybe replace high-order FIR downsampling filter with 2nd order butterworth followed by lower order FIR filter
 | 
					* (speedup) maybe replace high-order FIR downsampling filter with 2nd order butterworth followed by lower order FIR filter
 | 
				
			||||||
* (feature) implement RDS decoding
 | 
					* (feature) implement RDS decoding
 | 
				
			||||||
* (quality) consider FM demodulation with PLL instead of phase discriminator
 | 
					 | 
				
			||||||
* (quality) consider pulse-counting discriminator
 | 
					 | 
				
			||||||
* (speedup) consider downsampling of IF signal before FM detection
 | 
					 | 
				
			||||||
            (work in progress in branch "filterif")
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								main.cc
								
								
								
								
							
							
						
						
									
										12
									
								
								main.cc
								
								
								
								
							| 
						 | 
					@ -144,7 +144,7 @@ void adjust_gain(SampleVector& samples, double gain)
 | 
				
			||||||
 * This code runs in a separate thread.
 | 
					 * This code runs in a separate thread.
 | 
				
			||||||
 * The RTL-SDR library is not capable of buffering large amounts of data.
 | 
					 * The RTL-SDR library is not capable of buffering large amounts of data.
 | 
				
			||||||
 * Running this in a background thread ensures that the time between calls
 | 
					 * Running this in a background thread ensures that the time between calls
 | 
				
			||||||
 * to RtlSdrSource::get_samples() is very short. 
 | 
					 * to RtlSdrSource::get_samples() is very short.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
void read_source_data(RtlSdrSource *rtlsdr, DataBuffer<IQSample> *buf)
 | 
					void read_source_data(RtlSdrSource *rtlsdr, DataBuffer<IQSample> *buf)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -219,7 +219,8 @@ void usage()
 | 
				
			||||||
            "  -d devidx     RTL-SDR device index, 'list' to show device list (default 0)\n"
 | 
					            "  -d devidx     RTL-SDR device index, 'list' to show device list (default 0)\n"
 | 
				
			||||||
            "  -g gain       Set LNA gain in dB, or 'auto' (default auto)\n"
 | 
					            "  -g gain       Set LNA gain in dB, or 'auto' (default auto)\n"
 | 
				
			||||||
            "  -a            Enable RTL AGC mode (default disabled)\n"
 | 
					            "  -a            Enable RTL AGC mode (default disabled)\n"
 | 
				
			||||||
            "  -s ifrate     IF sample rate in Hz (default 1000000, min 900001)\n"
 | 
					            "  -s ifrate     IF sample rate in Hz (default 1000000)\n"
 | 
				
			||||||
 | 
					            "                (valid ranges: [225001, 300000], [900001, 3200000]))\n"
 | 
				
			||||||
            "  -r pcmrate    Audio sample rate in Hz (default 48000 Hz)\n"
 | 
					            "  -r pcmrate    Audio sample rate in Hz (default 48000 Hz)\n"
 | 
				
			||||||
            "  -M            Disable stereo decoding\n"
 | 
					            "  -M            Disable stereo decoding\n"
 | 
				
			||||||
            "  -R filename   Write audio data as raw S16_LE samples\n"
 | 
					            "  -R filename   Write audio data as raw S16_LE samples\n"
 | 
				
			||||||
| 
						 | 
					@ -355,8 +356,11 @@ int main(int argc, char **argv)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case 's':
 | 
					            case 's':
 | 
				
			||||||
                // NOTE: RTL does not suppor sample rate 900 kS/s or lower
 | 
					                // NOTE: RTL does not support some sample rates below 900 kS/s
 | 
				
			||||||
                if (!parse_dbl(optarg, ifrate) || ifrate <= 900000) {
 | 
					                // Also, max sampling rate is 3.2 MS/s
 | 
				
			||||||
 | 
					                if (!parse_dbl(optarg, ifrate) ||
 | 
				
			||||||
 | 
					                     (ifrate < 225001) || (ifrate > 3200000) ||
 | 
				
			||||||
 | 
					                     ((ifrate > 300000) && (ifrate < 900001))) {
 | 
				
			||||||
                    badarg("-s");
 | 
					                    badarg("-s");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										456
									
								
								pyfm.py
								
								
								
								
							
							
						
						
									
										456
									
								
								pyfm.py
								
								
								
								
							| 
						 | 
					@ -1,8 +1,18 @@
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Test lab for FM decoding algorithms.
 | 
					Test lab for FM decoding algorithms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Use as follows:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    >>> graw  = pyfm.lazyRawSamples('rtlsdr.dat', 1000000)
 | 
				
			||||||
 | 
					    >>> gtune = pyfm.freqShiftIQ(graw, 0.25)
 | 
				
			||||||
 | 
					    >>> bfir  = scipy.signal.firwin(20, 0.2, window='nuttall')
 | 
				
			||||||
 | 
					    >>> gfilt = pyfm.firFilter(gtune, bfir)
 | 
				
			||||||
 | 
					    >>> gbase = pyfm.quadratureDetector(gfilt, fs=1.0e6)
 | 
				
			||||||
 | 
					    >>> fs,qs = pyfm.spectrum(gbase, fs=1.0e6)
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
import types
 | 
					import types
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
import numpy.fft
 | 
					import numpy.fft
 | 
				
			||||||
| 
						 | 
					@ -11,6 +21,20 @@ import numpy.random
 | 
				
			||||||
import scipy.signal
 | 
					import scipy.signal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sincw(n):
 | 
				
			||||||
 | 
					    """Return Sinc or Lanczos window of length n."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    w = numpy.zeros(n)
 | 
				
			||||||
 | 
					    for i in xrange(n):
 | 
				
			||||||
 | 
					        if 2 * i == n + 1:
 | 
				
			||||||
 | 
					            w[i] = 1.0
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            t = 2 * i / float(n+1) - 1
 | 
				
			||||||
 | 
					            w[i] = numpy.sin(numpy.pi * t) / (numpy.pi * t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return w
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def readRawSamples(fname):
 | 
					def readRawSamples(fname):
 | 
				
			||||||
    """Read raw sample file from rtl_sdr."""
 | 
					    """Read raw sample file from rtl_sdr."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -364,3 +388,435 @@ def modulateAndReconstruct(sigfreq, sigampl, nsampl, fs, noisebw=None, ifbw=None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ampl1, phase1, noise1
 | 
					    return ampl1, phase1, noise1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def rdsDemodulate(d, fs):
 | 
				
			||||||
 | 
					    """Demodulate RDS bit stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    d       :: block of baseband samples or lazy baseband sample stream
 | 
				
			||||||
 | 
					    fs      :: sample frequency in Hz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Return (bits, levels)
 | 
				
			||||||
 | 
					    where bits is a list of RDS data bits
 | 
				
			||||||
 | 
					          levels is a list of squared RDS carrier amplitudes
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # RDS carrier in Hz
 | 
				
			||||||
 | 
					    carrier = 57000.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # RDS bit rate in bit/s
 | 
				
			||||||
 | 
					    bitrate = 1187.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Approximate nr of samples per bit.
 | 
				
			||||||
 | 
					    bitsteps = round(fs / bitrate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Prepare FIR coefficients for matched filter.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # The filter is a root-raised-cosine with hard cutoff at f = 2/bitrate.
 | 
				
			||||||
 | 
					    #   H(f) = cos(pi * f / (4*bitrate))   if f <  2*bitrate
 | 
				
			||||||
 | 
					    #   H(f) = 0                           if f >= 2*bitrate
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # Impulse response:
 | 
				
			||||||
 | 
					    #   h(t) = ampl * cos(pi*4*bitrate*t) / (1 - 4 * (4*bitrate*t)**2)
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    wlen = int(1.5 * fs / bitrate)
 | 
				
			||||||
 | 
					    w    = numpy.zeros(wlen)
 | 
				
			||||||
 | 
					    for i in xrange(wlen):
 | 
				
			||||||
 | 
					        t = (i - 0.5 * (wlen - 1)) * 4.0 * bitrate / fs
 | 
				
			||||||
 | 
					        if abs(abs(t) - 0.5) < 1.0e-4:
 | 
				
			||||||
 | 
					            # lim {t->0.5} {cos(pi*t) / (1 - 4*t**2)} = 0.25 * pi
 | 
				
			||||||
 | 
					            w[i] = 0.25 * numpy.pi - 0.25 * numpy.pi * (abs(t) - 0.5)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            w[i] = numpy.cos(numpy.pi * t) / (1 - 4.0 * t * t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Use Sinc window to reduce leakage.
 | 
				
			||||||
 | 
					    w *= sincw(wlen)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Scale filter such that peak output of filter equals original amplitude.
 | 
				
			||||||
 | 
					    w /= numpy.sum(w**2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    demod_phase = 0.0
 | 
				
			||||||
 | 
					    prev_a1 = 0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prevb = numpy.array([])
 | 
				
			||||||
 | 
					    pos   = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bits = [ ]
 | 
				
			||||||
 | 
					    levels = [ ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not isinstance(d, types.GeneratorType):
 | 
				
			||||||
 | 
					        d = [ d ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for b in d:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        n = len(b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # I/Q demodulate with fixed 57 kHz phasor
 | 
				
			||||||
 | 
					        ps  = numpy.arange(n) * (carrier / float(fs)) + demod_phase
 | 
				
			||||||
 | 
					        dem = b * numpy.exp(-2j * numpy.pi * ps)
 | 
				
			||||||
 | 
					        demod_phase = (demod_phase + n * carrier / float(fs)) % 1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Merge with remaining data from previous block
 | 
				
			||||||
 | 
					        prevb = numpy.concatenate((prevb[pos:], dem))
 | 
				
			||||||
 | 
					        pos   = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Detect bits.
 | 
				
			||||||
 | 
					        while pos + bitsteps + wlen < len(prevb):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Measure average phase of first impulse of symbol.
 | 
				
			||||||
 | 
					            a1 = numpy.sum(prevb[pos:pos+wlen] * w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Measure average phase of second impulse of symbol.
 | 
				
			||||||
 | 
					            a2 = numpy.sum(prevb[pos+bitsteps//2:pos+wlen+bitsteps//2] * w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Measure average phase in middle of symbol.
 | 
				
			||||||
 | 
					            a3 = numpy.sum(prevb[pos+bitsteps//4:pos+wlen+bitsteps//4] * w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Calculate inner product of first impulse and previous symbol.
 | 
				
			||||||
 | 
					            sym = a1.real * prev_a1.real + a1.imag * prev_a1.imag
 | 
				
			||||||
 | 
					            prev_a1 = a1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if sym < 0:
 | 
				
			||||||
 | 
					                # Consecutive symbols have opposite phase; this is a 1 bit.
 | 
				
			||||||
 | 
					                bits.append(1)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # Consecutive symbols are in phase; this is a 0 bit.
 | 
				
			||||||
 | 
					                bits.append(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Calculate inner product of first and second impulse.
 | 
				
			||||||
 | 
					            a1a2 = a1.real * a2.real + a1.imag * a2.imag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Calculate inner product of first impulse and middle phasor.
 | 
				
			||||||
 | 
					            a1a3 = a1.real * a3.real + a1.imag * a3.imag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            levels.append(-a1a2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if a1a2 >= 0:
 | 
				
			||||||
 | 
					                # First and second impulse are in phase;
 | 
				
			||||||
 | 
					                # we must be woefully misaligned.
 | 
				
			||||||
 | 
					                pos += 5 * bitsteps // 8
 | 
				
			||||||
 | 
					            elif a1a3 > -0.02 * a1a2:
 | 
				
			||||||
 | 
					                # Middle phasor is in phase with first impulse;
 | 
				
			||||||
 | 
					                # we are sampling slightly too early.
 | 
				
			||||||
 | 
					                pos += (102 * bitsteps) // 100
 | 
				
			||||||
 | 
					            elif a1a3 > -0.01 * a1a2:
 | 
				
			||||||
 | 
					                pos += (101 * bitsteps) // 100
 | 
				
			||||||
 | 
					            elif a1a3 < 0.02 * a1a2:
 | 
				
			||||||
 | 
					                # Middle phasor is opposite to first impulse;
 | 
				
			||||||
 | 
					                # we are sampling slightly too late.
 | 
				
			||||||
 | 
					                pos += (98 * bitsteps) // 100
 | 
				
			||||||
 | 
					            elif a1a3 < 0.01 * a1a2:
 | 
				
			||||||
 | 
					                pos += (99 * bitsteps) // 100
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # Middle phasor is zero; we are sampling just right.
 | 
				
			||||||
 | 
					                pos += bitsteps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (bits, levels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def rdsDecodeBlock(bits, typ):
 | 
				
			||||||
 | 
					    """Decode one RDS data block.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bits    :: list of 26 bits
 | 
				
			||||||
 | 
					    typ     :: expected block type, "A" or "B" or "C" or "C'" or "D" or "E"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Return (block, ber)
 | 
				
			||||||
 | 
					    where block is a 16-bit unsigned integer if the block is correctly decoded,
 | 
				
			||||||
 | 
					          block is None if decoding failed,
 | 
				
			||||||
 | 
					          ber is 0 if the block is error-free,
 | 
				
			||||||
 | 
					          ber is 1 if a single-bit error has been corrected,
 | 
				
			||||||
 | 
					          ber is 2 if decoding failed.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO : there are still problems with bit alignment on weak stations
 | 
				
			||||||
 | 
					# TODO : try to pin down the problem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Offset word for each type of block.
 | 
				
			||||||
 | 
					    rdsOffsetTable = { "A": 0x0fc, "B": 0x198,
 | 
				
			||||||
 | 
					                       "C": 0x168, "C'": 0x350,
 | 
				
			||||||
 | 
					                       "D": 0x1B4, "E": 0 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # RDS checkword generator polynomial.
 | 
				
			||||||
 | 
					    gpoly = 0x5B9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Convert bits to word.
 | 
				
			||||||
 | 
					    assert len(bits) == 26
 | 
				
			||||||
 | 
					    w = 0
 | 
				
			||||||
 | 
					    for b in bits:
 | 
				
			||||||
 | 
					        w = 2 * w + b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Remove block offset.
 | 
				
			||||||
 | 
					    w ^= rdsOffsetTable[typ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Calculate error syndrome.
 | 
				
			||||||
 | 
					    syn = w
 | 
				
			||||||
 | 
					    for i in xrange(16):
 | 
				
			||||||
 | 
					        if syn & (1 << (25 - i)):
 | 
				
			||||||
 | 
					            syn ^= gpoly << (15 - i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check block.
 | 
				
			||||||
 | 
					    if syn == 0:
 | 
				
			||||||
 | 
					        return (w >> 10, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Error detected; try all single-bit errors.
 | 
				
			||||||
 | 
					    p = 1
 | 
				
			||||||
 | 
					    for k in xrange(26):
 | 
				
			||||||
 | 
					        if p == syn:
 | 
				
			||||||
 | 
					            # Detected single-bit error in bit k.
 | 
				
			||||||
 | 
					            w ^= (1 << k)
 | 
				
			||||||
 | 
					            return (w >> 10, 1)
 | 
				
			||||||
 | 
					        p <<= 1
 | 
				
			||||||
 | 
					        if p & 0x400:
 | 
				
			||||||
 | 
					            p ^= gpoly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # No single-bit error can explain this syndrome.
 | 
				
			||||||
 | 
					    return (None, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RdsData(object):
 | 
				
			||||||
 | 
					    """Stucture to hold common RDS data fields."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pi  = None
 | 
				
			||||||
 | 
					    pty = None
 | 
				
			||||||
 | 
					    tp  = None
 | 
				
			||||||
 | 
					    ta  = None
 | 
				
			||||||
 | 
					    ms  = None
 | 
				
			||||||
 | 
					    af  = None
 | 
				
			||||||
 | 
					    di  = None
 | 
				
			||||||
 | 
					    pin = None
 | 
				
			||||||
 | 
					    pserv   = None
 | 
				
			||||||
 | 
					    ptyn    = None
 | 
				
			||||||
 | 
					    ptynab  = None
 | 
				
			||||||
 | 
					    rtext   = None
 | 
				
			||||||
 | 
					    rtextab = None
 | 
				
			||||||
 | 
					    time    = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tmp_afs = None
 | 
				
			||||||
 | 
					    tmp_aflen = 0
 | 
				
			||||||
 | 
					    tmp_afmode = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ptyTable = [
 | 
				
			||||||
 | 
					        'None',             'News',             
 | 
				
			||||||
 | 
					        'Current Affairs',  'Information',
 | 
				
			||||||
 | 
					        'Sport',            'Education',        
 | 
				
			||||||
 | 
					        'Drama',            'Cultures',
 | 
				
			||||||
 | 
					        'Science',          'Varied Speech',    
 | 
				
			||||||
 | 
					        'Pop Music',        'Rock Music',
 | 
				
			||||||
 | 
					        'Easy Listening',   'Light Classics M', 
 | 
				
			||||||
 | 
					        'Serious Classics', 'Other Music',
 | 
				
			||||||
 | 
					        'Weather & Metr',   'Finance',          
 | 
				
			||||||
 | 
					        "Children's Progs", 'Social Affairs',
 | 
				
			||||||
 | 
					        'Religion',         'Phone In',         
 | 
				
			||||||
 | 
					        'Travel & Touring', 'Leisure & Hobby',
 | 
				
			||||||
 | 
					        'Jazz Music',       'Country Music',    
 | 
				
			||||||
 | 
					        'National Music',   'Oldies Music',
 | 
				
			||||||
 | 
					        'Folk Music',       'Documentary',      
 | 
				
			||||||
 | 
					        'Alarm Test',       'Alarm - Alarm !' ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.pi is None:
 | 
				
			||||||
 | 
					            return str(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        s = 'RDS PI=%-5d' % self.pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        s += ' TP=%d' % self.tp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.ta is not None:
 | 
				
			||||||
 | 
					            s += ' TA=%d' % self.ta
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            s += '     '
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.ms is not None:
 | 
				
			||||||
 | 
					            s += ' MS=%d' % self.ms
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            s += '     '
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        s += ' PTY=%-2d %-20s' % (self.pty, '(' + self.ptyTable[self.pty] + ')')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.ptyn is not None:
 | 
				
			||||||
 | 
					            s += ' PTYN=%r' + str(self.ptyn).strip('\x00')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.di is not None or self.pserv is not None:
 | 
				
			||||||
 | 
					            s += '\n   '
 | 
				
			||||||
 | 
					            if self.di is not None:
 | 
				
			||||||
 | 
					                distr = '('
 | 
				
			||||||
 | 
					                distr += 'stereo' if self.di & 1 else 'mono'
 | 
				
			||||||
 | 
					                if self.di & 2:
 | 
				
			||||||
 | 
					                    distr += ',artificial'
 | 
				
			||||||
 | 
					                if self.di & 4:
 | 
				
			||||||
 | 
					                    distr += ',compressed'
 | 
				
			||||||
 | 
					                if self.di & 8:
 | 
				
			||||||
 | 
					                    distr += ',dynpty'
 | 
				
			||||||
 | 
					                distr += ')'
 | 
				
			||||||
 | 
					                s += ' DI=%-2d %-37s' % (self.di, distr)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                s += 45 * ' '
 | 
				
			||||||
 | 
					            if self.pserv is not None:
 | 
				
			||||||
 | 
					                s += ' SERV=%r' % str(self.pserv).strip('\x00')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.time is not None or self.pin is not None:
 | 
				
			||||||
 | 
					            s += '\n   '
 | 
				
			||||||
 | 
					            if self.time is not None:
 | 
				
			||||||
 | 
					                (day, hour, mt, off) = self.time
 | 
				
			||||||
 | 
					                dt = datetime.date.fromordinal(day + datetime.date(1858, 11, 17).toordinal())
 | 
				
			||||||
 | 
					                s += ' TIME=%04d-%02d-%02d %02d:%02d UTC ' % (dt.year, dt.month, dt.day, hour, mt)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                s += 27 * ' '
 | 
				
			||||||
 | 
					            if self.pin is not None:
 | 
				
			||||||
 | 
					                (day, hour, mt) = self.pin
 | 
				
			||||||
 | 
					                s += ' PIN=d%02d %02d:%02d' % (day, hour, mt)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                s += 14 * ' '
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.af is not None:
 | 
				
			||||||
 | 
					            s += '\n    AF='
 | 
				
			||||||
 | 
					            for f in self.af:
 | 
				
			||||||
 | 
					                if f > 1.0e6:
 | 
				
			||||||
 | 
					                    s += '%.1fMHz ' % (f * 1.0e-6)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    s += '%.0fkHz ' % (f * 1.0e-3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.rtext is not None:
 | 
				
			||||||
 | 
					            s += '\n    RT=%r' % str(self.rtext).strip('\x00')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def rdsDecode(bits, rdsdata=None):
 | 
				
			||||||
 | 
					    """Decode RDS data stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bits    :: list of RDS data bits
 | 
				
			||||||
 | 
					    rdsdata :: optional RdsData object to store RDS information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Return (rdsdata, ngroups, errsoft, errhard)
 | 
				
			||||||
 | 
					    where rdsdata is the updated RdsData object
 | 
				
			||||||
 | 
					          ngroup  is the number of correctly decoded RDS groups
 | 
				
			||||||
 | 
					          errsoft is the number of correctable bit errors
 | 
				
			||||||
 | 
					          errhard is the number of uncorrectable bit errors
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if rdsdata is None:
 | 
				
			||||||
 | 
					        rdsdata = RdsData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ngroup = 0
 | 
				
			||||||
 | 
					    errsoft = 0
 | 
				
			||||||
 | 
					    errhard = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    p = 0
 | 
				
			||||||
 | 
					    n = len(bits)
 | 
				
			||||||
 | 
					    while p + 4 * 26 <= n:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (wa, ea) = rdsDecodeBlock(bits[p:p+26], "A")
 | 
				
			||||||
 | 
					        if wa is None:
 | 
				
			||||||
 | 
					            errhard += 1
 | 
				
			||||||
 | 
					            p += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (wb, eb) = rdsDecodeBlock(bits[p+26:p+2*26], "B")
 | 
				
			||||||
 | 
					        if wb is None:
 | 
				
			||||||
 | 
					            errhard += 1
 | 
				
			||||||
 | 
					            p += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (wb >> 11) & 1:
 | 
				
			||||||
 | 
					            (wc, ec) = rdsDecodeBlock(bits[p+2*26:p+3*26], "C'")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            (wc, ec) = rdsDecodeBlock(bits[p+2*26:p+3*26], "C")
 | 
				
			||||||
 | 
					        if wc is None:
 | 
				
			||||||
 | 
					            errhard += 1
 | 
				
			||||||
 | 
					            p += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (wd, ed) = rdsDecodeBlock(bits[p+3*26:p+4*26], "D")
 | 
				
			||||||
 | 
					        if wd is None:
 | 
				
			||||||
 | 
					            errhard += 1
 | 
				
			||||||
 | 
					            p += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        errsoft += ea + eb + ec + ed
 | 
				
			||||||
 | 
					        ngroup += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Found an RDS group; decode it.
 | 
				
			||||||
 | 
					        typ  = (wb >> 12)
 | 
				
			||||||
 | 
					        typb = (wb >> 11) & 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # PI, TP, PTY are present in all groups
 | 
				
			||||||
 | 
					        rdsdata.pi  = wa
 | 
				
			||||||
 | 
					        rdsdata.tp  = (wb >> 10) & 1
 | 
				
			||||||
 | 
					        rdsdata.pty = (wb >> 5) & 0x1f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 0:
 | 
				
			||||||
 | 
					            # group type 0: TA, MS, DI, program service name 
 | 
				
			||||||
 | 
					            rdsdata.ta = (wb >> 4) & 1
 | 
				
			||||||
 | 
					            rdsdata.ms = (wb >> 3) & 1
 | 
				
			||||||
 | 
					            dseg = wb & 3
 | 
				
			||||||
 | 
					            if rdsdata.di is None:
 | 
				
			||||||
 | 
					                rdsdata.di = 0
 | 
				
			||||||
 | 
					            rdsdata.di &= ~(1 << dseg)
 | 
				
			||||||
 | 
					            rdsdata.di |= (((wb >> 2) & 1) << dseg)
 | 
				
			||||||
 | 
					            if rdsdata.pserv is None:
 | 
				
			||||||
 | 
					                rdsdata.pserv = bytearray(8)
 | 
				
			||||||
 | 
					            rdsdata.pserv[2*dseg]   = wd >> 8
 | 
				
			||||||
 | 
					            rdsdata.pserv[2*dseg+1] = wd & 0xff
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 0 and not typb:
 | 
				
			||||||
 | 
					            # group type 0A: alternate frequencies
 | 
				
			||||||
 | 
					            for f in ((wc >> 8), wc & 0xff):
 | 
				
			||||||
 | 
					                if f >= 224 and f <= 249:
 | 
				
			||||||
 | 
					                    rdsdata.tmp_aflen = f - 224
 | 
				
			||||||
 | 
					                    rdsdata.tmp_aflfmode = 0
 | 
				
			||||||
 | 
					                    rdsdata.tmp_afs = [ ]
 | 
				
			||||||
 | 
					                elif f == 250 and rdsdata.tmp_aflen > 0 and len(rdsdata.tmp_afs) < rdsdata.tmp_aflen:
 | 
				
			||||||
 | 
					                    rdsdata.tmp_aflfmode = 1
 | 
				
			||||||
 | 
					                elif f >= 1 and f <= 204 and rdsdata.tmp_aflen > 0 and len(rdsdata.tmp_afs) < rdsdata.tmp_aflen:
 | 
				
			||||||
 | 
					                    if rdsdata.tmp_aflfmode:
 | 
				
			||||||
 | 
					                        rdsdata.tmp_afs.append(144.0e3 + f * 9.0e3)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        rdsdata.tmp_afs.append(87.5e6 + f * 0.1e6)
 | 
				
			||||||
 | 
					                    if len(rdsdata.tmp_afs) == rdsdata.tmp_aflen:
 | 
				
			||||||
 | 
					                        rdsdata.af = rdsdata.tmp_afs
 | 
				
			||||||
 | 
					                        rdsdata.tmp_aflen = 0
 | 
				
			||||||
 | 
					                        rdsdata.tmp_afs = [ ]
 | 
				
			||||||
 | 
					                    rdsdata.tmp_aflfmode = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 1:
 | 
				
			||||||
 | 
					            # group type 1: program item number
 | 
				
			||||||
 | 
					            rdsdata.pin = (wd >> 11, (wd >> 6) & 0x1f, wd & 0x3f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 2:
 | 
				
			||||||
 | 
					            # group type 2: radio text
 | 
				
			||||||
 | 
					            dseg = wb & 0xf
 | 
				
			||||||
 | 
					            if rdsdata.rtext is None or ((wb >> 4) & 1) != rdsdata.rtextab:
 | 
				
			||||||
 | 
					                rdsdata.rtext = bytearray(64)
 | 
				
			||||||
 | 
					            rdsdata.rtextab = (wb >> 4) & 1
 | 
				
			||||||
 | 
					            if typb:
 | 
				
			||||||
 | 
					                rdsdata.rtext[2*dseg]   = (wd >> 8)
 | 
				
			||||||
 | 
					                rdsdata.rtext[2*dseg+1] = wd & 0xff
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                rdsdata.rtext[4*dseg]   = (wc >> 8)
 | 
				
			||||||
 | 
					                rdsdata.rtext[4*dseg+1] = wc & 0xff
 | 
				
			||||||
 | 
					                rdsdata.rtext[4*dseg+2] = (wd >> 8)
 | 
				
			||||||
 | 
					                rdsdata.rtext[4*dseg+3] = wd & 0xff
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 4 and not typb:
 | 
				
			||||||
 | 
					            # group type 4A: clock-time and date
 | 
				
			||||||
 | 
					            rdsdata.time = (((wb & 3) << 15) | (wc >> 1),
 | 
				
			||||||
 | 
					                            ((wc & 1) << 4) | (wd >> 12),
 | 
				
			||||||
 | 
					                            (wd >> 6) & 0x3f, (wd & 0x1f) - (wd & 0x20))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if typ == 10 and not typb:
 | 
				
			||||||
 | 
					            # group type 10A: program type name
 | 
				
			||||||
 | 
					            dseg = wb & 1
 | 
				
			||||||
 | 
					            if rdsdata.ptyn is None or ((wb >> 4) & 1) != rdsdata.ptynab:
 | 
				
			||||||
 | 
					                rdsdata.ptyn = bytearray(8)
 | 
				
			||||||
 | 
					            rdsdata.ptynab = (wb >> 4) & 1
 | 
				
			||||||
 | 
					            rdsdata.ptyn[4*dseg]    = (wc >> 8)
 | 
				
			||||||
 | 
					            rdsdata.ptyn[4*dsseg+1] = wc & 0xff
 | 
				
			||||||
 | 
					            rdsdata.ptyn[4*dseg+2]  = (wd >> 8)
 | 
				
			||||||
 | 
					            rdsdata.ptyn[4*dsseg+3] = wd & 0xff
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Go to next group.
 | 
				
			||||||
 | 
					        p += 4 * 26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (rdsdata, ngroup, errsoft, errhard)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue