Hands-on Digital transmission with your soundcard

This article is part of the fundamentals of my real-world tutorial on digital communications using a cheap soundcard as the radio. If this notebook is interesting to you, check out the full tutorial!

09 - Transmitting custom data

In the previous notebook, we eventually accomplished to transmit real digital data over our audio channel. However, the payload was fixed and could not be adapted. In this notebook, we will extend the chain to read textual data from a UDP stream and according generate packets. At the receiver side, we will print out the received textual data.

First, let's start with some common imports:

In[1]:
# Source code has been redacted in online version
# Omitting 9 lines of source code
In[2]:
try:
    %load_ext tikzmagic
except ModuleNotFoundError:
    print ("Did not find tikzmagic. You will not be able to compile the tex code!")
In[3]:
# Source code has been redacted in online version
# Omitting 7 lines of source code

At the beginning, let's write a component that reads from a UDP connection and transforms the received textual data into payload bits. There, each transmitted character corresponds to 8 payload bits. The component sends the payload bits for each frame via its output signal.

In[4]:
# Source code has been redacted in online version
# Omitting 46 lines of source code

Afterwards, the TransmitSignal component receives the bits and puts them into our designed frame structure. Moreover, when no new payload data is available, the component just creates a dummy packet from a default payload array.

In[5]:
import queue

class TransmitSignal(TX):
    def __init__(self, environment, Ts, packeting, defaultPayload):
        super().__init__( environment)
        self._Ts = Ts
        self._packeting = packeting
        self._defaultPayload = defaultPayload
        
        
        self._rxQueue = queue.Queue()
        
        t = np.arange(-5*Ts, 5*Ts, 1/environment._samplerate)
        g_samples = get_filter('rc', Ts, rolloff=0.995)(t)
        
        self._filter = StatefulFilter(g_samples, [1])
        self._samplerate = environment._samplerate
        
    def receive(self, data):
        """called from the UDP source. Just store the payload in a queue"""
        self._rxQueue.put(data.copy())
    
    def __createFrame(self):
        # if data is available in the queue, use it. Otherwise, transmit a dummy data 'X'
        try:
            payload = self._rxQueue.get_nowait()
        except queue.Empty:
            payload = self._defaultPayload#np.array(int2bin(ord('X'), 8))
        packet = self._packeting.createPacket(payload)
        
        # perform BPSK modulation of the bits
        frame = 1 - 2.0*packet
        return frame
        
        
    def _generateSignal(self):
        Ns = int(self._Ts*self._samplerate)
        
        # get the BPSK symbols and filter them through the TX filter
        data = self.__createFrame()
        
        dataUps = np.zeros(int(Ns)*len(data))
        dataUps[::Ns] = data
        
        filtered = self._filter.filter(dataUps)
        
        tx = np.real(filtered * 0.2).astype(np.float32)

        return tx

These are the new components at the transmitter side. At the receiver, we only need a component to print out the data in text format. In order to do that, it groups the payload into groups of 8 bits and converts each group to a decimal number, which is in turn translated to the corresponding textual symbol. In addition, the component filters out the dummy frames that just contain a single 'X' (we will define later on that a "null"-frame just contains 'X' as the payload); instead it prints a simple '.' to stdout to see show that the transmission is still ongoing:

In[6]:
# Source code has been redacted in online version
# Omitting 13 lines of source code

We are now ready to setup the overall transmission chain. Pictorially, it consists of the following components, where the new components are marked in red:

In[7]:
%%tikz -l positioning, --size=800,240
\input{tex/09-1.tex}
No description has been provided for this image
In[8]:
def runTransmission(Fc, channelFunc):
    samplerate = 44100
    env = Environment(samplerate=samplerate)
    
    Ts = 1/(441)  # symbol duration
    Ns = int(Ts*samplerate)
    # 32 bit payload means byte per frame
    packeting = Packeting(np.array([1,1,1,0,1,0,0,0,1,0,0,0,1,0]), PayloadLength=32)  
    defaultPayload = np.array(int2bin(ord('X'), 8))
    
    source = UDPSource(env, nPayloadBytes=4)
    transmitter = TransmitSignal(env, Ts, packeting, defaultPayload=defaultPayload)
    upconversion = Upconversion(env, Fc=Fc)
    
    channel = channelFunc(env)
    
    downconversion = Downconversion(env, Fc=Fc, B=4/Ts, removeCarrier=True)
    timingRecovery = TimingRecovery(env, Ns)
    detectFrames = DetectFrames(env, packeting)
    printFrames = TextDataSink(env)
    plotEye = PlotEyeDiagram(env, Ts=Ts, figsize=(8,3))

    source.transmitsTo(transmitter)
    transmitter.transmitsTo(upconversion)
    upconversion.transmitsTo(channel)
    channel.transmitsTo(downconversion)
    downconversion.transmitsTo(timingRecovery)
    timingRecovery.transmitsTo(detectFrames, stream='samples')
    timingRecovery.transmitsTo(plotEye)
    detectFrames.transmitsTo(printFrames)
    
    env.run() 

Let's run it over the simulated channel:

In[11]:
runTransmission(15000, lambda env: SimulatedChannel(env, channelEffect=HighpassChannelEffect(gain=0.2)))
....

Hm, the eye diagram looks nice, but no data is actually showing up, only the status dots from the TextDataSink. That's clear, as we did not yet provide any input data via UDP stream. In order to do that, we can use different tools:

  • On Linux, simply open a terminal and run nc -u localhost 50009. Then type something and press return. The typed data will be printed out to this jupyter notebook.
  • On Windows (or Linux) use the provided tool udpsender.py. Simply run it in a terminal, type something and press return. The typed data will appear as the output of this jupyter notebook. Let's try again with some data actually being transmitted:
In[16]:
runTransmission(15000, lambda env: SimulatedChannel(env, channelEffect=HighpassChannelEffect(gain=0.2)))
.......Frame content: Hell
Frame content: o
.....Frame content: This
Frame content:  is 
Frame content: just
Frame content: est
..........Using Audio device default

Ah, great, now we get the actual data received. I typed Hello and This is just a test. at the transmitter side, which was almost succesfully received. Apparently one frame was lost (the one containing ' a t' between the frames containing 'just' and 'st '. Most probably, a bit in the header was corrupted and the decoding was not attempted. We will address this problem in the next notebook.

Now, let's try out the same thing with the audio channel:

In[18]:
runTransmission(15000, AudioChannel)
Using Audio device default
....................................Frame content: 1234
Frame content: 5678
Frame content: 9ABC
Frame content: DEFG
Frame content: HI
..............................................Stream Stopped 1
Stream Stopped 2

Yes, also with the audio channel it works (Though, here I used the cabled shortcut connection). Let's look at the received data: 1234 5678 9ABC HI. At the transmitter, I sent 1234 5678 9ABC DEFG HI (I artificially inserted the spaces for easier reading). Apparently, one frame was lost during the transmission. We believe the frame loss was due to a bit error in the PHY header, such that the PHY frame was not detected. We will address this problem in the next notebook.

Outlook

In this notebook, we extended our transmission chain to use variable payload, which can be supplied via a UDP network connection. As a problem we identified that some frames were lost during the transmission. In the following notebook we will address this problem by adding another control layer and repeating each frame several times.

This article is part of the fundamentals of my real-world tutorial on digital communications using a cheap soundcard as the radio. If this notebook is interesting to you, check out the full tutorial!

Copyright (C) 2025 - dspillustrations.com


DSPIllustrations.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com, amazon.de, amazon.co.uk, amazon.it.