What if you could carry an entire song on a sheet of paper, with no Spotify, cloud storage, Wi-Fi, or cellular network?
That is the idea behind Paper Tunes, a QR-based physical music format combined with an off-grid LoRa music network.
I wanted to see whether it was possible to take an actual music file, compress it down far enough to make it practical, print that data onto paper as QR codes, and then reconstruct the song again.
And once I had the paper version working, I took the same compressed music data and sent it between two ESP32s using LoRa, creating a second completely offline way of sharing the song.
The paper version, though, is the part I find most interesting. A digital song has become a physical object.
The Problem: A Song Is Way Too Big for PaperI took a song that was about 2 minutes and 7 seconds long. As an MP3, it was approximately 2.9 MB.
Now consider a QR code. Even a maximum-size Version 40 QR code only holds roughly 3 KB of useful binary data, depending on the encoding and error-correction settings. So we're trying to do something ridiculous: Fit a 2.9 MB song into something that can hold only 3 KB.
If I simply tried to put the MP3 into QR codes, I'd need roughly a thousand of them. That doesn't sound much like a music cassette anymore. So the real challenge wasn't finding a bigger QR code. It was finding a much smaller way to represent the music.
Compressing Music Into Something TinyNormal file compression wasn't going to solve this.
An MP3 is already a lossy compressed format, so putting it through ZIP or another lossless compressor doesn't magically make it hundreds of times smaller.
I needed to represent the audio itself differently. That's where Meta's EnCodec comes in. EnCodec is a neural audio codec. Instead of treating the song as a normal audio file, an encoder converts the waveform into a compact representation made from discrete codes. A decoder can later turn those codes back into audio.
Think of it as translating a song into a very compact language that another copy of the codec understands.
Instead of putting the song itself on the paper, I put this compact representation on the paper.
For my first experiment, I used a target bandwidth of 3 kbps.
The result was surprisingly dramatic: 2.9 MB MP3 to approximately 21 KB of latent data
That's roughly a thousand-fold reduction.
But 21 KB is still much larger than one QR code.
So I had solved the first problem, but not the paper problem yet.
I also experimented with a lower 1.5 kbps setting. It produced an even smaller representation, but the decoded audio was heavily degraded.
The 3 kbps version sounded surprisingly good considering how aggressively it had been compressed, so I used that as the starting point.
The encoder essentially uses:
model.set_target_bandwidth(3.0)You can experiment with different bandwidths. Lower values give you smaller representations, while higher values generally preserve more audio quality.
Step 2: What If One QR Code Isn't Enough?At this point I had a roughly 21 KB file. A QR code still holds only around 3 KB. I tried to squeeze more capacity out of a single code. But no matter what I tried, it couldn't fit more than about 3.3 KB of data.
So I stopped trying to make one QR code do something it wasn't designed to do. Instead, I thought, "Why not use eight QR codes?"
Step 3: The Paper Music CassetteMy approximately 21 KB compressed song could be divided into about eight pieces, each small enough to fit into a QR code.
So I split the data into eight chunks:
QR 1 > ID 1 + music dataQR 2 > ID 2 + music dataQR 3 > ID 3 + music data, and so on...
Each QR code begins with a small identifier. The decoder doesn't have to know which QR code you scanned first. It can simply read the IDs and put the pieces back into the correct order.
I wanted the result to feel more like a physical music format rather than just a sheet covered in barcodes.
So I took inspiration from something that existed long before streaming: double sided physical media.
Some vinyl records had to be flipped over halfway through the listening experience. So I decided to do the same thing with my QR cassette.
I did the same thing, 4 QR codes on side A and 4 on side B.
I wrote a Python script that takes the compressed music.raw file and:
- Splits it into eight chunks.
- Adds an ID to each chunk.
- Generates the QR codes.
- Arranges four QR codes on Side A.
- Arranges four QR codes on Side B.
- Produces printable images.
An important thing is the quiet zone around each QR code. The codes need a clear area around them so scanners can reliably detect their boundaries. Finally, I printed both sides at full size on ordinary paper.
And this is the part that still feels crazy. The song now physically exists on the paper. Not a link to the song. Not a QR code that points to a streaming service. The QR codes contain the compressed data needed to reconstruct the song. The paper itself is the storage medium!
Step 5: Reading the Paper Back Into a SongOf course, printing the song is only half the experiment.
The real test is whether I can get it back.
The receiving program is given the two cassette images:
Side A → QR 1, 2, 3, 4
Side B → QR 5, 6, 7, 8It detects the QR codes and reads their payloads.
Each payload contains its identifier, so the program can determine which chunk is which.
It then:
- Reads all eight QR codes.
- Extracts their IDs.
- Sorts the chunks.
- Removes the identifier bytes.
- Joins the eight chunks.
- Reconstructs the original
music.raw. - Sends that data through the EnCodec decoder.
- Produces the reconstructed audio.
If the paper can physically carry a song, could I also transfer the same compressed representation from one place to another without the internet?
That led me to LoRa.
LoRa is a long-range, low-power radio technology designed for sending relatively small amounts of data.
For this experiment, I used two ESP32 development boards and two REYAX RYLR998 LoRa modules.
Why Reyax Lora module?
Reyax makes great communiccation modules that are easy to program and use for hobbyists. In this case, I'm using two Reyax RYLR998 modules. You can purchase them from:
Amazon: Purchase Link
Other distributors: Purchase Link
One ESP32 stores the compressed music file. The other receives it. The interesting part is that there is no router, cloud service, or cellular network in between them. The two LoRa modules communicate directly.
And because I'm sending the approximately 21 KB compressed representation rather than the original 2.9 MB MP3, the experiment becomes much more realistic. The paper and LoRa systems are therefore two different physical forms of the same idea: store the music locally, compress it aggressively, and remove the internet from the equation.
Step 7: Wiring the LoRa HardwareThe RYLR998 communicates with the ESP32 over UART.
My connections are:
RYLR998 -> ESP32
VDD -> 3.3V
GND -> GND
TXD -> ESP32 RX
RXD -> ESP32 TX
The ESP32 communicates with the RYLR998 using AT commands, while the LoRa module takes care of the actual radio communication.
Make sure you use the appropriate RYLR998 frequency version and configure the radio according to the regulations in your region.
Step 8: Sending the Music Over LoRaThe transmitting ESP32 stores music.raw in its LittleFS filesystem.
When the receiver sends a GET request, the transmitter starts the transfer. It first sends metadata describing the file. Then the file is broken into small packets. The transmitter waits for an ACK before continuing. If an acknowledgement doesn't arrive, it retries that packet. Finally, it sends an END message.
This creates a simple reliable file-transfer system on top of the LoRa connection.
The important thing is that we're not trying to send a several-megabyte MP3 through this slow radio link.
We're sending the tiny representation created specifically for this project.
Step 9: Reconstructing the SongThe second ESP32 receives the packets and reconstructs them into:
received.rawFor every packet it receives, it checks the packet number, decodes the Base64 data, writes it to the file, and sends an acknowledgement.
Once the transfer is complete, it checks that the number of bytes received matches the original file size.
The ESP32 then sends the reconstructed binary file to a PC over USB.
The Python program takes over from there and decodes the EnCodec representation back into audio.
For this proof of concept, I deliberately kept the PC involved rather than building a dedicated Android application first. That makes it easier to experiment with the underlying storage and communication system.
Step 10: The SoftwareThe Python receiver can work with both versions of the project:
QR mode
Reads the two paper cassette images, reconstructs the eight chunks, and decodes the music.
LoRa mode
Receives the reconstructed file from the ESP32 over USB and decodes it.
The required Python packages are:
pip install pyserial numpy soundfile torch encodec opencv-pythonThen run:
python pc_receiver.pyThe program provides a simple menu for selecting either LoRa reception or QR cassette decoding.
The project files include the encoder, decoder, QR generation code, transmitter sketch, receiver sketch, and PC receiver.
The Final ResultThe finished system gives me two ways to move the same song without relying on online infrastructure.
PaperSong → EnCodec → 21 KB → 8 QR codes → paper → scan → decode → song
RadioSong → EnCodec → 21 KB → LoRa → ESP32 → PC → decode → song
There is something satisfying about taking something as intangible as a digital song and turning it into an object you can put on a desk.
You can print it. You can fold it. You can hand it to someone.
And, if the experiment works, you can scan it and hear the song that was hiding inside the paper all along.
What's Next?This is very much a proof of concept, and there are plenty of directions I'd like to take it.
The obvious ones are better low-bitrate audio quality, more efficient QR encoding, stronger recovery when a printed code is damaged, and faster LoRa transfers.
I'd also like to build a proper Android application so that the entire process can happen directly on a phone, without the PC.







Comments