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!

08 - Our first end-to-end data transmission

In the previous notebook, we designed a simple frame structure for aligning data into packets which can be decoded independently. The frame consisted of a header and a payload, where the payload was protected with a channel code and a checksum to ensure successful detection. We implemented the packet creating and decoding in a Packeting class.

In this notebook, we will use this class and all the previous blocks to create an end-to-end digital transmission using your soundcard. However, in this notebook, we will keep the transmitted data constant. We will extend the system to textual data received from a UDP stream in the next notebook.

First, let's have some standard imports:

In[1]:
import matplotlib
import matplotlib.pyplot as plt
%matplotlib ipympl

%load_ext autoreload
%autoreload 2

import numpy as np
import sys; sys.path.append('..')
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]:
from audioComms import Environment, TX
from audioComms.utils import get_filter, StatefulFilter, get_filter
from audioComms.buffers import RingBuffer
from audioComms.channels import AudioChannel, SimulatedChannel, IdentityChannelEffect, HighpassChannelEffect
from audioComms.plotting import PlotEyeDiagram
from audioComms.components import Component, Upconversion, Downconversion, TimingRecovery
from audioComms.packeting import Packeting

Then, let's define our transmitter. For now, we fix the payload it transmits to a given value of [1,1,1,1,0,0,0,0,1,1,0,0,1,0,1,0]. The principle is simple: Whenever the transmitter is asked to generate a transmit signal, it creates a frame with the constant payload given in the createFrame method.

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

At the receiver side, we create a component which reads a stream of bits and attempts to decode the packets from the stream. In order to do this, for each received bit the decoder is run on the last N bits, where N is the length of one PHY packet. If the decoder found a packet, the contained payload data is forwarded to the next component:

In[5]:
class DetectFrames(Component):
    def __init__(self, environment, packeting):
        super().__init__(environment, None)
        self._buffer = RingBuffer(packeting._packetLength, dtype=np.int8)        
        self._packeting = packeting
        
    def receive(self, data):
        bits = np.sign(data) == -1
        for b in bits:
            self._buffer.add(np.array([b]))
            d = self._buffer.data()
            packet = self._packeting.decodePacket(d)
            if packet is not None:
                self._send(packet)

As the data sink, we simple write a component that just prints out the bits in a received frame:

In[6]:
class PrintFrameBits(Component):
    def __init__(self, environment):
        super().__init__(environment, None)
        
    def receive(self, data): # Called for each received Frame
        print ("Received Frame bits:", data)

Already now, we can set up our transmission chain. Within the chain, we use all the blocks from the previous notebooks: Upconversion, Downconversion and Timing Recovery. Pictorially, we have the following connections, with the blocks relevant from this notebook marked:

In[7]:
%%tikz -l positioning, --size=800,240
\input{tex/08-1.tex}
No description has been provided for this image
In[8]:
# Source code has been redacted in online version
# Omitting 28 lines of source code

Let's try our system in the simulated channel. If everything works well, the PrintFrames component should print frames with exactly the payload we setup in the TransmitSignal component.

In[9]:
runTransmission(18000, lambda env: SimulatedChannel(env, channelEffect=HighpassChannelEffect(gain=0.2)))
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Using Audio device default

Great! After some settling of the internal gain parameters, the eye diagram stabilizes and the receiver prints out the expected payload! We have successfully created a rudimentary digital transmission system!

Let's try the system in the real audio channel:

In[10]:
runTransmission(8000, AudioChannel)
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
Received Frame bits: [1 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]

Yeah! It also works over the audio channel. After moving the microphone towards the speaker and letting the gains settle, the eye diagram stabilizes. Then, the frames are correctly detected!

Summary

In this notebook, we have successfully integrated our packet encoding and decoding into the transmission system. With this packeting mechanism, we are now able to really transmit digital information over our audio channel.

In the next notebook, we will extend this system to receive data from a UDP network stream, such that we can control the transmitted payload on the fly.

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.