I took the evening tonight to try to do some cleanup on the new
pippi.renderer module. The cython Instrument interface is a
little more like a wrapper for the lpinstrument_t struct,
which makes passing things around somewhat easier to think about. Just
like most of the C API takes a pointer to the
lpinstrument_t struct, the cython API (more often) takes a
reference to the Instrument to do things, and the cython
interface can call into the C API as it likes with the internal
reference to the lpinstrument_t struct. There was also a
weird leftover from earlier days that was loading the python renderer –
in other words the actual python instrument script which gets live
reloaded – externally and calling it an “instrument” confusingly…
I got a bit of a start on thinking through a revision to the way the ADC works… and ended up realizing the way it works already is probably fine, what I really want to be able to do is to create named buffers in shared memory from python ad hoc, like a sampler.
The current ADC interface creates a fixed-size ringbuffer and has interfaces for writing samples into it (locking the buffer during the writes) and reading samples from it. There’s a global name for the shared memory / semaphore pair because it was originally designed to run in a single ADC program.
It seems like the main thing to do is just to take a prefix for the name as a param and leave the rest as it is more or less. It’s a project for another night, but I’m really excited to get the ADC working again.
The ugen interface is also still in limbo pretty much. The way I’ve
implemented it so far is a bit backward, and I think I need to go back
and add the ugen features to the test C pulsar instrument first, and
then expose some APIs to cython that are similar to the current cython
ugen module interfaces.
The way I’m doing parameter updates to the graph is really clunky and weird. I talked through some ideas with Paul B recently who has been inspiring the C API of this project from the beginning (lots of his code has also been embedded in pippi for a long while!) and pointed me toward some nice ideas…
Tonight it also occurred to me something I’d really like to do with the stream interface is basically simple mixing: routing inputs to outputs without much processing.
For example the inputs of the soundcard to the inputs of an instrument’s internal ringbuffer, or the inputs of the soundcard to the outputs of a soundcard with some basic attenuation or something. The last time astrid was functional enough for performance I was using either a supercollider script or a PD patch to do that routing. It would be really nice to do it from within the scripts themselves.
Maybe it looks like a new mixer callback:
def mixer(ctx):
# mono passthrough
ctx.inputs(0).connect_to(ctx.outputs(0))
# use a param from the session context to control routing
if ctx.s.foo == 'bar':
ctx.inputs(1).connect_to(ctx.outputs(0))
else:
ctx.inputs(1).connect_to(ctx.outputs(1))
Or something like that?
One of the things I was discussing with Paul was how I disliked writing connections like the above as function calls. It feels clunky and difficult to read when patches start to grow larger than trivial things. Like this test patch I wrote when developing the basic graph system is really hard for me to parse and understand:
from pippi import ugens
graph = ugens.Graph()
graph.add_node('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.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)
out = graph.render(10)
out = fx.norm(out, 1).taper(0.1)
out.write('ugens_sine.wav')
Okay “really hard” is relative, but it doesn’t feel like I can scan it and make sense of it at a glance the way that a graphical patch language shows the flow very clearly.
He’s getting me interested in exploring a forth-y DSL for this maybe, but I’m not really sure what that would look like at the moment. I’m inspired by how beautiful and small his sporth programs are. I have a really hard time with the stack-based approach, but it feels like once you learn to read it fluently, it could be a really beautiful thing to describe signal flows… The way it would actually look in an astrid script is unclear. Something nutty like this maybe:
from pippi import renderer
STREAM = """
# store melody in a table
_seq "0 5 0 3 0 3 5 0 3" gen_vals
# create beat and half-beat triggers
_half var _beat var
4 metro _half set
_half get 2 0 tdiv _beat set
# play melody with a sine wave
(_half get 0 _seq tseq 75 +) mtof 0.1 sine
# apply envelopes on beats and half-beats
_half get 0.001 0.1 0.2 tenvx *
_beat get 0.001 0.1 0.2 tenvx *
# add delay
dup (0.5 (4 inv) delay) (0.4 *) +
"""
# other callbacks and stuff
def play(ctx):
yield None
if __name__ == '__main__':
renderer.run_forever('sporthy')
The idea being that instead of a callback you have a DSL string that gets evaluated on save. (That sporth patch is stolen from the interactive version of Paul’s sporth cookbook.)
It seems like an interesting thing to try in a future nim interface, too, since nim has very strong compile time metaprogamming abilities for creating DSLs… maybe a sporth-inspired frontend DSL?
I’ll probably deal with the clunky interfaces for the medium-to-long-term future, but that’s a good project to think about for later improvements.