double densed uncounthest hour of allbleakest age with a bad of wind and a barrel of rain
double densed uncounthest hour of allbleakest age with a bad of wind and a barrel of rain is an in-progress piece for resonators and brass. I’m keeping a composition log here as I work on it.
There are sure to be many detours. Getting it in shape might involve:
- more work on libpippi unit generators & integrating streams into astrid
- testing serial triggers with the solenoid tree & relay system built into the passive mixer
- finishing the passive mixer / relay system and firmware (what to do about the enclosure!?)
- general astrid debugging and quality-of-life improvements…
- composing maybe?
Friday May 31st
Param handling is coming along. I decided not to encode messages at all. Instead I’m keeping everything in a string format until the instrument gets the update message in the message thread. At that point it calls a param map callback defined on the instrument with the string versions of each of the param names and values and lets the instrument decide how to do the decoding.
It’s not the most elegant thing in the world. I mapped out some
params over lunch today and it’s already getting kinda wordy. The
extract_<type>_from_token
functions decode the string
representations of the values into the appropriate type, and the
astrid_instrument_set_param_<type>
functions set the
raw decoded value (usually a float, but can be an int, float list or
pattern buffer, as well as other new types in the future) into the LMDB
session where they can be safely read back from the audio thread without
blocking.
It’s simple enough for now though and I think the eventual python version of it can be more of a simple map of names to types, maybe with optional scaling or etc.
the param map callback
int param_map_callback(void * arg, char * keystr, char * valstr) {
* instrument = (lpinstrument_t *)arg;
lpinstrument_t * ctx = (localctx_t *)instrument->context;
localctx_t float val_f = 0;
uint32_t val_i32 = 0;
= {1,{1}};
lppatternbuf_t val_pattern
if(strcmp(keystr, "oamp") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_AMP, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "omix") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_MIX, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "opw") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_PULSEWIDTH, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "osat") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_SATURATION, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "ospeed") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_ENVELOPE_SPEED, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "odrift") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_DRIFT_DEPTH, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "odist") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_OSC_DISTORTION_AMOUNT, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "octspread") == 0) {
(valstr, &val_i32);
extract_int32_from_token(instrument, PARAM_OSC_OCTAVE_SPREAD, val_i32);
astrid_instrument_set_param_int32} else if(strcmp(keystr, "octoffset") == 0) {
(valstr, &val_i32);
extract_int32_from_token(instrument, PARAM_OSC_OCTAVE_OFFSET, val_i32);
astrid_instrument_set_param_int32} else if(strcmp(keystr, "freqs") == 0) {
(valstr, &val_i32);
extract_int32_from_token->selected_freqs[LPRand.randint(0, NUMFREQS)] = scale[val_i32 % NUMFREQS] * 0.5f + LPRand.rand(0.f, 1.f);
ctx(instrument, PARAM_OSC_FREQS, ctx->selected_freqs, NUMFREQS);
astrid_instrument_set_param_float_list} else if(strcmp(keystr, "gamp") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_AMP, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gmix") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_MIX, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gspeed") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_SPEED, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gpw") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_PULSEWIDTH, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gsat") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_SATURATION, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gshape") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_SHAPE, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gdrift") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_GATE_DRIFT_DEPTH, val_f);
astrid_instrument_set_param_float} else if(strcmp(keystr, "gpat") == 0) {
(valstr, val_pattern.pattern, &val_pattern.length);
extract_patternbuf_from_token(instrument, PARAM_GATE_PATTERN, &val_pattern);
astrid_instrument_set_param_patternbuf} else if(strcmp(keystr, "mmix") == 0) {
(valstr, &val_f);
extract_float_from_token(instrument, PARAM_MIC_MIX, val_f);
astrid_instrument_set_param_float}
return 0;
}
I want to add some more param types though, to take frequency lists
at least, maybe some chord conversion? But probably all the preparation
of the frequency lists can happen in python where the tune
module is available for constructing harmony. All the instruments need
to be able to do for now is accept a list of frequencies.
This also means I’m sending param updates via serial as command
strings rather than encoded payloads. It simplifies the daisy firmware
concerns a bit, too. (Even tho it’s more annoying to work with strings
than just memcpy
some bytes into a field, that’s OK.)
Here’s some playing around with the C instrument (littlefield) I’m working on now being sequenced from another python instrument script (littleseq) which has a trigger callback that sequences sending update messages to littlefield via the new param mappings.
Update: almost feels like a real instrument with all the params mapped and some basic interactivity going!
Sunday May 26th
Got serial control working this weekend! Here’s a very exciting video demonstrating a volume control mapped to an amplitude parameter in the test instrument:
I’m working my way toward sorting through the param handling stuff:
- [C] is the instrument command console which parses cmdlines breaking
on spaces, then
=
to build up a series oflpmsg_t
update messages: one for each space-separated param in the cmdline. - [P] is the instrument param decode callback, which takes a string
token for the key, a string token for the value, and a pointer to an
lpmsg_t
struct to fill. It looks up the key ID enum value with strcmp, then switches on the ID to encode the value in the appropriate way. Error handling here is TBD, at this point the callback expects to get sane data. - [M] is the instrument message thread which handles new messages on the instrument mqueue. The update msg handler passes the update msg to [U] which at this point is already encoded and ready to be handled by the instrument callback. (The [U] instrument callback is what actually gets the values and does something.)
- [U] is the instrument update callback which takes an
lpmsg_t
update message whosemsg
field has a payload of a key/value pair encoded as bytes. The callback decodes the payload (differently depending on the key value, more on that later)
For example, the cmdline u amp=0.5 freq=220
gets parsed
by [C] into two lpmsg_t
update messages via [p] into
payloads which are IDs (mapped to an enum in the instrument) and float
values encoded into the msg
field of the update message,
then relayed back via [M] and finally received by [U] for
processing.
The video above is a simpler version of this flow that doesn’t include the [C] console parsing. That’s tonight’s project!
The procedure is basically:
- Loop over each param with
strtok_r
breaking the cmdline on spaces. - Loop over the key/value pairs in each paramline by breaking it on
=
withstrtok_r
- For keys, just store them in a tmp variable for later (very soon) processing
- For values, pass the key, value and a pointer to a fresh update message to [P] which encodes the msg
- Then send the msg!
The way encoding happens is determined by the instrument [P] callback, which means instruments get to decide what kinds of params they support and how to deal with them…
Monday May 20th
I’d like to share this essay I wrote about process music for Roots/Routes:
Where do you place agency in a process-based music system?
Sunday May 19th
Here’s something of a plan…
On the microcontroller side messages get sent as
lpserialmsg_t
structs:
typedef struct lpserialmsg_t {
uint16_t type; // corresponds to LPMessageTypes enum used by normal lpmsg_t messages
size_t size; // when greater than 0, the following bytes have a data payload of `size`
char instrument_name[LPMAXNAME]; // the instrument name, for message routing
} lpserialmsg_t;
This allows a press-a-button send-a-play-message situation – mapping any control to some astrid message.
When the size
field is greater than 0, the listener
thread should read size
more bytes via serial before
processing the message. For now, this is only used as the payload for
update messages when sending MIDI-like id/value pairs. It could be
expanded for other message types (a DATA or BUFFER message?) to support
sending arbitrary data like a stream of onsets, or whatever. I’m leaving
that unimplemented for now since I’m still not sure if I need it yet. It
also occurred to me since the lpserialmsg_t
messages can
relay play messages to any instrument, I could just do my onset
recordings on the microcontroller, and send sequences via play messages
over serial rather than scheduling them in astrid…
This feels a little more sane. I still have some things to think
about w/r/t encoding update messages, but I’m leaning toward letting
them just be always encoded – in other words, never writing the plain
text params into the msg
field of the update message, but
always using it as a buffer to store the id/value. It could probably use
a better name than “update” at some point.
Saturday May 18th
The project of the moment is to get serial messaging bidirectional in some kind of sane, useful way… I haven’t touched pretty much anything in astrid or pippi land in more than a week. (For a nice reason! I got really hooked on a book my cousin lent me last week.)
The hard part about this (as usual) is trying to decide on the structure of the data. None of my ideas are fancy. Some are decided:
- The data going over the serial wire will be a fixed size, like
astrid’s
lpmsg_t
structs. It makes reading and buffering data coming in easier to manage and reason about when all payloads are the same size. I’m tempted to use a header + payload approach like libpippi buffers, but it would mean a more complicated situation for reading and writing data: reading enough bytes for the header, then parsing the header section to see how many more to read for the payload. - Params are identified by an arbitrary integer that only has contextual meaning, like a file descriptor. It’s up to the instrument to define these numbers. (More on that later!)
- Params are delivered to instruments:
- From the astrid console by typing commands like
u amp=0.5 freq=234.2
which send anlpmsg_t
struct of the typeLPMSG_UPDATE
and themsg
member of the struct has the unparsed command string - From microcontrollers over the serial tty: the
lpserialmsg_t
struct comes in as a fixed payload of bytes, and gets relayed to the instrument as an encoded update message (more on this in a moment)
- From the astrid console by typing commands like
There are lots of things to figure out from there:
- Right now instruments listen for
LPMSG_UPDATE
messages, and have a special update callback to handle them. This allows any kind of arbitrary logic to receive and update params: the callback gets the instrument and the copy of the message, and it can do what it likes with that. However when the update message arrives I think it needs to already be encoded as a payload of bytes so that the update message can be decoded easily: in other words I want the update callbacks to only have to deal with one possible type of message. - I started out with the idea that I’d just do a kind of MIDI-style ID/value payload, and just support a float type for all messages… that works great for ranged values (like the value of a knob) and for triggers and booleans and etc (like a switch, button, etc) but… not sequences, not arrays, not patterns and that sort of thing. A float can be an index into any kind of complex behavior… but the situation I’d like to support is to record onset times on the microcontroller (think: tap in a rhythm) and then send them over serial. So I think it makes sense to try to at least leave room to support that, even if it’s not supported in the first implementation.
- I guess an enum for the datatype makes sense… then the
lpserialmsg_t
struct could be something like: int for the ID, int for the type, and… maybe a union for the value? but that means changing the microcontroller firmware every time a new type for the value is added. (Because of the union type.) Maybe that’s OK, but keeping the value field as just a buffer of bytes is probably more flexible… In that case, how large should the buffer be?? A pattern type could express a pattern as bits in the buffer…
After typing all that out, I’m starting to wonder again if the payload should be variable, and I just need to deal up front with it in the serial reader?
Let’s say I use 64 bits for the value: it can hold a double, a long int, and a 64 step pattern… but if I wanted to hold a 61 step pattern…?
Maybe the last (or first) byte of the payload for a pattern type can hold the length of the pattern, which still leaves enough room for 60 steps to be encoded.
What about just using a header?? Maybe this is actually simpler…
- It means always decoding up to the header bytes first, then reading the payload bytes before being able to decode a full message. My implementation just writes bytes into a buffer until it has MSGSIZE bytes to decode then resets… but handling two steps in that process wouldn’t be impossible I guess. I could maybe track the bytes read in two places: header bytes, and payload bytes. Then once there are enough header bytes, start reading payload bytes, but wait to reset both counters until the (variable) number of payload bytes have been read, and the message is totally parsed.
- Once that’s worked out tho payloads could be… whatever. Do they need
to be whatever? The
msg
field of thelpmsg_t
only holdsMAXMSG
bytes anyway. (PIPE_BUF - the struct and other members
)
No answers. None of it seems apparent.
Saturday May 4th
I was planning to go camping today, but it was a pretty crappy rainy day and I decided to stay in… I tried mounting some of the amplifier into the box I was thinking of using for controls… didn’t feel really great about any of it, and I’m starting to think I just want to use two channels anyway.
I’m thinking about just adapting the pulsar test drone, adding some more controls but keeping it probably pretty simple in terms of inputs and outputs… I’m not really sure, but I felt kind of overwhelmed by trying to figure out the mixer stuff I wanted to build into it all… and realized I can do it in software if I break out some controls instead…
Friday May 3rd
Hooray, I hit a bit of a milestone today! I’m pretty sure there are still some memory leaks, and I’ve got some work on the new sampler left to do… however it’s pretty exciting to be testing out some coordinated events with a little astrid script orchestrating the solenoid wiggling and the pulsar bloops this morning.
What I’d like to do in the meantime (while finishing the hardware build and testing the four channel setup, adding motor control, etc…) is keep prototyping some instrument scripts in python, and then port them to C instruments when I’m happy with them.
Hopefully there will be time for it, I’m not really super into live-coding during performance (though it’s a great way to prototype & compose) and would like to map all controls to the arduino’s inputs along with some simple orchestration commands at the astrid prompt…
Porting the instrument to C should let me get a little more fancy in the render callback, and the stream interface only exists in C at the moment, that would be nice to use… we’ll see!
Log April 2024
Log March 2024
Log February 2024
Log January 2024
Log December 2023