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++) {
oscs[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);
}
(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++) {
oscs[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);
}
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.