In order to customize the current set of callbacks (which currently just post the value of the parameters to the Max console), a developer/user will want to modify them to do something other than the default and then re-compile the external.
In practice, callbacks in chuck-max
are constrained by what their function signatures allow. To do something useful one will typically want to access the pointer to an instance of the chuck~
object which is not directly available as an argument to any the callbacks. For example, in the case of the cb_get_int
callback, one only has the parameter name and value:
void cb_get_int(const char* name, t_CKINT val)
{
post("cb_get_int: name: %s value: %d", name, val);
}
To get around this limitation, one can use the knowledge that chuck~
instances are given the scripting name chuck-<x>
where x
is the order of instanciation and that one can get to retrieve the relevant object pointer by using void *object_findregistered(t_symbol *name_space, t_symbol *s)
as in:
void cb_get_int(const char* name, t_CKINT val)
{
t_object* x;
for (auto name : CK_INSTANCE_NAMES) {
x = (t_object*)object_findregistered(CLASS_BOX, gensym(name.c_str()));
object_post(x, (char*)"name: %s value: %d", name.c_str(), val);
}
}
It's not elegant, but it works until something better comes along such as if void *
arguments were included in all callbacks:
void cb_get_int(const char* name, t_CKINT val, void* data)
{
t_object *x = (t_object*)data;
// ...
}
cmake options:
-DC74_BUILD_FAT=ON
-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64
In fauck, use of lipo
:
# copy arm64 into universal just to use the headers
cp -r darwin-arm64 darwin-universal
# combine the static libfaust.a from x64 and arm64 into a universal library.
lipo darwin-x64/Release/lib/libfaust.a darwin-arm64/Release/lib/libfaust.a -create -output darwin-universal/Release/lib/libfaust.a
and
#!/bin/sh
VERSION=2.72.14
if [ "$(uname)" = "Darwin" ]; then
echo "You are running macOS"
if [ ! -f Faust-$VERSION-arm64.dmg ]; then
curl -L https://github.com/grame-cncm/faust/releases/download/$VERSION/Faust-$VERSION-arm64.dmg -o Faust-$VERSION-arm64.dmg
hdiutil attach Faust-$VERSION-arm64.dmg
mkdir -p "darwin-arm64/Release"
cp -R /Volumes/Faust-$VERSION/Faust-$VERSION/* darwin-arm64/Release/
hdiutil detach /Volumes/Faust-$VERSION/
fi
if [ ! -f Faust-$VERSION-x64.dmg ]; then
curl -L https://github.com/grame-cncm/faust/releases/download/$VERSION/Faust-$VERSION-x64.dmg -o Faust-$VERSION-x64.dmg
hdiutil attach Faust-$VERSION-x64.dmg
mkdir -p "darwin-x64/Release"
cp -R /Volumes/Faust-$VERSION/Faust-$VERSION/* darwin-x64/Release/
hdiutil detach /Volumes/Faust-$VERSION/
fi
elif [ "$(expr substr $(uname -s) 1 5)" = "Linux" ]; then
echo "You are running Linux"
if [ ! -f libfaust-ubuntu-x86_64.zip ]; then
curl -L https://github.com/grame-cncm/faust/releases/download/$VERSION/libfaust-ubuntu-x86_64.zip -o libfaust-ubuntu-x86_64.zip
mkdir -p "ubuntu-x86_64/Release"
unzip libfaust-ubuntu-x86_64.zip -d ubuntu-x86_64/Release
fi
elif [ "$(expr substr $(uname -s) 1 10)" = "MINGW32_NT" ] || [ "$(expr substr $(uname -s) 1 10)" = "MINGW64_NT" ]; then
echo "You are running Windows. You should run \"call download_libfaust.bat\"" >&2
exit 1
else
echo "Unknown operating system" >&2
exit 1
fi
Use ext_sysprocess
#include "ext_sysprocess.h"
@@ -789,11 +789,11 @@ t_max_err ck_edit(t_ck* x, t_symbol* s)
if (s != gensym("")) {
x->edit_file = ck_check_file(x, s);
if (x->edit_file != gensym("")) {
- std::string cmd;
- // post("edit: %s", x->edit_file->s_name);
-
- cmd = std::string(x->editor->s_name) + " " + std::string(x->edit_fi
le->s_name);
- std::system(cmd.c_str());
+ if (sysprocess_launch(x->editor->s_name, x->edit_file->s_name) == 0
) {
+ error("could not open %s with editor %s",
+ x->edit_file->s_name, x->editor->s_name);
+ return MAX_ERR_GENERIC;
+ }
return MAX_ERR_NONE;
}
Sidestepping limitations of callbacks (see here and here) especially due to lack of capture of context variables, their use is limited in Max as they don't have access to the struct pointer unless it is stored in a global pointer..
The following provides for more than one family of callbacks through the use of maps of function pointers. Probably abandon it due to above limitation.
To have more than 1 callback instance which can be retrieved by name from a map:
// typedefs
typedef void (*t_cb_event)(const char*);
typedef void (*t_cb_int)(const char*, t_CKINT);
typedef void (*t_cb_float)(const char*, t_CKFLOAT);
typedef void (*t_cb_string)(const char*, const char*);
typedef void (*t_cb_int_array)(const char*, t_CKINT[], t_CKUINT);
typedef void (*t_cb_float_array)(const char*, t_CKFLOAT[], t_CKUINT);
typedef std::unordered_map<std::string, t_cb_event> t_map_cb_event;
typedef std::unordered_map<std::string, t_cb_int> t_map_cb_int;
typedef std::unordered_map<std::string, t_cb_float> t_map_cb_float;
typedef std::unordered_map<std::string, t_cb_string> t_map_cb_string;
typedef std::unordered_map<std::string, t_cb_int_array> t_map_cb_int_array;
typedef std::unordered_map<std::string, t_cb_float_array> t_map_cb_float_array;
x->default_cb = gensym("default");
// init callback maps
x->map_cb_event = t_map_cb_event();
x->map_cb_int = t_map_cb_int();
x->map_cb_float = t_map_cb_float();
x->map_cb_string = t_map_cb_string();
x->map_cb_int_array = t_map_cb_int_array();
x->map_cb_float_array = t_map_cb_float_array();
// ? add others
// register callbacks
x->map_cb_event.emplace("default", &cb_event);
x->map_cb_int.emplace("default", &cb_get_int);
x->map_cb_float.emplace("default", &cb_get_float);
x->map_cb_string.emplace("default", &cb_get_string);
x->map_cb_int_array.emplace("default", &cb_get_int_array);
x->map_cb_float_array.emplace("default", &cb_get_float_array);
// ? add others
// then getting or using a fpointer is a matter of lookup on 'default' or other.
t_max_err ck_register(t_ck* x, t_symbol* s, long listen_forever)
{
if (!x->map_cb_event.count(s->s_name)) {
error("event/callback not found: %s", s->s_name);
return MAX_ERR_GENERIC;
}
std::string key = std::string(s->s_name);
t_cb_event cb = x->map_cb_event[key];
// false: for a one off call, strue: called everytime it is called
if (x->chuck->vm()->globals_manager()->listenForGlobalEvent(s->s_name, cb, (bool)listen_forever)) {
post("%s event/callback registered", s->s_name);
return MAX_ERR_NONE;
};
return MAX_ERR_GENERIC;
}
t_max_err ck_unregister(t_ck* x, t_symbol* s)
{
if (!x->map_cb_event.count(s->s_name)) {
error("event/callback not found: %s", s->s_name);
return MAX_ERR_GENERIC;
}
std::string key = std::string(s->s_name);
t_cb_event cb = x->map_cb_event[key];
if (x->chuck->vm()->globals_manager()->stopListeningForGlobalEvent(s->s_name, cb)) {
post("%s event/callback unregistered", s->s_name);
return MAX_ERR_NONE;
};
return MAX_ERR_GENERIC;
}
// struct
t_object* code_editor; // code editor object
char** code; // handle to code buffer for code editor
long code_size; // length of code buffer
t_fourcc code_filetype; // filetype four char code of 'TEXT'
t_fourcc code_outtype; // savetype four char code of 'TEXT'
char code_filename[MAX_PATH_CHARS]; // file name field
char code_pathname[MAX_PATH_CHARS]; // file path field
short code_path; // short code for max file system
long run_on_save; // evaluate/run code in editor on save
long run_on_close; // evaluate/run code in editor on close
void ck_dblclick(t_ck* x)
{
if ((x->run_file != gensym("")) && (x->editor != gensym(""))) {
ck_edit(x, x->run_file);
} else if (x->code_editor) {
object_attr_setchar(x->code_editor, gensym("visible"), 1);
} else {
x->code_editor = (t_object*)object_new(CLASS_NOBOX, gensym("jed"), x, 0);
object_method(x->code_editor, gensym("settext"), *x->code, gensym("utf-8"));
object_attr_setchar(x->code_editor, gensym("scratch"), 1);
object_attr_setsym(x->code_editor, gensym("title"), gensym("ck-editor"));
}
}
// etc.
sync~
rate~
also see this article- sync with gen~
- Counting, and a World without bang Messages
Possible to use alternative (active transport) notion?
1nd - Dotted whole note - 2880 ticks
1n - Whole note - 1920 ticks
1nt - Whole note triplet - 1280 ticks
2nd - Dotted half note - 1440 ticks
2n - Half note - 960 ticks
2nt - Half note triplet - 640 ticks
4nd - Dotted quarter note - 720 ticks
4n - Quarter note - 480 ticks
4nt - Quarter note triplet - 320 ticks
8nd - Dotted eighth note - 360 ticks
8n - Eighth note - 240 ticks
8nt - Eighth note triplet - 160 ticks
16nd - Dotted sixteenth note - 180 ticks
16n - Sixteenth note - 120 ticks
16nt - Sixteenth note triplet - 80 ticks
32nd - Dotted thirty-second note - 90 ticks
32n - thirty-second note - 60 ticks
32nt - thirty-second-note triplet - 40 ticks
64nd - Dotted sixty-fourth note - 45 ticks
64n - Sixty-fourth note - 30 ticks
128n - One-hundred-twenty-eighth note - 15 ticks
umenu:
1nd, 1n, 1nt, 2nd, 2n, 2nt, 4nd, 4n, 4nt, 8nd, 8n, 8nt, 16nd, 16n, 16nt, 32nd, 32n, 32nt, 64nd, 64n, 128n
t_max_err ck_send_max_msg(t_ck* x, t_symbol* s, const char* parsestr)
{
t_max_err err;
t_object *maxobj = (t_object*)object_new(CLASS_NOBOX, gensym("max"));
if (maxobj == NULL) {
error("could not get max object");
}
err = object_method_parse(maxobj, s, parsestr, NULL);
if (err != MAX_ERR_NONE) {
error("could not send msg: ;max %s %s", s->s_name, parsestr);
return err;
}
}
It seemed to be a bit difficult to retrieve the path of the default external editor.
So if x->editor
is also an attribute then it should be possible set as follows:
if (const char* editor = std::getenv("EDITOR")) { // 1
post("editor: %s", editor);
x->editor = gensym(editor);
} else if (x->editor = preferences_getsym("externaleditor")) { // 2
post("editor: %s", x->editor->s_name);
} else {
x->editor = gensym("");
}
(1) didn't initially work, but works after a restart or intermittently? and (2) retrieves only the stem of the path of the configured executable in which locatefile_extended
doesn't work.
t_max_err ck_edit(t_ck* x, t_symbol* s)
{
if (x->editor == gensym("")) {
error("editor attribute or EDITOR env var not set");
return MAX_ERR_GENERIC;
}
if (s != gensym("")) {
char conform_path[MAX_PATH_CHARS];
std::string cmd;
path_nameconform(s->s_name, conform_path, PATH_STYLE_MAX, PATH_TYPE_BOOT);
post("edit: %s", conform_path);
if (x->editor_from_prefs) {
post("open editor: %s", x->editor->s_name);
cmd = std::string("/usr/bin/open -a '") + std::string(x->editor->s_name) + "' " + std::string(conform_path);
} else {
post("exec editor: %s", x->editor->s_name);
cmd = std::string(x->editor->s_name) + " " + std::string(conform_path);
std::system(cmd.c_str());
}
return MAX_ERR_NONE;
}
error("ck_edit: reguires a filename");
return MAX_ERR_GENERIC;
}
also see: get-path-of-executable
The chuck shell in core/chuck_shell.h
has a very nice interface but is a repl (chuck --shell
from the commandline). The core/lib_machine.h
api is also quite nice, but is meant to be called from chuck code. Chuck shell uses the core/chuck_otf.h
functionality. Maybe easier to hook into one or more of these apis instead or re-implementing the wheel.
see:
-
chuck shellpaper
-
chuck floss manual
-
chuck versions changelog
implemented Chuck_Msg_Type(s)
[x] CK_MSG_ADD
[x] CK_MSG_REMOVE
[x] CK_MSG_REMOVEALL
[x] CK_MSG_REPLACE
[x] CK_MSG_STATUS
[-] CK_MSG_PAUSE OTF only
[-] CK_MSG_EXIT Not applicable
[x] CK_MSG_TIME
[x] CK_MSG_RESET_ID
[-] CK_MSG_DONE OTF only
[-] CK_MSG_ABORT OTF only
[-] CK_MSG_ERROR Not implemented
[x] CK_MSG_CLEARVM
[x] CK_MSG_CLEARGLOBALS
TODO:
[x] add <path> <args>
[x] add <code>
[x] replace <shredID> <path> <args>
[ ] replace <shredID> <withShredID>
[ ] replace <shredID> <code>
Fauck chugin can be deployed in two ways for macOS:
-
Default is via a macos bundle with the faust
stdlib
included inside the bundle -
As a non-bundled module with
chugins
and fauststdlib
in theexamples
folder
Preference in this case is for (2) since it is useful to have direct access to chugins and stdlib (and related examples). The package format is more natural for Max/MSP.
Converting (1) to (2) was a major pain because the faust dependency needs to be git cloned as branch 2.69.3
, if current main was cloned it would. segfault inexplicably at the compute
function.
This has been pretty straightforward, especially as the project is currently only targeting one platform (macOS).
However, static compilation and integration of chugins is not working although this is indicated as possible in the makefiles: static chugins were not recognized when invoked via test scripts.
The directories chuck/src/core
, chuck/src/host
, chuck/src/host-embed
are directly included for the sake of practicality in the chuck-max/source/projects/chuck~
folder. Typically the chuck
repo would have been included as an external dependency and downloaded and built when required.
The prior reason was that the chuck
repo until recently, even at --depth=1
, required more than 1 GB of storage and could take ages to download.
With this problem solved, a script (source/scripts/update.sh
) is preferred to including chuck and chugins directories as git submodules.
The initial audio processing algorithm was pretty much taken from David Braun's ChucKDesigner processBlock
function in Plugin_ChucK.cpp
CHUCKDESIGNERSHARED_API bool processBlock(unsigned int chuckID, const float** inBuffer, int inBufferNumChannels, int inBufferNumSamples, float* inChucKBuffer, float* outChucKBuffer, float** outBuffer, int numOutSamples, int numOutChannels)
{
if (chuck_instances.count(chuckID) == 0) {
return false;
}
ChucK* chuck = chuck_instances[chuckID];
int numOutChans = chuck->vm()->m_num_dac_channels;
if (numOutChans != numOutChannels) {
return false;
}
int numSamples;
int numInChannels = std::min<int>(inBufferNumChannels, (int)chuck->vm()->m_num_adc_channels);
for (int i = 0; i < numOutSamples; i += CHUCKDESIGNERCHOP_BUFFER_SIZE) {
// chuck->run(inbuffer, *output->channels, output->numSamples); // this doesn't work because of interleaved samples.
// Chuck returns LRLRLRLR but for touchdesigner we want LLLLRRRR.
// Therefore we must use an intermediate buffer
float* inPtr = inChucKBuffer;
numSamples = min(CHUCKDESIGNERCHOP_BUFFER_SIZE, numOutSamples - i);
if (inBuffer) {
for (int samp = i; samp < std::min<int>(inBufferNumSamples, i + CHUCKDESIGNERCHOP_BUFFER_SIZE); samp++) {
for (int chan = 0; chan < numInChannels; chan++) {
*(inPtr++) = inBuffer[chan][samp];
}
}
}
float* outPtr = outChucKBuffer;
chuck->run(inChucKBuffer, outChucKBuffer, numSamples);
for (int samp = 0; samp < numSamples; samp++) {
for (int chan = 0; chan < numOutChans; chan++) {
outBuffer[chan][i + samp] = *outPtr++;
}
}
}
return true;
}
This was initially changed to this:
void ck_perform64(t_ck *x, t_object *dsp64, double **ins, long numins, double **outs, long numouts, long sampleframes, long flags, void *userparam)
{
int n = sampleframes; // n = 64
int numSamples;
int numOutSamples = n;
int inBufferNumSamples = n;
for (int i = 0; i < numOutSamples; i += BUFFER_SIZE) {
float* inPtr = x->in_chuck_buffer;
numSamples = min(BUFFER_SIZE, numOutSamples - i);
if (ins) {
for (int samp = i; samp < std::min<int>(inBufferNumSamples, i + BUFFER_SIZE); samp++) {
for (int chan = 0; chan < N_IN_CHANNELS; chan++) {
*(inPtr++) = ins[chan][samp];
}
}
}
float* outPtr = x->out_chuck_buffer;
x->chuck->run(x->in_chuck_buffer, x->out_chuck_buffer, numSamples);
for (int samp = 0; samp < numSamples; samp++) {
for (int chan = 0; chan < N_OUT_CHANNELS; chan++) {
outs[chan][i + samp] = *outPtr++;
}
}
}
}
and finally, it was simplified to the following with the help of Professor GE Wang
void ck_perform64(t_ck* x, t_object* dsp64, double** ins, long numins,
double** outs, long numouts, long sampleframes, long flags,
void* userparam)
{
float* in_ptr = x->in_chuck_buffer;
float* out_ptr = x->out_chuck_buffer;
long n = sampleframes; // n = 64
if (ins) {
for (int i = 0; i < n; i++) {
for (int chan = 0; chan < numins; chan++) {
*(in_ptr++) = ins[chan][i];
}
}
}
x->chuck->run(x->in_chuck_buffer, x->out_chuck_buffer, n);
for (int i = 0; i < n; i++) {
for (int chan = 0; chan < numouts; chan++) {
outs[chan][i] = *out_ptr++;
}
}
}
As a general rule, if the chuck code will not run in chuck, then it will not run in chuck-max.
So global parameters should be initialized in the chuck script as normal and chuck-max will change the value of initialization on opening. It practice, this is a non-issue.
We have added the new global keyword to enable integrated communication between ChucK code and the outside environment that ChucK is embedded in (the embedding host). The
global
keyword is used when declaring the type of a variable, such as (A) below
public class EventResponder : MonoBehaviour {
private ChuckSubInstance myChuck;
void Start() {
myChuck = GetComponent<ChuckSubInstance>();
// broadcast "notifier" every 250 ms
myChuck.RunCode( @"
global Event notifier; // <- Here (A)
while( true ) {
notifier.broadcast();
250::ms => now;
} ");
// create a ChuckEventListener
ChuckEventListener listener = gameObject
.AddComponent<ChuckEventListener>();
// call MyCallback() during Update()
// after every broadcast from "notifier"
listener.ListenForEvent( myChuck, "notifier", // <- Here (B)
MyCallback );
}
void MyCallback() {
// react to event (rotate my object)
transform.Rotate( new Vector3( 5, 10, 15 ) );
}
}
The main guiding principle in the design of this keyword is that it is not necessary for ChucK to know anything about the embedding host, or whether it is embedded at all. Instead,
global
variables appear like normal variables within their own ChucK script, but can be inspected, edited, or listened to by other ChucK scripts or by the embedding host.So far, the
global
keyword is enabled for three types of variables;
The first type of global variable is primitives:
ints
,floats
, andstrings
. The embedding host can get and set their values. The get operation requires the use of a callback because the embedding host often runs on a different thread than the audio thread.The second type of global variable is
Events
. ChucK Events are used to pause execution in a ChucK script until the Event signals that it has occurred. The embedding host can signal or broadcast a global Event (i.e. trigger one or all ChucK scripts waiting on the event). The embedding host can also register a callback to be called every time a global Event is broadcast, as (B) in the code example above. This callback to user code occurs on the audio thread and thus is timed with sample-level accuracy; a tighter integration of timing between audio and visuals is not achievable.The third type of global variable is
UGens
(unit generators). ChucK UGens are signal processing elements that generate streams of audio. The embedding host can fetch a global UGen’s most recent samples.... The desire to embed ChucK in Unity motivated the wider libChucK rearchitecture project, which enables ChucK to act as an embeddable component in any C++ project.
The ChucK source was separated into core and host code- bases. The core comprises the language parser, which compiles code, and virtual machine (VM), which translates audio inputs to outputs. One embeds ChucK in a new project by simply writing a new host that calls these functions.
The rearchitecture allowed multiple VMs to exist in the same address space (useful for contexts where the number of channels is limited and multiple outputs are desired, such as in a digital audio plugin or Unity’s spatial audio system). It also enabled the redirection of all ChucK error messages to an optional callback (e.g. the Unity debug console).
See above section on events.. "ChucK Events are used to pause execution in a ChucK script until the Event signals that it has occurred."
Documented here
Relevant interface is provided in chuck/core/chuck_oo.h
and chuck/core/chuck_globals.h
There are local events and global
events, see test-local-event.ck
and test-global-event.ck
for examples of both. Trigger a single named event is via ck_sigal
, and all events of the same name via ck_broadcast
which implement:
t_CKBOOL Chuck_Globals_Manager::signalGlobalEvent( const char * name );
t_CKBOOL Chuck_Globals_Manager::broadcastGlobalEvent( const char * name );
t_CKBOOL getGlobalUGenSamples( const char * name, SAMPLE* buffer, int numFrames );
Need example of how to use this.
$<$<AND:$<BOOL:${WIN32}>,$<BOOL:${ENABLE_WARNINGS}>>:/W4>
Console level flags
defs
__WINDOWS_DS__
__WINDOWS_ASIO__
__WINDOWS_WASAPI__
WIN32
NDEBUG for debug, _DEBUG for release
_CONSOLE
console
dsound.lib
dinput8.lib
dxguid.lib
wsock32.lib
ws2_32.lib
iphlpapi.lib
winmm.lib
..\core\lo\address.c
..\core\lo\blob.c
..\core\lo\bundle.c
..\core\lo\message.c
..\core\lo\method.c
..\core\lo\pattern_match.c
..\core\lo\server.c
..\core\lo\send.c
..\core\lo\timetag.c
defs
HAVE_CONFIG_H
__PLATFORM_WIN32__
__WINDOWS_DS__
WIN32
NDEBUG
_CONSOLE
additional_includes:
lo
Add an insert <filename | code> [shredID=last]
command to chuck core vm messages.
The insert
command corresponds to the hypothetical msg type CK_MSG_INSERT
where the default op looks like insert <filename|code> <filename | code> [shredID=last]
applying to the following scenario:
if shred_1
is launched which consists of
SoundSource s => dac;
1::hour => now;
It should be possible to insert fx_1.ck
as a second shred_fx_1
which operates as an effect between shred_1
and the dac
such that effectively
shred_1 => shred_fx_1 => dac
of course, it should be possible to similiarly insert another shred_fx_2
via insert fx_2.ck
shred_1 => shred_fx_1 => shred_fx_2 => dac
and so on ...
An optional additional argument to specify shred_id as in insert <filename|code> <shredID>
, allows for specifying exactly where to insert in the chain..
It would make callbacks a lot more useful if at least one consistent variant of available callbacks followed the example of getAllGlobalVariables
.
t_CKBOOL getAllGlobalVariables( void (*callback)( const std::vector<Chuck_Globals_TypeValue> & list, void * data ), void * data = NULL );
referencing @godlygeek on python discord #c-extensions sub discord:
there aren't closures in C, but there are in C++ . In C, the typical setup is that every library function that accepts a callback (pointer to some function) also accepts a user data object (void user_data). The library stores both pointers, and when it calls the callback it passes the user_data pointer to it as well and you, as the user of that library, would pass your Python callable as the user data, and a C function as the callback. The library would call the C function and pass the user data pointer as one of its arguments. The C function would cast the user data argument back to PyObject and then call using PyObject_CallObject or some such