Hi, Pd doesn't have a notion of global transport time, it just knows
about logical time. People can (and have) build their own transport
abstractions on top of that.
For further inspiration you could look into Supercollider's TempoClock
class. The idea is that each TempoClock can have its own logical time
and tempo. Changing the time or tempo of a TempoClock affects all
Routines that are scheduled on this clock. You can, of course, have more
than one TempoClock at the same time.
Christof
On 25.10.2020 16:16, Iain Duncan wrote:
Thanks Christof, that is helpful again, and also encouraging as it
describes pretty well what I've done so far in the Max version. :-)
I've enabled both one shot clocks (storing them in a hashtable owned
by the external) and periodic timers. The one shot clocks exist in
both a transport aware and transport ignorant format for working with
and quantizing off the Max global transport, and there are periodic
timers for both too. (The transport time stops when the transport is
stopped, the other is just a general X ms timer). I have also ensured
the callback handle gets passed back so that any timer or clock can be
cancelled from Scheme user-space. (is there such a thing as a global
transport in PD?)
I was actually planning on something like you described in Scheme
space: a user defined scheduler running off the timers. I will look
into priority queues. I had one thought, which I have not seen much,
and was part of the reason I was asking on here about schedulers. I
would like to ensure the user can run multiple transports at once, and
hop around in time without glitches. I was thinking that instead of
using just a priority queue, I would do something like a two stage
structure, perhaps with a hashmap or some other fast-read-anywhere
structure with entries representing a time period, and holding
priority queues for each period. This would be to enable the system to
seek instantly to the bar (say) and iterate through the queue/list for
that bar. Wondering if anyone has used or seen this type of pattern or
has suggestions? Basically I want to make sure random access in time
will work ok even if the number of events in the schedule is very
high, thus allowing us to blur the lines between a scheduler and a
full blown sequencer engine. Thoughts, suggestions, and warnings are
all welcome.
iain
On Sun, Oct 25, 2020 at 4:21 AM Christof Ressi <[email protected]
<mailto:[email protected]>> wrote:
Actually, there is no need to use a clock for every scheduled LISP
function. You can also maintain a seperate scheduler, which is
just a priority queue for callback functions. In C++, you could
use a std::map<double, callback_type>. "double" is the desired
(future) system time, which you can get with "clock_getsystimeafter".
Then you create a *single* clock in the setup function *) with a
tick method that reschedules itself periodically (e.g.
clock_delay(x, 1) ). In the tick method, you get the current
logical time with "clock_getlogicaltime", walk over the priority
queue and dispatch + remove all items which have a time equal or
lower. You have to be careful about possible recursion, though,
because calling a scheduled LISP function might itself schedule
another function. In the case of std::map, however, it is safe,
because insertion doesn't invalidate iterators.
Some more ideas:
Personally, I like to have both one-shot functions and repeated
functions, being able to change the time/interval and also cancel
them. For this, it is useful that the API returns some kind of
identifier for each callback (e.g. an integer ID). This is what
Javascript does with "setTimeout"/"clearTimeout" and
"setInterval"/"clearInterval". I use a very similar system for the
Lua scripting API of my 2D game engine, but I also have
"resetTimeout" and "resetInterval" functions.
On the other hand, you could also have a look at the scheduling
API of the Supercollider, which is a bit different: if a routine
yields a number N, it means that the routine will be scheduled
again after N seconds.
Generally, having periodic timers is very convenient in a musical
environment :-)
Christof
*) Don't just store the clock in a global variable, because Pd can
have several instances. Instead, put the clock in a struct which
you allocate in the setup function. The clock gets this struct as
the owner.
typedef struct _myscheduler { t_clock *clock; } t_myscheduler; //
this would also be a good place to store the priority queue
t_scheduler *x = getbytes(sizeof(t_myscheduler));
t_clock *clock = clock_new(x, (t_method)myscheduler_tick);
x->clock = clock;
On 25.10.2020 02:02, Iain Duncan wrote:
Thanks Christof, that's very helpful.
iain
On Sat, Oct 24, 2020 at 5:53 PM Christof Ressi
<[email protected] <mailto:[email protected]>> wrote:
But if you're still worried, creating a pool of objects of
the same size is actually quite easy, just use a
https://en.wikipedia.org/wiki/Free_list
<https://en.wikipedia.org/wiki/Free_list>.
Christof
On 25.10.2020 02:45, Christof Ressi wrote:
A) Am I right, both about being bad, and about clock
pre-allocation and pooling being a decent solution?
B) Does anyone have tips on how one should implement and
use said clock pool?
ad A), basically yes, but in Pd you can get away with it.
Pd's scheduler doesn't run in the actual audio callback
(unless you run Pd in "callback" mode) and is more tolerant
towards operations that are not exactly realtime friendly
(e.g. memory allocation, file IO, firing lots of messages,
etc.). The audio callback and scheduler thread exchange
audio samples via a lockfree ringbuffer. The "delay"
parameter actually sets the size of this ringbuffer, and a
larger size allows for larger CPU spikes.
In practice, allocating a small struct is pretty fast even
with the standard memory allocator, so in the case of Pd
it's nothing to worry about. In Pd land, external authors
don't really care too much about realtime safety, simply
because Pd itself doesn't either.
---
Now, in SuperCollider things are different. Scsynth and
Supernova are quite strict regarding realtime safety because
DSP runs in the audio callback. In fact, they use a special
realtime allocator in case a plugin needs to allocate memory
in the audio thread. Supercollider also has a seperate
non-realtime thread where you would execute asynchronous
commands, like loading a soundfile into a buffer.
Finally, all sequencing and scheduling runs in a different
program (sclang). Sclang sends OSC bundles to scsynth, with
timestamps in the near future. Conceptually, this is a bit
similar to Pd's ringbuffer scheduler, with the difference
that DSP itself never blocks. If Sclang blocks, OSC messages
will simply arrive late at the Server.
Christof
On 25.10.2020 02:10, Iain Duncan wrote:
Hi folks, I'm working on an external for Max and PD
embedding the S7 scheme interpreter. It's mostly intended
to do things at event level, (algo comp, etc) so I have
been somewhat lazy around real time issues so far. But I'd
like to make sure it's as robust as it can be, and can be
used for as much as possible. Right now, I'm pretty sure
I'm being a bad real-time-coder. When the user wants to
delay a function call, ie (delay 100 foo-fun), I'm doing
the following:
- callable foo-fun gets registered in a scheme hashtable
with a gensym unique handle
- C function gets called with the handle
- C code makes a clock, storing it in a hashtable (in C) by
the handle, and passing it a struct (I call it the "clock
callback info struct") with the references it needs for
it's callback
- when the clock callback fires, it gets passed a void
pointer to the clock-callback-info-struct, uses it to get
the cb handle and the ref to the external (because the
callback only gets one arg), calls back into Scheme with
said handle
- Scheme gets the callback out of it's registry and
executes the stashed function
This is working well, but.... I am both allocating and
deallocating memory in those functions: for the clock, and
for the info struct I use to pass around the reference to
the external and the handle. Given that I want to be
treating this code as high priority, and having it execute
as timing-accurate as possible, I assume I should not be
allocating and freeing in those functions, because I could
get blocked on the memory calls, correct? I think I should
probably have a pre-allocated pool of clocks and their
associated info structs so that when a delay call comes in,
we get one from the pool, and only do memory management if
the pool is empty. (and allow the user to set some
reasonable config value of the clock pool). I'm thinking
RAM is cheap, clocks are small, people aren't likely to
have more than 1000 delay functions running concurrently or
something at once, and they can be allocated from the init
routine.
My questions:
A) Am I right, both about being bad, and about clock
pre-allocation and pooling being a decent solution?
B) Does anyone have tips on how one should implement and
use said clock pool?
I suppose I should probably also be ensuring the Scheme
hash-table doesn't do any unplanned allocation too, but I
can bug folks on the S7 mailing list for that one...
Thanks!
iain
_______________________________________________
Pd-dev mailing list
[email protected] <mailto:[email protected]>
https://lists.puredata.info/listinfo/pd-dev
<https://lists.puredata.info/listinfo/pd-dev>
_______________________________________________
Pd-dev mailing list
[email protected] <mailto:[email protected]>
https://lists.puredata.info/listinfo/pd-dev
<https://lists.puredata.info/listinfo/pd-dev>
_______________________________________________
Pd-dev mailing list
[email protected] <mailto:[email protected]>
https://lists.puredata.info/listinfo/pd-dev
<https://lists.puredata.info/listinfo/pd-dev>
_______________________________________________
Pd-dev mailing list
[email protected] <mailto:[email protected]>
https://lists.puredata.info/listinfo/pd-dev
<https://lists.puredata.info/listinfo/pd-dev>
_______________________________________________
Pd-dev mailing list
[email protected]
https://lists.puredata.info/listinfo/pd-dev