sineko
2023-12-08
In my post from yesterday I forgot to mention one small project I also recently did at Recurse. We had an activity called "Impossible Stuff Day" where we were invited to come up with an "impossible" project and then work on it for one day. The idea of this is to challenge your own notions of what is and isn't possible.
My main project at the time was DaisyX7 and I thought to myself: can I run this synthesis engine inside the Linux kernel? It doesn't make sense (to me anyway) but I thought it was a fun idea.
The DX7 synthesis engine is based on sine waves so I figured the first step was to generate a sine wave. I looked a bit into how audio work in Linux (the foundation layer seems to be Alsa) but interacting with that from within the kernel looked complicated. In order to run code in the kernel, my first thought was eBPF because it is an in-vogue technology but thinking about it longer it made more sense to try and make a Linux kernel module. This would allow me to compile and run just my code and not the entire kernel every time. Unlike eBPF, however, my code could do whatever it wants.
Looking into kernel modules I stumbled into this amazing online book: The Linux Kernel Module Programming Guide ("LKMPG"). It is an introduction to writing kernel modules with examples you can compile and run yourself. In one of the early chapters, the book shows you how to create a Linux character device (/dev/foobar
).
This helped me narrow down what I was going to do next. I would make a kernel module sine.ko
which would generate raw PCM audio data that you could read from /dev/sine
. If you want to see the code now you can skip ahead to GitHub to look at the "sineko" project.
So why did I want to create raw PCM audio data? At first I thought of generating a WAV file but it seems those contain length information and I did not want to have t o worry about how to communicate to the kernel how much data it should generate. An endless sine wave seemed simpler. I could then read that data in userspace and send it back to the kernel using the play
command from SoX.
Thanks to the LKMPG book I could create my /dev/sine
device quite easily, but I then had to generate the sine wave data instead of "Hello world". This led to another obstacle. My limited experience with DSP so far has been on the Daisy platform. The Daisy hardware abstraction library provides audio callbacks that use floating point values (C float
s). This works out because the microcontroller (MCU) of the Daisy can do floating point operations. (It's an STM32H7.)
While the hardware I was targeting with my Linux kernel (amd64) also supports floating point instructions, the Linux kernel is not keen on letting you use them. If I understand correctly the reason for this is that the floating point unit (FPU) has its own register state, and by banning FPU instructions in the kernel, Linux avoids having to save and restore FPU register state on each context switch. There are ways around this but they looked very fidgety to me.
This opened an interesting new can of worms. I know from reading (for example here) that you can implement a sine function using a lookup-table. But how do I do that? How many entries should the table have? Luckily I found linux/fixp-arith.h
inside the kernel source code, which includes sine functions. The implementation contains a look-up table so this solved my problem of having to make my own.
With this I was able to have /dev/sine
generate raw PCM data for a sine wave at 220Hz. Not long after this I ran out of time for what I could do in one day.
While I don't see myself "finishing" this project, it was a lot of fun because I got to learn about Alsa, kernel modules, FPU's and fixed point arithmetic.
Tags: recurse