diff --git a/template/calfbox/jackio.c b/template/calfbox/jackio.c index 47dc64a..9cc938e 100644 --- a/template/calfbox/jackio.c +++ b/template/calfbox/jackio.c @@ -1061,6 +1061,35 @@ static gboolean cbox_jack_io_process_cmd(struct cbox_command_target *ct, struct } return TRUE; } + else if (!strcmp(cmd->command, "/client_set_property") && !strcmp(cmd->arg_types, "sss")) + /*same as set_property above, but works directly on our own jack client. + parameters: key, value, type according to jack_property_t (empty or NULL for string) + */ + { + const char *key = CBOX_ARG_S(cmd, 0); + const char *value = CBOX_ARG_S(cmd, 1); + const char *type = CBOX_ARG_S(cmd, 2); + + char* subject; + subject = jack_get_uuid_for_client_name(jii->client, jii->client_name); //lookup our own client + if (!subject) { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Cannot get string UUID for our jack client."); + return FALSE; + } + + jack_uuid_t j_client_uuid_t; + if (jack_uuid_parse(subject, &j_client_uuid_t)) { //from jack/uuid.h // 0 on success + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "jack_uuid_parse() couldn't parse our string client UUID %s as numerical jack_uuid_t %ld", subject, j_client_uuid_t); + return FALSE; + } + + if (jack_set_property(jii->client, j_client_uuid_t, key, value, type)) // 0 on success + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Set client property key:'%s' value: '%s' was not successful", key, value); + return FALSE; + } + return TRUE; + } else if (!strcmp(cmd->command, "/get_property") && !strcmp(cmd->arg_types, "ss")) //parameters: "client:port", key //returns python key, value and type as strings diff --git a/template/calfbox/py/cbox.py b/template/calfbox/py/cbox.py index b449e69..acefc6e 100644 --- a/template/calfbox/py/cbox.py +++ b/template/calfbox/py/cbox.py @@ -952,6 +952,9 @@ class SamplerEngine(NonDocObj): def get_patches(self): """Return a map of program identifiers to program objects.""" return self.get_thing("/patches", '/patch', {int : (str, SamplerProgram, int)}) + def get_keyswitch_state(self, channel, group): + """Return a map of program identifiers to program objects.""" + return self.get_thing("/keyswitch_state", '/last_key', int, channel, group) class FluidsynthEngine(NonDocObj): class Status: @@ -1111,6 +1114,8 @@ class SamplerProgram(DocObj): return {self.get_global() : self.get_global().get_hierarchy()} def get_control_inits(self): return self.get_thing("/control_inits", '/control_init', [(int, int)]) + def get_keyswitch_groups(self): + return self.get_thing("/keyswitch_groups", '/key_range', [(int, int)]) def new_group(self): # Obsolete return self.cmd_makeobj("/new_group") diff --git a/template/calfbox/py/metadata.py b/template/calfbox/py/metadata.py index f088ec0..03e31db 100644 --- a/template/calfbox/py/metadata.py +++ b/template/calfbox/py/metadata.py @@ -6,7 +6,7 @@ This file implements the JackIO Python side of Jack Medata as described here: http://www.jackaudio.org/files/docs/html/group__Metadata.html """ - +import base64 # for icons import os.path #get_thing @@ -47,6 +47,20 @@ class Metadata: return TypeError("value {} must be int or str but was {}".format(value, type(value))) do_cmd("/io/set_property", None, [port, key, value, jackPropertyType]) + @staticmethod + def client_set_property(key, value, jackPropertyType=""): + """empty jackPropertyType leads to UTF-8 string + for convenience we see if value is a python int and send the right jack_property_t::type + + This is directly for our client, which we do not need to provide here. + """ + if type(value) is int: + jackPropertyType = "http://www.w3.org/2001/XMLSchema#int" + value = str(value) + elif not type(value) is str: + return TypeError("value {} must be int or str but was {}".format(value, type(value))) + do_cmd("/io/client_set_property", None, [key, value, jackPropertyType]) + @staticmethod def remove_property(port, key): """port is the portname as string System:out_1""" @@ -102,42 +116,47 @@ class Metadata: @staticmethod - def _set_icon_name(port, freeDeskopIconName): - """Internal function used in set_icon_small and set_icon_large""" + def set_icon_name(freeDeskopIconName): + """ + This sets the name of the icon according to freedesktop specs. + The name is the basename without extension like so: + /usr/share/icons/hicolor/32x32/apps/patroneo.png -> "patroneo" + + The name of the icon for the subject (typically client). + This is used for looking up icons on the system, possibly with many sizes or themes. Icons + should be searched for according to the freedesktop Icon + Theme Specification: + https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + """ if not os.path.splitext(freeDeskopIconName)[0] == freeDeskopIconName: raise ValueEror(f"Icon name must not have a file extension. Expected {os.path.splitext(freeDeskopIconName)[0]} but was {freeDeskopIconName}") if not os.path.basename(freeDeskopIconName) == freeDeskopIconName: raise ValueError(f"Icon name must not be path. Expected {os.path.basename(freeDeskopIconName)} but was {freeDeskopIconName}") - self.set_property(port, "http://jackaudio.org/metadata/icon-name", freeDeskopIconName) + Metadata.client_set_property("http://jackaudio.org/metadata/icon-name", freeDeskopIconName) @staticmethod - def set_icon_small(port, freeDeskopIconName, base64png): + def set_icon_small(base64png): """ A value with a MIME type of "image/png;base64" that is an encoding of an NxN (with 32 < N <= 128) image to be used when displaying a visual representation of that client or port. - The name of the icon for the subject (typically client). - This is used for looking up icons on the system, possibly with many sizes or themes. Icons - should be searched for according to the freedesktop Icon - Theme Specification: - https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html - The small icon of our JACK client. Setting icons to ports seems to be technically possible, but this is not the function for port-icons. - This function does not check if base64png has the correct format. - - This also sets the name of the icon according to freedesktop specs. - The name is the basename without extension like so: - /usr/share/icons/hicolor/32x32/apps/patroneo.png -> "patroneo" + This function checks if the data is actually base64 and a shallow test if the data is PNG. """ + testDecode = base64.b64decode(base64png) + if not base64png.encode("utf-8") == base64.b64encode(testDecode): + raise ValueError("Provided data must be uft-8 and base64 encoded. But it was not") - self.set_property(port, "http://jackaudio.org/metadata/icon-small", base64png, jackPropertyType="image/png;base64") - self._set_icon_name(port, freeDeskopIconName) + if not "PNG" in repr(testDecode)[:16]: + raise ValueError("Provided data does not seem to be a PNG image. It is missing the PNG header.") + + Metadata.client_set_property("http://jackaudio.org/metadata/icon-small", base64png, jackPropertyType="image/png;base64") @staticmethod def set_icon_large(base64png): @@ -145,21 +164,17 @@ class Metadata: NxN (with N <=32) image to be used when displaying a visual representation of that client or port. - The name of the icon for the subject (typically client). - This is used for looking up icons on the system, possibly with many sizes or themes. Icons - should be searched for according to the freedesktop Icon - Theme Specification: - https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html - The large icon of our JACK client. Setting icons to ports seems to be technically possible, but this is not the function for port-icons. + This function checks if the data is actually base64 and a shallow test if the data is PNG. + """ - This function does not check if base64png has the correct format. + testDecode = base64.b64decode(base64png) + if not base64png.encode("utf-8") == base64.b64encode(testDecode): + raise ValueError("Provided data must be uft-8 and base64 encoded. But it was not") - This also sets the name of the icon according to freedesktop specs. - The name is the basename without extension like so: - /usr/share/icons/hicolor/32x32/apps/patroneo.png -> "patroneo" - """ - self.set_property(port, "http://jackaudio.org/metadata/icon-large", base64png, jackPropertyType="image/png;base64") - self._set_icon_name(port, freeDeskopIconName) + if not "PNG" in repr(testDecode)[:16]: + raise ValueError("Provided data does not seem to be a PNG image. It is missing the PNG header.") + + Metadata.client_set_property("http://jackaudio.org/metadata/icon-large", base64png, jackPropertyType="image/png;base64") diff --git a/template/calfbox/sampler.c b/template/calfbox/sampler.c index b3bdf92..fb6d4c3 100644 --- a/template/calfbox/sampler.c +++ b/template/calfbox/sampler.c @@ -432,6 +432,25 @@ gboolean sampler_process_cmd(struct cbox_command_target *ct, struct cbox_command CBOX_OBJECT_DEFAULT_STATUS(&m->module, fb, error); } else + if (!strcmp(cmd->command, "/keyswitch_state") && !strcmp(cmd->arg_types, "ii")) + { + int channel = CBOX_ARG_I(cmd, 0); + if (channel < 1 || channel > 16) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid channel %d", channel); + return FALSE; + } + int group = CBOX_ARG_I(cmd, 1); + if (group < 0 || group >= MAX_KEYSWITCH_GROUPS) + { + g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid keyswitch group %d", group); + return FALSE; + } + if (!cbox_execute_on(fb, NULL, "/last_key", "i", error, m->channels[channel - 1].keyswitch_lastkey[group])) + return FALSE; + return TRUE; + } + else if (!strcmp(cmd->command, "/patches") && !strcmp(cmd->arg_types, "")) { if (!cbox_check_fb_channel(fb, cmd->command, error)) diff --git a/template/calfbox/sampler.h b/template/calfbox/sampler.h index 6dba6e9..e1bd0a0 100644 --- a/template/calfbox/sampler.h +++ b/template/calfbox/sampler.h @@ -72,6 +72,7 @@ struct sampler_channel float floatcc[smsrc_perchan_count]; uint8_t last_polyaft, last_chanaft; uint8_t keyswitch_state[MAX_KEYSWITCH_GROUPS]; + uint8_t keyswitch_lastkey[MAX_KEYSWITCH_GROUPS]; }; struct sampler_lfo diff --git a/template/calfbox/sampler_api_example5.py b/template/calfbox/sampler_api_example5.py index 79acd86..43f8670 100644 --- a/template/calfbox/sampler_api_example5.py +++ b/template/calfbox/sampler_api_example5.py @@ -73,7 +73,7 @@ print ("Control Inits:", pgm.get_control_inits()) #Empty . Is this ? globalHierarchy = pgm.get_global() # -> Single SamplerLayer. Literally sfz . But not the global scope, e.g. no under any . #If there is no tag in the .sfz this will still create a root SamplerLayer print ("Global:", globalHierarchy) - +print ("Groups:", pgm.get_keyswitch_groups()) print("\nShow all SamplerLayer in their global/master/group/region hierarchy through indentations.\n" + "=" * 80) def recurse(item, level = 0): @@ -157,7 +157,10 @@ def findKeyswitches(program): keyswitches = findKeyswitches(pgm) pprint(keyswitches) -scene.send_midi_event(0x80, 61 ,64) #Trigger Keyswitch c#4 . Default for this example is 60/c4 +instrument.engine.set_patch(1, pgm_no) +print ("Group 0 lastkey before keyswitch:", instrument.engine.get_keyswitch_state(1, 0)) +scene.send_midi_event(0x90, 61 ,64) #Trigger Keyswitch c#4 . Default for this example is 60/c4 +print ("Group 0 lastkey after keyswitch:", instrument.engine.get_keyswitch_state(1, 0)) #The following were just stages during development, now handled by recurse and recurse2() above diff --git a/template/calfbox/sampler_channel.c b/template/calfbox/sampler_channel.c index 4af9396..93078cb 100644 --- a/template/calfbox/sampler_channel.c +++ b/template/calfbox/sampler_channel.c @@ -53,13 +53,20 @@ void sampler_channel_reset_keyswitches(struct sampler_channel *c) if (c->program && c->program->rll) { memset(c->keyswitch_state, 255, sizeof(c->keyswitch_state)); + memset(c->keyswitch_lastkey, 255, sizeof(c->keyswitch_lastkey)); for (uint32_t i = 0; i < c->program->rll->keyswitch_group_count; ++i) { const struct sampler_keyswitch_group *ksg = c->program->rll->keyswitch_groups[i]; if (ksg->def_value == 255) + { c->keyswitch_state[i] = ksg->key_offsets[0]; + c->keyswitch_lastkey[i] = ksg->lo; + } else + { c->keyswitch_state[i] = ksg->key_offsets[ksg->def_value]; + c->keyswitch_lastkey[i] = ksg->def_value + ksg->lo; + } } } } @@ -257,7 +264,10 @@ void sampler_channel_start_note(struct sampler_channel *c, int note, int vel, gb { const struct sampler_keyswitch_group *ks = prg->rll->keyswitch_groups[i]; if (note >= ks->lo && note <= ks->hi) + { + c->keyswitch_lastkey[i] = note; c->keyswitch_state[i] = ks->key_offsets[note - ks->lo]; + } } } diff --git a/template/calfbox/sampler_prg.c b/template/calfbox/sampler_prg.c index 129a1d5..8972fa1 100644 --- a/template/calfbox/sampler_prg.c +++ b/template/calfbox/sampler_prg.c @@ -238,6 +238,18 @@ static gboolean sampler_program_process_cmd(struct cbox_command_target *ct, stru return FALSE; return cbox_execute_on(fb, NULL, "/data", "b", error, blob); } + if (!strcmp(cmd->command, "/keyswitch_groups") && !strcmp(cmd->arg_types, "")) + { + if (!cbox_check_fb_channel(fb, cmd->command, error)) + return FALSE; + for (uint32_t i = 0; i < program->rll->keyswitch_group_count; ++i) + { + struct sampler_keyswitch_group *ksg = program->rll->keyswitch_groups[i]; + if (!cbox_execute_on(fb, NULL, "/key_range", "ii", error, ksg->lo, ksg->hi)) + return FALSE; + } + return TRUE; + } return cbox_object_default_process_cmd(ct, fb, cmd, error); }