From 348eaf96c8b38795319d785a0b1f1749ffb1a55a Mon Sep 17 00:00:00 2001 From: Vito Caputo Date: Thu, 19 Jan 2023 23:01:07 -0800 Subject: 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... --- src/modules/rocket/rocket.c | 351 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 321 insertions(+), 30 deletions(-) (limited to 'src/modules/rocket') 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 #include +#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 */ }; -- cgit v1.2.3