My focus for this december adventure is to work on my live instrument and make progress on a piece (working title rain) that I’m writing for it and brass.
This might involve:
- more work on getting libpippi unit generators in better shape
- testing serial triggers with the relay system built into a passive mixer
- adding more channels to the passive mixer / relay system and finishing the firmware for it
- general astrid debugging and quality-of-life improvements
- also like, some composing probably! :-)
But we’ll see what happens.
I’m starting pretty late into December but I’ll update this page as I go.
Sunday December 31st
I ported the bells.cpp
firmware to a standalone linux
program. It uses parts of astrid, but is essentially just a small jack
app with an audio callback that looks essentally the same as the daisy
firmware. One of the first things I did was go from 2 voices to four
clusters of eight voices. It’s nice to have more breathing room on the
laptop. I wrote “a toolkit for live setups” in the lab journal.
Saturday December 30th
Moved the bells.cpp
pulsar firmware to the daisy pod. I
did it to be able to work on the train (though I ended up staying home)
more easily, but it also made me question the utility of the modular
rig, and doing synthesis on micrcontrollers more generally for
rain.
Plugging a lavaveler mic directly into the daisy pod is pretty neat, it’s nice to have the whole unit so self-contained. I sort of wish I could stream audio over USB and use the daisy as an audio interface. (Maybe it’s possible?)
Still, it’s becoming clearer I don’t really want to limit myself to the number of voices the daisy can support and I’ll probably go back to doing synthesis on the laptop.
Playing with the pod firmware also made it clear that simply streaming a delayed signal through the pulsar wavetables isn’t going to be enough. A sampling / looping frontend probably makes sense. A place to build up buffers that can be tapped for wavetables on demand. I’m not sure what the interface looks like. Something command-based maybe.
Friday December 29th
Started working on getting tests passing for the cython interface on top of the new pulsar ugens. Setting and updating tables needs a bit of work. (See lab notes.)
Thursday December 28th
Got a basic firmware going on the daisy patch and fixed a few more issues with the new pulsar osc. Added a new libpippi pulsar ugen type.
Tuesday December 26th
We made a family trip to the local surplus store and I found the perfect momentary switches for the passive mixer. I’m no whiz with finding components, I’m still just learning what things are called, but I’ve had a really difficult time finding suitable switches for the mixer.
My ideal would be the classic arcade style button hooked up to a microswitch, but I couldn’t find any double-pole, double-throw (DPDT so I’ve learned recently) variations. A single throw switch just breaks the connection. A double throw switch sends the connection to a different output. That’s the typical microswitch style I’ve seen. A double pole and double throw switch has two inputs and four outputs. That’s what I need for the mixer. It lets me wire up the relay, the slide toggle switch (also a DPDT style) and the momentary switch in series in a way that lets the relay alternate the flow to either output and have both the switches flip the direction of the connections, so that firing the relay or any of the switches all have the effect of reversing the connection order.
The idea is to wire the two outputs to different channels (really: different resonators) and be able to move a sound from one resonator to the other via serial triggers from a computer toggling the relay, use the slide switch to reverse the direction and keep it like that, and the momentary button lets me override the direction manually via button mashing. The mixer is wired so that each channel has two mixed inputs and two alternating outputs, so patching the inputs and outputs around will let me play with various routing configurations in an ad hoc way.
I found three different sources on ebay for footswitch-style momentary buttons that looked promising. They’re the sort you find in guitar pedals, but not the latching kind where you toggle and untoggle the switch with each press, they’re more like normal momentary pushbutton switches: press down to toggle, release to untoggle. Still, they all had the same latching-style resistance: a very satisfying clicking action that you kind of have to push through to fully toggle the switch. That makes it pretty difficult to button mash, and after trying all three sources and not finding anything better online I decided to just skip the momentary switches entirely.
These switches I found at axman are double-pole double-throw, they have the same sturdy panel-mount style as the footswitches, but a narrow plunger instead of the stubby footswitch type (I’ll have to find something to cover them with) with a wonderfully smooth action. No friction at all! They feel easy to press rapidly even with the pointy plungers. With some nice covers I think they’re going to be perfect. And just $1.50 a piece! The ones I could find online were way more expensive. (Still good to use as footswitches for a different project though.)
Monday December 25th
I updated the stack generator routines in libpippi to support the new
pulsar osc style. They just return normal lpbuffer_t
buffers now, but also require pointers to two arrays which store the
offsets and sizes of each table inside the buffer. Otherwise the
interface is similar: the first param is the number of tables in the
stack, then there are two new required params for the offsets and
lengths for each table (it’s up to the caller to make sure they’re the
right size) and finally a series of alternating constants for the
wavetype, and the requested size. The special WT_USER
and
WIN_USER
constants now also can be given a pointer to an
lpbuffer_t
instead of a size, and the table will be
included in the stack along with any built-in tables.
So, for example to create a stack of tables to be used for windows that consists of a 4096 element hanning window, a 128 element sine window, an arbitrarily sized window buffer (maybe filled with a microfragment of a fish vocalization? or anything) and a 1024 element triangle window:
#define num_tables 4
size_t onsets[num_tables] = {};
size_t lengths[num_tables] = {};
lpbuffer_t * fish = LPSoundFile.read("fish.wav");
lpbuffer_t * tables = LPWindow.create_stack(num_tables,
onsets, lengths,
WT_HANN, 4096,
WT_SINE, 128,
WT_USER, fish,
WT_TRI, 1024
);
Sunday December 24th
Speaking of off-by-one errors, I found one more in the new pulsar osc implementation! The phase-aligned triangle wavetable (phase-aligned to the sine wavetable) morphs as expected again.
Saturday December 23rd
I’m out of town visiting family for the holidays. Staring at the bricks above my parent’s fireplace helped me fix some lingering modulo issues with the new libpippi pulsar osc implementation.
The major bug ended up being in preparing the stacks of tables though – some wrong lengths made for wacky situations, but it’s all chugging along again as usual.
I should add some new built-in waveshapes meant for the pulsar osc. Like a triangle shape phase aligned with the zero crossings of the sine-like shapes would be handy for morphing purposes.
Some stack factory functions also make sense. It’s easy to break the osc with an off-by-one error, but it should also be pretty easy to verify the data going into the osc is correct (or at least makes sense) with some assertions checking for overflows and inconsistencies.
Thursday / Friday December 21st & 22nd
Sick days. Lotsa sleeping and no composing.
Wednesday December 20th
This morning I did some more work on the libpippi pulsar osc implementation. I’m still using the cython implemention of pulsar systhesis in pippi for composing. A few years ago I ported it to libpippi’s sndkit/soundpipe-style API, with lifecycles for unit generator-like abstractions taking a universal create/process/destroy shape.
It was fun getting it running on the electrosmith daisy, though I quickly went off on some side quests to optimize libpippi for embedded use and didn’t take the new pulsar osc much further after that.
I never felt good about the API I’d settled on either. The 2d pulsar osc works with a stack of tables. One stack for the pulsar wavetables, and another stack for the pulse windows. My first pass in libpippi added a special buffer stack type, which was just a thin container around an array of pointers to libpippi buffers.
Here’s an example of the create stage for a bank of four pulsar oscs with the older API:
for(int i=0; i < 4; i++) {
[i] = LPPulsarOsc.create();
oscs[i]->samplerate = SR;
oscs[i]->freq = 30.f;
oscs[i]->saturation = 1;
oscs[i]->pulsewidth = LPRand.rand(0.01, 1);
oscs[i]->wts = LPWavetable.create_stack(4, WT_SINE, WT_SQUARE, WT_TRI, WT_SINE);
oscs[i]->wins = LPWindow.create_stack(3, WIN_SINE, WIN_HANN, WIN_SINE);
oscs[i]->burst = LPArray.create_from(4, 1, 1, 0, 1);
oscs}
(LPWavetable and LPWindow are generators for built-in tables that
libpippi knows about. They create and populate a lpbuffer_t
struct like any other libpippi buffer.)
I’m working on a slightly more awkward, but I think simpler and more flexible variation on this that eliminates the special stack containers. Creating a pulsar osc now looks something like this:
for(i=0; i < 4; i++) {
[i] = LPPulsarOsc.create(4, wts, wt_onsets, wt_lengths, 1, win, win_onsets, win_lengths);
oscs[i]->samplerate = SR;
oscs[i]->freq = freqs[i];
oscs[i]->saturation = 1;
oscs[i]->pulsewidth = LPRand.rand(0.01, 1);
oscs[i]->burst = LPArray.create_from(4, 1, 1, 0, 1);
oscs}
Where wts
and win
are both just a normal
lpbuffer_t
struct that have all the buffers in the stack
arranged end-to-end. Then wt_onsets
is the maybe poorly
named (offsets is probably clearer) array whose size is equal to the
stack size, and wt_lengths
is another such array. The
onsets (offsets!) are the indexes in the buffer struct where each table
begins, and the lengths corresponding to each are stored as well.
I’ll probably make some helper functions that look like
LPWavetable.create_stack()
– maybe just adapt those to the
new API – but this also makes slicing one big buffer up pretty
straightforward. Using a ring buffer filled from the audio input on the
daisy for the wavetable stack for example, then filling the offset and
length tables as you like by maybe scanning across the buffer and
finding arbitrary zero crossing points.
Tuesday December 19th
I decided last night to go to the reembodied sound conference in February. My submission got rejected, but the festival looks really awesome and I’ve never seen Tudor’s Rainforest live before. Michel and I will probably be able to do some recording (maybe a show?) while I’m in town though, so I’m putting rain on pause, but continuing to develop the rain system – or something resembling it – to use for that.
I tried to figure out what a smaller & more portable four channel version of that might look like last night.
- One four channel battery powered amplifier unit with push tab terminals for speaker wire connections to transducers, and panel mount RCA for connection to a mixer.
- A four channel pulsar osc running on the daisy patch, which has four outs that can go direct to the amplifier unit and takes USB serial input for external control.
- Something running astrid hooked up via USB serial to the daisy patch and plugged into a zoom 4 channel audio interface with those outputs going to the daisy patch audio inputs.
The something running astrid will be my laptop at first, but I wonder if it could eventually be the pinephone instead.
I started working on the amplifier unit last night. It’s based around a dayton audio rechargable battery unit and two adafruit stereo amplifer boards.
The battery pack fits perfectly into an enclosure I got at the hardware store a while back. I got the RCA jacks mounted but ran into trouble figuring out how to mount the push tab terminal components. They need two mounting holes and then two fairly large rectangular gaps to be able to mount to the side of the enclosure. The plastic is tough and gave up on my plan to try to drill out a square. I’ll need to figure out a better way to cut into this enclosure or find a new one. I hope I can use it because it’s just barely big enough to fit everything inside and feels super sturdy when it’s all assembled.
Monday December 18th
Took a break from the instrument adventure to play with my pinephone today.
pinephone side quest
I’ve seen people talking about side quests in their adventure logs and I like that phrasing. My side quest this morning (and a bit last night) has been to try playing around with the pinephone pro / keyboard combo I splurged on a couple years ago. After some frustrating initial experiences I put it away for a long while. This time around has been more promising though.
Yesterday I tried out postmarketos with sxmo. I love sxmo. Alpine linux (and postmarketos from what I can tell) have given me all positive experiences too – except for what seems to be maybe a problem with the modem software? Text messages would usually make the phone shut off. The phone would just shut off without warning. Sometimes it would reboot, sometimes it would stay off. I think using the modem made it more likely to shut off but it was hard to tell.
Support for the pinephone pro seems better on arch arm so far though! It comes with sxmo preinstalled, though the configuration seems more barebones than the postmarketos setup. It hasn’t crashed so far. I tried one very noisy call with a friend. We could talk to each other but he said I sounded extremely distorted and he was getting a terrible echo. I’m curious if I’ll be able to get a bluetooth headset to work with it, but since it’s routing audio with pipewire more ridiculous scenarios like piping audio to my laptop could be possible, too…
I also sent some text messages with a bash script. That was pretty satisfying to do without having to mess around with a service like twilio or go through an email gateway or something like that. A part of me likes the idea of leaving the pinephone at home and using it as a relay: pushing messages back and forth over the web so I can check my messages and make calls from my laptop instead? Perhaps…
I’m most interested in it as a potential console interface for astrid, though. Pippi and astrid install on the phone as well, and the test suites seem to run the same (slower though!) as they do on my laptop which is promising. I wonder if I can even do the pulsar synthesis for this piece on the pinephone? It might make a good controller anyway.
Sunday December 17th
Lefse day today! This batch actually turned out thanks to help from my aunt.
imagemagick side quest
I started working on the mini-site for an Audiobulb compilation that’s been in the works for a number of years. The fun part is putting together the art from scans of the booklets and playing around with imagemagick pipelines.
From a directory of GIMP project files, flatten the layers and then
process them into a chunky dithered monochrome. The imagemagick pipeline
boosts brightness 35% and contrast 45%, then scales the image down to
19%, converts it to monochrome dithered with everyone’s favorte Floyd
Steinberg algorithm, and then resized without interpolation (the
-filter point
part near the end) to upscale back again to
the original size. The last step is really for printing the next version
of the booklets which are based on scans of the previous version. I’m
using the same pipeline without the very last bit for the web images so
far. They look suitably crappy but still readable.
#!/bin/bash
scale=19
blowout=35x45
restore=$(python -c "print(round(10000 / $scale))")
for f in ./*.xcf; do
filename=$(basename "$f" .xcf)
convert "$f" -flatten "$filename-bw.png"
done
mogrify -brightness-contrast ${blowout} -resize ${scale}% -monochrome -dither FloydSteinberg -filter point -resize ${restore}% *-bw.png
Saturday December 16th
I learned today that when the teensy does serial over USB, it can run closer to USB speeds! I’m fully tabling the internal LFO / sequencer idea in favor of the teensy being a dumb relay for external triggers over USB. Tested that everything worked the same when switching the current firmware from 9,600 baud to 230,400 baud (!!) this morning and I could already feel the difference in latency when keyboard mashing. It feels much more responsive to interact with, so that’s good.
I’ve also settled on a first pass of the little protocol I’ll use for serial communication. I’m only sending serial messages from the laptop to the microcontroller right now, but I plan to support listening for them as well, which will let me connect pippi to future homebrew hardware controllers and other external devices via serial bridges.
In the normal case, sending a trigger from astrid to the relays just looks like sending any ASCII byte over the wire. The values map to a given state for a given relay. There aren’t many states, just toggled or not for each of the relays because I’m skipping the internal sequencing for now so there’s plenty of room to grow there.
Alternately, astrid can send a special message struct over the wire, which can contain values of various types. The firmware will know if it should keep reading more bytes beyond the first if the sign bit is set, so the serial message structs are packed with that as the first member.
typedef struct lpserial_param_t {
unsigned int flag : 1;
unsigned int type : 31;
size_t id;
int group;
int device;
/* a union whose type is based on the type field */
;
lpserial_param_value_t value} lpserial_param_t;
This’ll all certainly change as I work on it, but that seems like a reasonable place to start.
I don’t actually have any use for sending anything except discrete triggers right now, but I’m excited to get that in place for when I extend the hardware into motor controls etc where I’ll want to be sending more than just toggles.
Friday December 15th
I haven’t hooked up astrid serial triggers again yet tonight, but it’s already pretty fun just keyboard mashing with one channel while astrid bloops.
The aux channel isn’t hooked up to anything so the relay being triggered acts like a mute switch. When I hold down a key the repeated letters do a little unstable roll.
This’ll be fun with all the channels and controls in place.
The relays are really pretty loud, but I’m happy about that. I might try to keep the relays exposed enough that I can muffle them by putting objects or materials on them, or amplify them by placing contact mics on them… donno :) lots of ideas, will have to keep screwing around and see.
I’m excited to get serial triggering wired up properly in astrid to try some syncronization: a ping through a cymbal with a short burst of relay clicks at the attack, maybe. A sustained chord broken by relay switching alternating between two cymbals while a third voice patters on through the wood resonator…
I’ve had this little prototype dingus attached to my desk for a few years now, made early on when I started working towards all this. It’s basically just a large piezo disk soldered onto a medium-sized solenoid which is attached to a bracket that can be clamped to a table. I’ve got some things hanging off of it by alligator clips. I’d really like to add things like this to the mileau, where there are small situated speakers (the prepared piezo soldered on top) embeded in objects attached to solenoids, motors, etc. Synchronized mechanical movement with resonances through the same materials.
Baby steps.
Thursday December 14th
Once again no coding! But I finished soldering up the connector and did a little testing of the full system. One channel of it anyway. :-)
Wednesday December 13th
No coding today, just finished adding the rest of the transistors to the relay board and started wiring up a new 15 pin connector that will sit between the passive mixer and the microcontroller board.
Tuesday December 12th
This isn’t a score, it’s just an outline for the extended version of rain. I’m shooting to do a compressed section of it for February: the end of the D2 section going into the start of the A prime section. In case you can’t read my scribbles, D2 is marked “big windchimes”, E (E1 to E2) is marked “arps” and then the return to A’ is marked “air/small sounds” like the original A section.
For the big windchimes into the arps section maybe I’m thinking about trying a gesture that passes a click around to larger pulses until the sound fills up into the arps section. I can’t do a full realization until I finish the rest of the mixer channels, but a gesture that looks something like a journey from unamplified relay clicks; expanded slowly by pinging the pulsar oscs through their resonators gradually; maybe starting with wood and growing into the cymbols, until it’s a big ringy resonant clatter going into the arp section. Or something like that. It’s an outline because these types of plans always change when listening gets involved.
Seems like a good excuse to dust off the firmware for the microcontroller that triggers the relays though. I’m just planning to start with four channels on the passive mixer with one relay per channel. The relays make a small (still, fairly loud) mechanical click when activated, but they also switch the output of the passive mixer channel from its main out to its aux out. The aux outs on each channel dangle, so this could mute the channel, but it could also route it to an alternate channel if the aux is patched in to the secondary input of a different channel. (Or really anything else I guess.)
For now I’m just focusing on the mechanical clicking. The microcontroller (a teensy 3.2) has some room to grow but I’ve only just soldered on enough components for two relays. There’s enough space for two more but there are also lots of open pins on the teensy, so as long as there aren’t power issues I should be able to add more relays to the same board later, too. I’d like to end up with ten in total, I think. Lots! Maybe four for the mixer and six to use with solenoids and motors, but I’ll have to play around and see what’s useful.
Since there’s so few things to control, and they’re all just toggling states on and off, I think I can leave my serial triggering scheme for astrid basically as-is. The way I’ve prototyped it so far is to make messages a single byte, and toggling states with bit flags.
In a prototype for a firmware controlling six solenoids, there were two sets of flags: the index of the solenoid and its state.
enum SOLE_SELECTION_FLAGS {
= 0,
SOLEALL = 1,
SOLE1 = 2,
SOLE2 = 4,
SOLE3 = 8,
SOLE4 = 16,
SOLE5 = 32,
SOLE6 };
enum SOLE_STATE_FLAGS {
= 1,
SOLE_STATE_TRIGGERED = 2,
SOLE_STATE_LFO_ENABLED = 4
SOLE_STATE_RND_ENABLED };
typedef struct sole_t {
char flags;
unsigned long trigger_start;
unsigned long lfo_start;
float lfo_period;
unsigned long button_press_start;
int lfo_triggers;
} sole_t;
Triggers in this prototype don’t map to state changes, they cause a
short toggle on and toggle off again. That makes more sense for a
solenoid than a relay, so if I stick with this approach I’ll add another
toggle
state type.
The other state that can be enabled and disabled per channel is the LFO. In this prototype it was an internal LFO that ran at a fixed rate. The idea is that the LFO is independent of the triggers (and toggles would be the same probably) so that you can engage the LFO, but still send arbitrary triggers on top if you like.
What’s the point of the LFO if I’m already scheduling triggers with a laptop that is certainly capable of acting like an LFO? It’s a small point, and maybe moot. There’s a limit to the serial baud rate, and I’d like to try driving the relays as fast as I can. The relays have a top speed too though, and I don’t know which is faster (my guess: the relay is faster than 9600 baud) so this might be a non-issue. I probably won’t bother implementing it in this next iteration but I’m going to keep the basic scheduling approach, so it’s possible to add back in again later if I want.
Leaving out the LFO for now keeps the messaging simple, too. I’ll have to add another byte or two if I want to support sending reasonably high resolution values as payloads to control the LFO speeds.
Monday December 11th
I added a couple new unit generators. The ‘mult’ generator does what you’d expect. It’s a utility module for multiplying two signals together. I also added a ‘tape’ ugen, which is variable speed sampler basically. Here it is with its speed param being driven by a sine ugen:
from unittest import TestCase
from pippi import dsp, fx, ugens
class TestUgens(TestCase):
def test_ugen_tape(self):
= dsp.read('tests/sounds/living.wav')
buf
= ugens.Graph()
graph 's1', 'sine', freq=100)
graph.add_node('s2', 'sine', freq=0.1)
graph.add_node('t', 'tape', buf=buf)
graph.add_node(
connect('s1.output', 't.speed', 0.1, 1)
graph.connect('s2.output', 's1.freq', 10, 2000)
graph.connect('t.output', 'main.output', mult=0.5)
graph.
= graph.render(10)
out = fx.norm(out, 1)
out 'tests/renders/ugens_tape.wav') out.write(
I’d like everything available as a ugen eventually (as an option at least) but I’ll probably put the ugen system aside again for the moment as it does mostly everything I want it to do for the piece I’m working on now. I’d also like to get back to testing the relay trigger system wired into the passive mixer I’ve been building, so maybe this’ll be on hold for a bit.
A thing I have in mind for this ugen system is to make it easier to write with feedback: making small graphs of ugens to use as per-note / per-event filters & waveshapers in the astrid instruments I’m working on for rain.
I’m still a bit unsure of a change to the API I made to support the tape ugen. All the unit generators have the same interface:
struct ugen_t {
void * params;
(*get_output)(ugen_t * u, int index);
lpfloat_t void (*set_param)(ugen_t * u, int index, void * value);
void (*process)(ugen_t * u);
void (*destroy)(ugen_t * u);
};
The params
member is just an opaque blob the ugen can do
as it pleases with internally. The rest of the members of a ugen struct
point to functions that implement the ugen. They all take the ugen
instance as an argument to have access to the params blob.
get_output
takes the index of an output and returns the current value of that output. All the ugens have amain
output, but they also can map any or all params to outputs, or provide a suite of outputs as they please. These additional outputs can be used when making connections in the graph. Like stacking banana cables.set_param
takes the param index and a void pointer to the value to set. Using a void pointer here is the change I made to support the tape ugen. It lets a param update swap in a new internal buffer. (In theory I guess if I made a pointer table ugen you could modulate the buffer selection with a sinewave or something but that goofery will wait for another day.)process
just does whatever per-frame processing the ugen might need to do, and then copies the latest values to all of its outputs.destroy
is just cleanup, freeing internal ugen resources as needed.
All of the graph functionality is implemented in cython with dict
lookup tables right now. When connections are made strings from python
are converted to enum ints for the indexes in the
get_output
and set_param
methods. All the
ugens define an enum of constants for output channels and params. (Like
USINEIN_FREQ
and USINEOUT_MAIN
)
I’m pretty happy with the python interfaces for this (the API for the test scripts above and below) but if I want to add ugens as first class citizens in astrid too, then I’ll need to move all of the graph stuff into libpippi. That way the graph doesn’t have to touch python at all but can still be orchestrated from python. It would be nice to have for rain but I’ll save it for later since there’s other stuff I’d like to do this month.
Sunday December 10th
This month I started working on the start of a unit generator system for pippi.
On Sunday I got graph connections working! Here’s some spaghetti thrown at the wall, just a hodge podge of some sine oscs wired up to modulate each other.
Occurs to me I should always include an amp param for ugens, so I don’t have to add a special VCA ugen every time I’d like to modulate the amplitude of something.
from unittest import TestCase
from pippi import dsp, fx, ugens
class TestUgens(TestCase):
def test_ugen_sine(self):
= ugens.Graph()
graph 's0b', 'sine', freq=0.1)
graph.add_node('s0a', 'sine', freq=0.1)
graph.add_node('s0', 'sine', freq=100)
graph.add_node('s1', 'sine')
graph.add_node('s2', 'sine')
graph.add_node(
connect('s0b.output', 's0a.freq', 0.1, 200)
graph.connect('s0a.output', 's0.freq', 100, 300)
graph.connect('s0.output', 's1.freq', 0.1, 1)
graph.connect('s0.output', 'main.output', mult=0.1)
graph.connect('s1.output', 's2.freq', 60, 100)
graph.connect('s2.output', 'main.output', mult=0.2)
graph.connect('s2.freq', 's0b.freq', 0.15, 0.5, 60, 100)
graph.
= graph.render(10)
out = fx.norm(out, 1).taper(0.1)
out 'tests/renders/ugens_sine.wav') out.write(