diff options
author | Vito Caputo <vcaputo@pengaru.com> | 2023-01-19 23:01:07 -0800 |
---|---|---|
committer | Vito Caputo <vcaputo@pengaru.com> | 2023-01-21 13:34:12 -0800 |
commit | 348eaf96c8b38795319d785a0b1f1749ffb1a55a (patch) | |
tree | f3cbbfce78bffc69d14ec9987d85cdb5472d1e38 /src | |
parent | a583ead0f41e6bcec76b32a95985667fdbe2192a (diff) |
modules/rocket: implement GNU Rocket integration
This adds a rudimentary but functional rocket module for
sequencing "tapped" variables in rototiller modules according to
a timeline via GNU Rocket editors.
Currently this only supports a single seq_module= as a setting
which will be used for rendering. Any tapped variables present
in the nested modules under seq_module will be available for
sequencing, and should automatically appear in a connected rocket
editor.
If you specify connect=off then rocket sync tracks will be read
from the filesystem if present. It's a bit clumsy as-is due to
how the GNU Rocket library handles this currently. There's a
"base" label concept for the virtual rocket device, and the
tracks are intended to be files in a directory named using that
base= setting.
The way you create those track files is by triggering a remote
export from the editor while connected. The location of the
directory is relative to the cwd of the rototiller process, and
you can't specify absolute paths as the base= setting to be
explicit about where things go. The setting isn't really a path,
as that's not what the library wants it to be. It's an area in
need of improvement.
In any case, as long as you start with the same base= setting,
from the same CWD, as when you did the remote export, you can
re-run with connect=off and the exported tracks will be used
automagically and things should replay without the editor
connected.
If you start with connect=on, which is the default, you need to
have the editor already running. Otherwise the rocket module
will fail @ context create, and you'll get a confusing error
about being unable to allocate memory. This is just for now, the
context create needs to start returning an errno instead of just
the context pointer so the error messages can be more informative
now that context create may be doing complicated things like
connecting to sockets.
Another thing to improve is probably having the module just
reconnect periodically if connect=on but it failed @ context
create. It could just start anyways and not fail the context
create at all there, and just start working once you get the
editor online. That'd be a better user experience.
This is a good first step regardless...
Diffstat (limited to 'src')
-rw-r--r-- | src/modules/rocket/rocket.c | 351 |
1 files changed, 321 insertions, 30 deletions
diff --git a/src/modules/rocket/rocket.c b/src/modules/rocket/rocket.c index 1ad2c51..6add6be 100644 --- a/src/modules/rocket/rocket.c +++ b/src/modules/rocket/rocket.c @@ -2,10 +2,16 @@ #include <string.h> #include <time.h> +#include "rocket/rocket/lib/device.h" +#include "rocket/rocket/lib/sync.h" +#include "rocket/rocket/lib/track.h" + #include "til.h" #include "til_fb.h" #include "til_module_context.h" #include "til_settings.h" +#include "til_stream.h" +#include "til_tap.h" #include "til_util.h" #include "txt/txt.h" @@ -14,52 +20,75 @@ /* This implements a rudimentary sequencing module varying * "tapped" variables of other modules on a timeline via - * GNU Rocket. + * GNU Rocket (https://github.com/rocket/rocket) */ typedef struct rocket_context_t { til_module_context_t til_module_context; - const til_module_t *module; - til_module_context_t *module_ctxt; - char *module_settings; + const til_module_t *seq_module; + til_module_context_t *seq_module_ctxt; + + struct sync_device *sync_device; + double rows_per_ms; + double rocket_row; + unsigned last_ticks; + unsigned paused:1; } rocket_context_t; typedef struct rocket_setup_t { til_setup_t til_setup; - const char *module; + const char *seq_module_name; + const char *base; + double rows_per_ms; + unsigned connect:1; + const char *host; + unsigned short port; } rocket_setup_t; -static rocket_setup_t rocket_default_setup = { .module = "rtv" }; +static rocket_setup_t rocket_default_setup = { .seq_module_name = "compose" }; -static til_module_context_t * rocket_create_context(til_stream_t *stream, unsigned seed, unsigned ticks, unsigned n_cpus, char *path, til_setup_t *setup) +static til_module_context_t * rocket_create_context(const til_module_t *module, til_stream_t *stream, unsigned seed, unsigned ticks, unsigned n_cpus, char *path, til_setup_t *setup) { rocket_context_t *ctxt; - const til_module_t *module; + const til_module_t *seq_module; if (!setup) setup = &rocket_default_setup.til_setup; - module = til_lookup_module(((rocket_setup_t *)setup)->module); - if (!module) + seq_module = til_lookup_module(((rocket_setup_t *)setup)->seq_module_name); + if (!seq_module) return NULL; - ctxt = til_module_context_new(stream, sizeof(rocket_context_t), seed, ticks, n_cpus, path); + ctxt = til_module_context_new(module, sizeof(rocket_context_t), stream, seed, ticks, n_cpus, path); if (!ctxt) return NULL; - ctxt->module = module; + ctxt->sync_device = sync_create_device(((rocket_setup_t *)setup)->base); + if (!ctxt->sync_device) + return til_module_context_free(&ctxt->til_module_context); + + if (((rocket_setup_t *)setup)->connect) { + /* XXX: it'd be better if we just reconnected periodically instead of hard failing */ + if (sync_tcp_connect(ctxt->sync_device, ((rocket_setup_t *)setup)->host, ((rocket_setup_t *)setup)->port)) + return til_module_context_free(&ctxt->til_module_context); + } + + ctxt->seq_module = seq_module; { til_setup_t *module_setup = NULL; - (void) til_module_randomize_setup(ctxt->module, rand_r(&seed), &module_setup, NULL); + (void) til_module_randomize_setup(ctxt->seq_module, rand_r(&seed), &module_setup, NULL); - (void) til_module_create_context(ctxt->module, stream, rand_r(&seed), ticks, 0, path, module_setup, &ctxt->module_ctxt); + (void) til_module_create_context(ctxt->seq_module, stream, rand_r(&seed), ticks, 0, path, module_setup, &ctxt->seq_module_ctxt); til_setup_free(module_setup); } + ctxt->rows_per_ms = ((rocket_setup_t *)setup)->rows_per_ms; + ctxt->last_ticks = ticks; + return &ctxt->til_module_context; } @@ -68,57 +97,319 @@ static void rocket_destroy_context(til_module_context_t *context) { rocket_context_t *ctxt = (rocket_context_t *)context; - til_module_context_free(ctxt->module_ctxt); + if (ctxt->sync_device) + sync_destroy_device(ctxt->sync_device); + til_module_context_free(ctxt->seq_module_ctxt); free(context); } +static void rocket_sync_pause(void *context, int flag) +{ + rocket_context_t *ctxt = context; + + if (flag) + ctxt->paused = 1; + else + ctxt->paused = 0; +} + + +static void rocket_sync_set_row(void *context, int row) +{ + rocket_context_t *ctxt = context; + + ctxt->rocket_row = row; +} + + +static int rocket_sync_is_playing(void *context) +{ + rocket_context_t *ctxt = context; + + /* returns bool, 1 for is playing */ + return !ctxt->paused; +} + + +static struct sync_cb rocket_sync_cb = { + rocket_sync_pause, + rocket_sync_set_row, + rocket_sync_is_playing, +}; + + +typedef struct rocket_pipe_t { + /* rocket basically only applies to floats, so we only need a float, its tap, and a sync track */ + til_tap_t tap; + + union { + float f; + double d; + } var; + + union { + float *f; + double *d; + } ptr; + + const struct sync_track *track; + char track_name[]; +} rocket_pipe_t; + + +int rocket_stream_pipe_ctor(void *context, til_stream_t *stream, const void *owner, const void *owner_foo, const char *parent_path, uint32_t parent_hash, const til_tap_t *tap, const void **res_owner, const void **res_owner_foo, const til_tap_t **res_driving_tap) +{ + rocket_context_t *ctxt = context; + rocket_pipe_t *rocket_pipe; + size_t track_name_len; + + assert(stream); + assert(tap); + assert(res_owner); + assert(res_owner_foo); + assert(res_driving_tap); + + if (tap->type != TIL_TAP_TYPE_FLOAT && + tap->type != TIL_TAP_TYPE_DOUBLE) + return 0; /* not interesting to us */ + + /* we take ownership, and create our own tap and rocket track to stow @ owner_foo */ + + /* rocket has its own syntax for track names so instead of consttructing a concatenated path + * in til_stream_pipe_t and passing it to the ctor, just construct our own in the end of rocket_pipe_t + */ + track_name_len = strlen(parent_path) + 1 + strlen(tap->name) + 1; + rocket_pipe = calloc(1, sizeof(rocket_pipe_t) + track_name_len); + if (!rocket_pipe) + return -ENOMEM; + + snprintf(rocket_pipe->track_name, track_name_len, "%s:%s", parent_path, tap->name); + rocket_pipe->tap = til_tap_init(ctxt, tap->type, &rocket_pipe->ptr, 1, &rocket_pipe->var, tap->name); + rocket_pipe->track = sync_get_track(ctxt->sync_device, rocket_pipe->track_name); + + *res_owner = ctxt; + *res_owner_foo = rocket_pipe; + *res_driving_tap = rocket_pipe->track->num_keys ? &rocket_pipe->tap : tap; + + return 1; +} + + +static const til_stream_hooks_t rocket_stream_hooks = { + .pipe_ctor = rocket_stream_pipe_ctor, + /* .pipe_dtor unneeded */ +}; + + +static int rocket_pipe_update(void *context, til_stream_pipe_t *pipe, const void *owner, const void *owner_foo, const til_tap_t *driving_tap) +{ + rocket_pipe_t *rocket_pipe = (rocket_pipe_t *)owner_foo; + rocket_context_t *ctxt = context; + double val; + + /* just ignore pipes we don't own (they're not types we can drive w/rocket) */ + if (owner != ctxt) + return 0; + + /* when there's no keys in the track, flag as inactive so someone else can drive */ + if (!rocket_pipe->track->num_keys) { + rocket_pipe->tap.inactive = 1; + + return 0; + } + + rocket_pipe->tap.inactive = 0; + if (driving_tap != &rocket_pipe->tap) + til_stream_pipe_set_driving_tap(pipe, &rocket_pipe->tap); + + /* otherwise get the current interpolated value from the rocket track @ owner_foo->track + * to update owner_foo->var.[fd], which _should_ be the driving tap. + */ + val = sync_get_val(rocket_pipe->track, ctxt->rocket_row); + switch (rocket_pipe->tap.type) { + case TIL_TAP_TYPE_FLOAT: + rocket_pipe->var.f = val; + break; + case TIL_TAP_TYPE_DOUBLE: + rocket_pipe->var.d = val; + break; + default: + assert(0); + } + + return 0; +} + + static void rocket_render_fragment(til_module_context_t *context, til_stream_t *stream, unsigned ticks, unsigned cpu, til_fb_fragment_t **fragment_ptr) { rocket_context_t *ctxt = (rocket_context_t *)context; - til_module_render(ctxt->module_ctxt, stream, ticks, fragment_ptr); + if (!ctxt->paused) + ctxt->rocket_row += ((double)(ticks - ctxt->last_ticks)) * ctxt->rows_per_ms; + + ctxt->last_ticks = ticks; + + /* hooks-setting is idempotent and cheap so we just always do it, and technicallly the stream can get changed out on us frame-to-frame */ + til_stream_set_hooks(stream, &rocket_stream_hooks, ctxt); + + /* ctxt->rocket_row needs to be updated */ + sync_update(ctxt->sync_device, ctxt->rocket_row, &rocket_sync_cb, ctxt); + + /* this drives our per-rocket-track updates, with the tracks registered as owner_foo on the pipes, respectively */ + til_stream_for_each_pipe(stream, rocket_pipe_update, ctxt); + + til_module_render(ctxt->seq_module_ctxt, stream, ticks, fragment_ptr); } static int rocket_setup(const til_settings_t *settings, til_setting_t **res_setting, const til_setting_desc_t **res_desc, til_setup_t **res_setup) { - const char *module; + const char *connect_values[] = { + "off", + "on", + NULL + }; + const char *seq_module; + const char *base; + const char *bpm; + const char *rpb; + const char *connect; + const char *host; + const char *port; int r; + /* TODO: + * Instead of driving a single module, we could accept a list of module specifiers + * including settings for each (requiring the recursive settings support to land). + * Then just use a module selector track for switching between the modules... that + * might work for getting full-blown demos sequenced via rocket. + */ r = til_settings_get_and_describe_value(settings, &(til_setting_desc_t){ .name = "Module to sequence", - .key = "module", - .preferred = "rtv", + .key = "seq_module", + .preferred = "compose", + .annotations = NULL, + }, + &seq_module, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Rocket \"base\" label", + .key = "base", + .preferred = "tiller", + .annotations = NULL, + }, + &base, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Beats per minute", + .key = "bpm", + .preferred = "125", + .annotations = NULL, + }, + &bpm, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Rows per beat", + .key = "rpb", + .preferred = "8", .annotations = NULL, }, - &module, + &rpb, res_setting, res_desc); if (r) return r; - /* turn layers colon-separated list into a null-terminated array of strings */ + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Editor connection toggle", + .key = "connect", + /* TODO: regex */ + .preferred = connect_values[1], + .values = connect_values, + .annotations = NULL, + }, + &connect, + res_setting, + res_desc); + if (r) + return r; + + if (!strcasecmp(connect, "on")) { + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Editor host", + .key = "host", + .preferred = "localhost", + /* TODO: regex */ + .annotations = NULL, + }, + &host, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_value(settings, + &(til_setting_desc_t){ + .name = "Editor port", + .key = "port", + .preferred = TIL_SETTINGS_STR(SYNC_DEFAULT_PORT), + /* TODO: regex */ + .annotations = NULL, + }, + &port, + res_setting, + res_desc); + if (r) + return r; + } + if (res_setup) { - const til_module_t *til_module; + const til_module_t *til_seq_module; rocket_setup_t *setup; + unsigned ibpm, irpb; - if (!strcmp(module, "rocket")) + if (!strcmp(seq_module, "rocket")) return -EINVAL; - til_module = til_lookup_module(module); - if (!til_module) + til_seq_module = til_lookup_module(seq_module); + if (!til_seq_module) return -ENOENT; - if (til_module->flags & (TIL_MODULE_HERMETIC | TIL_MODULE_EXPERIMENTAL)) - return -EINVAL; - + /* TODO: we're going to need a custom setup_free to cleanup host+base etc. */ setup = til_setup_new(sizeof(*setup), (void(*)(til_setup_t *))free); if (!setup) return -ENOMEM; - setup->module = til_module->name; + setup->seq_module_name = til_seq_module->name; + setup->base = strdup(base); /* FIXME errors */ + if (!strcasecmp(connect, "on")) { + setup->connect = 1; + setup->host = strdup(host); /* FIXME errors */ + sscanf(port, "%hu", &setup->port); /* FIXME parse errors */ + } + sscanf(bpm, "%u", &ibpm); + sscanf(rpb, "%u", &irpb); + setup->rows_per_ms = ((double)(ibpm * irpb)) * (1.0 / (60.0 * 1000.0)); *res_setup = &setup->til_setup; } @@ -134,5 +425,5 @@ til_module_t rocket_module = { .name = "rocket", .description = "GNU Rocket module sequencer", .setup = rocket_setup, - .flags = TIL_MODULE_HERMETIC | TIL_MODULE_EXPERIMENTAL, + .flags = TIL_MODULE_HERMETIC | TIL_MODULE_EXPERIMENTAL, /* this needs refinement esp. if rocket gets split into a player and editor */ }; |