chris@552: /* chris@555: ____ _____ _ _ chris@555: | __ )| ____| | / \ chris@555: | _ \| _| | | / _ \ chris@555: | |_) | |___| |___ / ___ \ chris@555: |____/|_____|_____/_/ \_\ chris@555: chris@555: The platform for ultra-low latency audio and sensor processing chris@555: chris@555: http://bela.io chris@555: chris@555: A project of the Augmented Instruments Laboratory within the chris@555: Centre for Digital Music at Queen Mary University of London. chris@555: http://www.eecs.qmul.ac.uk/~andrewm chris@555: chris@555: (c) 2016 Augmented Instruments Laboratory: Andrew McPherson, chris@555: Astrid Bin, Liam Donovan, Christian Heinrichs, Robert Jack, chris@555: Giulio Moro, Laurel Pardue, Victor Zappi. All rights reserved. chris@555: chris@555: The Bela software is distributed under the GNU Lesser General Public License chris@555: (LGPL 3.0), available here: https://www.gnu.org/licenses/lgpl-3.0.txt chris@555: */ chris@555: chris@555: /* chris@555: * USING A CUSTOM RENDER.CPP FILE FOR PUREDATA PATCHES - HEAVY chris@555: * =========================================================== chris@555: * || || chris@555: * || OPEN THE ENCLOSED _main.pd PATCH FOR MORE INFORMATION || chris@555: * || ----------------------------------------------------- || chris@555: * =========================================================== chris@552: */ chris@552: chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: #include chris@552: chris@552: /* chris@552: * MODIFICATION chris@552: * ------------ chris@552: * Global variables for tremolo effect applied to libpd output chris@552: */ chris@552: chris@552: float gTremoloRate = 4.0; chris@552: float gPhase; chris@552: chris@552: /*********/ chris@552: chris@552: /* chris@552: * HEAVY CONTEXT & BUFFERS chris@552: */ chris@552: chris@552: Hv_bela *gHeavyContext; chris@552: float *gHvInputBuffers = NULL, *gHvOutputBuffers = NULL; chris@552: unsigned int gHvInputChannels = 0, gHvOutputChannels = 0; chris@552: chris@552: float gInverseSampleRate; chris@552: chris@552: /* chris@552: * HEAVY FUNCTIONS chris@552: */ chris@552: chris@552: // TODO: rename this chris@552: #define LIBPD_DIGITAL_OFFSET 11 // digitals are preceded by 2 audio and 8 analogs (even if using a different number of analogs) chris@552: chris@552: void printHook(double timestampSecs, const char *printLabel, const char *msgString, void *userData) { chris@552: rt_printf("Message from Heavy patch: [@ %.3f] %s: %s\n", timestampSecs, printLabel, msgString); chris@552: } chris@552: chris@552: chris@552: // digitals chris@552: static DigitalChannelManager dcm; chris@552: chris@552: void sendDigitalMessage(bool state, unsigned int delay, void* receiverName){ chris@552: hv_sendFloatToReceiver(gHeavyContext, hv_stringToHash((char*)receiverName), (float)state); chris@552: // rt_printf("%s: %d\n", (char*)receiverName, state); chris@552: } chris@552: chris@552: // TODO: turn them into hv hashes and adjust sendDigitalMessage accordingly chris@552: char hvDigitalInHashes[16][21]={ chris@552: {"bela_digitalIn11"},{"bela_digitalIn12"},{"bela_digitalIn13"},{"bela_digitalIn14"},{"bela_digitalIn15"}, chris@552: {"bela_digitalIn16"},{"bela_digitalIn17"},{"bela_digitalIn18"},{"bela_digitalIn19"},{"bela_digitalIn20"}, chris@552: {"bela_digitalIn21"},{"bela_digitalIn22"},{"bela_digitalIn23"},{"bela_digitalIn24"},{"bela_digitalIn25"}, chris@552: {"bela_digitalIn26"} chris@552: }; chris@552: chris@552: static void sendHook( chris@552: double timestamp, // in milliseconds chris@552: const char *receiverName, chris@552: const HvMessage *const m, chris@552: void *userData) { chris@552: chris@552: /* chris@552: * MODIFICATION chris@552: * ------------ chris@552: * Parse float sent to receiver 'tremoloRate' and assign it to a global variable chris@552: */ chris@552: chris@552: if(strncmp(receiverName, "tremoloRate", 11) == 0){ chris@552: float value = hv_msg_getFloat(m, 0); // see the Heavy C API documentation: https://enzienaudio.com/docs/index.html#8.c chris@552: gTremoloRate = value; chris@552: } chris@552: chris@552: /*********/ chris@552: chris@552: // Bela digital chris@552: chris@552: // Bela digital run-time messages chris@552: chris@552: // TODO: this first block is almost an exact copy of libpd's code, should we add this to the class? chris@552: // let's make this as optimized as possible for built-in digital Out parsing chris@552: // the built-in digital receivers are of the form "bela_digitalOutXX" where XX is between 11 and 26 chris@552: static int prefixLength = 15; // strlen("bela_digitalOut") chris@552: if(strncmp(receiverName, "bela_digitalOut", prefixLength)==0){ chris@552: if(receiverName[prefixLength] != 0){ //the two ifs are used instead of if(strlen(source) >= prefixLength+2) chris@552: if(receiverName[prefixLength + 1] != 0){ chris@552: // quickly convert the suffix to integer, assuming they are numbers, avoiding to call atoi chris@552: int receiver = ((receiverName[prefixLength] - 48) * 10); chris@552: receiver += (receiverName[prefixLength+1] - 48); chris@552: unsigned int channel = receiver - LIBPD_DIGITAL_OFFSET; // go back to the actual Bela digital channel number chris@552: bool value = hv_msg_getFloat(m, 0); chris@552: if(channel < 16){ //16 is the hardcoded value for the number of digital channels chris@552: dcm.setValue(channel, value); chris@552: } chris@552: } chris@552: } chris@552: } chris@552: chris@552: // Bela digital initialization messages chris@552: if(strcmp(receiverName, "bela_setDigital") == 0){ chris@552: // Third argument (optional) can be ~ or sig for signal-rate, message-rate otherwise. chris@552: // [in 14 ~( chris@552: // | chris@552: // [s bela_setDigital] chris@552: // is signal("sig" or "~") or message("message", default) rate chris@552: bool isMessageRate = true; // defaults to message rate chris@552: bool direction = 0; // initialize it just to avoid the compiler's warning chris@552: bool disable = false; chris@552: int numArgs = hv_msg_getNumElements(m); chris@552: if(numArgs < 2 || numArgs > 3 || !hv_msg_isSymbol(m, 0) || !hv_msg_isFloat(m, 1)) chris@552: return; chris@552: if(numArgs == 3 && !hv_msg_isSymbol(m,2)) chris@552: return; chris@552: char * symbol = hv_msg_getSymbol(m, 0); chris@552: chris@552: if(strcmp(symbol, "in") == 0){ chris@552: direction = INPUT; chris@552: } else if(strcmp(symbol, "out") == 0){ chris@552: direction = OUTPUT; chris@552: } else if(strcmp(symbol, "disable") == 0){ chris@552: disable = true; chris@552: } else { chris@552: return; chris@552: } chris@552: int channel = hv_msg_getFloat(m, 1) - LIBPD_DIGITAL_OFFSET; chris@552: if(disable == true){ chris@552: dcm.unmanage(channel); chris@552: return; chris@552: } chris@552: if(numArgs >= 3){ chris@552: char* s = hv_msg_getSymbol(m, 2); chris@552: if(strcmp(s, "~") == 0 || strncmp(s, "sig", 3) == 0){ chris@552: isMessageRate = false; chris@552: } chris@552: } chris@552: dcm.manage(channel, direction, isMessageRate); chris@552: } chris@552: } chris@552: chris@552: chris@552: /* chris@552: * SETUP, RENDER LOOP & CLEANUP chris@552: */ chris@552: chris@552: // leaving this here, trying to come up with a coherent interface with libpd. chris@552: // commenting them out so the compiler does not warn chris@552: // 2 audio + (up to)8 analog + (up to) 16 digital + 4 scope outputs chris@552: //static const unsigned int gChannelsInUse = 30; chris@552: //static unsigned int gAnalogChannelsInUse = 8; // hard-coded for the moment, TODO: get it at run-time from hv_context chris@552: //static const unsigned int gFirstAudioChannel = 0; chris@552: //static const unsigned int gFirstAnalogChannel = 2; chris@552: static const unsigned int gFirstDigitalChannel = 10; chris@552: static const unsigned int gFirstScopeChannel = 26; chris@552: static unsigned int gDigitalSigInChannelsInUse; chris@552: static unsigned int gDigitalSigOutChannelsInUse; chris@552: chris@552: // Bela Midi chris@552: Midi midi; chris@552: unsigned int hvMidiHashes[7]; chris@552: // Bela Scope chris@552: Scope scope; chris@552: unsigned int gScopeChannelsInUse; chris@552: float* gScopeOut; chris@552: chris@552: chris@552: bool setup(BelaContext *context, void *userData) { chris@552: if(context->audioInChannels != context->audioOutChannels || chris@552: context->analogInChannels != context->analogOutChannels){ chris@552: // It should actually work, but let's test it before releasing it! chris@552: printf("Error: TODO: a different number of channels for inputs and outputs is not yet supported\n"); chris@552: return false; chris@552: } chris@552: chris@552: /* chris@552: * MODIFICATION chris@552: * ------------ chris@552: * Initialise variables for tremolo effect chris@552: */ chris@552: chris@552: gPhase = 0.0; chris@552: chris@552: /*********/ chris@552: chris@552: /* HEAVY */ chris@552: hvMidiHashes[kmmNoteOn] = hv_stringToHash("__hv_notein"); chris@552: // hvMidiHashes[kmmNoteOff] = hv_stringToHash("noteoff"); // this is handled differently, see the render function chris@552: hvMidiHashes[kmmControlChange] = hv_stringToHash("__hv_ctlin"); chris@552: // Note that the ones below are not defined by Heavy, but they are here for (wishing) forward-compatibility chris@552: // You need to receive from the corresponding symbol in Pd and unpack the message, e.g.: chris@552: //[r __hv_pgmin] chris@552: //| chris@552: //[unpack f f] chris@552: //| | chris@552: //| [print pgmin_channel] chris@552: //[print pgmin_number] chris@552: hvMidiHashes[kmmProgramChange] = hv_stringToHash("__hv_pgmin"); chris@552: hvMidiHashes[kmmPolyphonicKeyPressure] = hv_stringToHash("__hv_polytouchin"); chris@552: hvMidiHashes[kmmChannelPressure] = hv_stringToHash("__hv_touchin"); chris@552: hvMidiHashes[kmmPitchBend] = hv_stringToHash("__hv_bendin"); chris@552: chris@552: gHeavyContext = hv_bela_new(context->audioSampleRate); chris@552: chris@552: gHvInputChannels = hv_getNumInputChannels(gHeavyContext); chris@552: gHvOutputChannels = hv_getNumOutputChannels(gHeavyContext); chris@552: chris@552: gScopeChannelsInUse = gHvOutputChannels > gFirstScopeChannel ? chris@552: gHvOutputChannels - gFirstScopeChannel : 0; chris@552: gDigitalSigInChannelsInUse = gHvInputChannels > gFirstDigitalChannel ? chris@552: gHvInputChannels - gFirstDigitalChannel : 0; chris@552: gDigitalSigOutChannelsInUse = gHvOutputChannels > gFirstDigitalChannel ? chris@552: gHvOutputChannels - gFirstDigitalChannel - gScopeChannelsInUse: 0; chris@552: chris@552: printf("Starting Heavy context with %d input channels and %d output channels\n", chris@552: gHvInputChannels, gHvOutputChannels); chris@552: printf("Channels in use:\n"); chris@552: printf("Digital in : %u, Digital out: %u\n", gDigitalSigInChannelsInUse, gDigitalSigOutChannelsInUse); chris@552: printf("Scope out: %u\n", gScopeChannelsInUse); chris@552: chris@552: if(gHvInputChannels != 0) { chris@552: gHvInputBuffers = (float *)calloc(gHvInputChannels * context->audioFrames,sizeof(float)); chris@552: } chris@552: if(gHvOutputChannels != 0) { chris@552: gHvOutputBuffers = (float *)calloc(gHvOutputChannels * context->audioFrames,sizeof(float)); chris@552: } chris@552: chris@552: gInverseSampleRate = 1.0 / context->audioSampleRate; chris@552: chris@552: // Set heavy print hook chris@552: hv_setPrintHook(gHeavyContext, printHook); chris@552: // Set heavy send hook chris@552: hv_setSendHook(gHeavyContext, sendHook); chris@552: chris@552: // TODO: change these hardcoded port values and actually change them in the Midi class chris@552: midi.readFrom(0); chris@552: midi.writeTo(0); chris@552: midi.enableParser(true); chris@552: chris@552: if(gScopeChannelsInUse > 0){ chris@552: // block below copy/pasted from libpd, except chris@552: scope.setup(gScopeChannelsInUse, context->audioSampleRate); chris@552: gScopeOut = new float[gScopeChannelsInUse]; chris@552: } chris@552: // Bela digital chris@552: dcm.setCallback(sendDigitalMessage); chris@552: if(context->digitalChannels > 0){ chris@552: for(unsigned int ch = 0; ch < context->digitalChannels; ++ch){ chris@552: dcm.setCallbackArgument(ch, hvDigitalInHashes[ch]); chris@552: } chris@552: } chris@552: // unlike libpd, no need here to bind the bela_digitalOut.. receivers chris@552: chris@552: return true; chris@552: } chris@552: chris@552: chris@552: void render(BelaContext *context, void *userData) chris@552: { chris@552: { chris@552: int num; chris@552: while((num = midi.getParser()->numAvailableMessages()) > 0){ chris@552: static MidiChannelMessage message; chris@552: message = midi.getParser()->getNextChannelMessage(); chris@552: switch(message.getType()){ chris@552: case kmmNoteOn: { chris@552: //message.prettyPrint(); chris@552: int noteNumber = message.getDataByte(0); chris@552: int velocity = message.getDataByte(1); chris@552: int channel = message.getChannel(); chris@552: // rt_printf("message: noteNumber: %f, velocity: %f, channel: %f\n", noteNumber, velocity, channel); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmNoteOn], 0, "fff", chris@552: (float)noteNumber, (float)velocity, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmNoteOff: { chris@552: /* PureData does not seem to handle noteoff messages as per the MIDI specs, chris@552: * so that the noteoff velocity is ignored. Here we convert them to noteon chris@552: * with a velocity of 0. chris@552: */ chris@552: int noteNumber = message.getDataByte(0); chris@552: // int velocity = message.getDataByte(1); // would be ignored by Pd chris@552: int channel = message.getChannel(); chris@552: // note we are sending the below to hvHashes[kmmNoteOn] !! chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmNoteOn], 0, "fff", chris@552: (float)noteNumber, (float)0, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmControlChange: { chris@552: int channel = message.getChannel(); chris@552: int controller = message.getDataByte(0); chris@552: int value = message.getDataByte(1); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmControlChange], 0, "fff", chris@552: (float)value, (float)controller, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmProgramChange: { chris@552: int channel = message.getChannel(); chris@552: int program = message.getDataByte(0); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmProgramChange], 0, "ff", chris@552: (float)program, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmPolyphonicKeyPressure: { chris@552: //TODO: untested, I do not have anything with polyTouch... who does, anyhow? chris@552: int channel = message.getChannel(); chris@552: int pitch = message.getDataByte(0); chris@552: int value = message.getDataByte(1); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmPolyphonicKeyPressure], 0, "fff", chris@552: (float)channel+1, (float)pitch, (float)value); chris@552: break; chris@552: } chris@552: case kmmChannelPressure: chris@552: { chris@552: int channel = message.getChannel(); chris@552: int value = message.getDataByte(0); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmChannelPressure], 0, "ff", chris@552: (float)value, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmPitchBend: chris@552: { chris@552: int channel = message.getChannel(); chris@552: int value = ((message.getDataByte(1) << 7) | message.getDataByte(0)); chris@552: hv_vscheduleMessageForReceiver(gHeavyContext, hvMidiHashes[kmmPitchBend], 0, "ff", chris@552: (float)value, (float)channel+1); chris@552: break; chris@552: } chris@552: case kmmNone: chris@552: case kmmAny: chris@552: break; chris@552: } chris@552: } chris@552: } chris@552: chris@552: // De-interleave the data chris@552: if(gHvInputBuffers != NULL) { chris@552: for(unsigned int n = 0; n < context->audioFrames; n++) { chris@552: for(unsigned int ch = 0; ch < gHvInputChannels; ch++) { chris@552: if(ch >= context->audioInChannels+context->analogInChannels) { chris@552: // THESE ARE PARAMETER INPUT 'CHANNELS' USED FOR ROUTING chris@552: // 'sensor' outputs from routing channels of dac~ are passed through here chris@552: break; chris@552: } else { chris@552: // If more than 2 ADC inputs are used in the pd patch, route the analog inputs chris@552: // i.e. ADC3->analogIn0 etc. (first two are always audio inputs) chris@552: if(ch >= context->audioInChannels) { chris@552: int m = n/2; chris@552: float mIn = context->analogIn[m*context->analogInChannels + (ch-context->audioInChannels)]; chris@552: gHvInputBuffers[ch * context->audioFrames + n] = mIn; chris@552: } else { chris@552: gHvInputBuffers[ch * context->audioFrames + n] = context->audioIn[n * context->audioInChannels + ch]; chris@552: } chris@552: } chris@552: } chris@552: } chris@552: } chris@552: chris@552: // Bela digital in chris@552: // note: in multiple places below we assume that the number of digital frames is same as number of audio chris@552: // Bela digital in at message-rate chris@552: dcm.processInput(context->digital, context->digitalFrames); chris@552: chris@552: // Bela digital in at signal-rate chris@552: if(gDigitalSigInChannelsInUse > 0) chris@552: { chris@552: unsigned int j, k; chris@552: float *p0, *p1; chris@552: const unsigned int gLibpdBlockSize = context->audioFrames; chris@552: const unsigned int audioFrameBase = 0; chris@552: float* gInBuf = gHvInputBuffers; chris@552: // block below copy/pasted from libpd, except chris@552: // 16 has been replaced with gDigitalSigInChannelsInUse chris@552: for (j = 0, p0 = gInBuf; j < gLibpdBlockSize; j++, p0++) { chris@552: unsigned int digitalFrame = audioFrameBase + j; chris@552: for (k = 0, p1 = p0 + gLibpdBlockSize * gFirstDigitalChannel; chris@552: k < gDigitalSigInChannelsInUse; ++k, p1 += gLibpdBlockSize) { chris@552: if(dcm.isSignalRate(k) && dcm.isInput(k)){ // only process input channels that are handled at signal rate chris@552: *p1 = digitalRead(context, digitalFrame, k); chris@552: } chris@552: } chris@552: } chris@552: } chris@552: chris@552: chris@552: // replacement for bang~ object chris@552: //hv_vscheduleMessageForReceiver(gHeavyContext, "bela_bang", 0.0f, "b"); chris@552: chris@552: hv_bela_process_inline(gHeavyContext, gHvInputBuffers, gHvOutputBuffers, context->audioFrames); chris@552: chris@552: // Bela digital out chris@552: // Bela digital out at signal-rate chris@552: if(gDigitalSigOutChannelsInUse > 0) chris@552: { chris@552: unsigned int j, k; chris@552: float *p0, *p1; chris@552: const unsigned int gLibpdBlockSize = context->audioFrames; chris@552: const unsigned int audioFrameBase = 0; chris@552: float* gOutBuf = gHvOutputBuffers; chris@552: // block below copy/pasted from libpd, except chris@552: // context->digitalChannels has been replaced with gDigitalSigOutChannelsInUse chris@552: for (j = 0, p0 = gOutBuf; j < gLibpdBlockSize; ++j, ++p0) { chris@552: unsigned int digitalFrame = (audioFrameBase + j); chris@552: for (k = 0, p1 = p0 + gLibpdBlockSize * gFirstDigitalChannel; chris@552: k < gDigitalSigOutChannelsInUse; k++, p1 += gLibpdBlockSize) { chris@552: if(dcm.isSignalRate(k) && dcm.isOutput(k)){ // only process output channels that are handled at signal rate chris@552: digitalWriteOnce(context, digitalFrame, k, *p1 > 0.5); chris@552: } chris@552: } chris@552: } chris@552: } chris@552: // Bela digital out at message-rate chris@552: dcm.processOutput(context->digital, context->digitalFrames); chris@552: chris@552: // Bela scope chris@552: if(gScopeChannelsInUse > 0) chris@552: { chris@552: unsigned int j, k; chris@552: float *p0, *p1; chris@552: const unsigned int gLibpdBlockSize = context->audioFrames; chris@552: float* gOutBuf = gHvOutputBuffers; chris@552: chris@552: // block below copy/pasted from libpd chris@552: for (j = 0, p0 = gOutBuf; j < gLibpdBlockSize; ++j, ++p0) { chris@552: for (k = 0, p1 = p0 + gLibpdBlockSize * gFirstScopeChannel; k < gScopeChannelsInUse; k++, p1 += gLibpdBlockSize) { chris@552: gScopeOut[k] = *p1; chris@552: } chris@552: scope.log(gScopeOut); chris@552: } chris@552: } chris@552: chris@552: // Interleave the output data chris@552: if(gHvOutputBuffers != NULL) { chris@552: for(unsigned int n = 0; n < context->audioFrames; n++) { chris@552: chris@552: /* chris@552: * MODIFICATION chris@552: * ------------ chris@552: * Processing for tremolo effect while writing libpd output to Bela output buffer chris@552: */ chris@552: chris@552: // Generate a sinewave with frequency set by gTremoloRate chris@552: // and amplitude from -0.5 to 0.5 chris@552: float lfo = sinf(gPhase) * 0.5; chris@552: // Keep track and wrap the phase of the sinewave chris@552: gPhase += 2.0 * M_PI * gTremoloRate * gInverseSampleRate; chris@552: if(gPhase > 2.0 * M_PI) chris@552: gPhase -= 2.0 * M_PI; chris@552: chris@552: /*********/ chris@552: chris@552: for(unsigned int ch = 0; ch < gHvOutputChannels; ch++) { chris@552: if(ch <= context->audioOutChannels+context->analogOutChannels) { chris@552: if(ch >= context->audioOutChannels) { chris@552: int m = n/2; chris@552: context->analogOut[m * context->analogFrames + (ch-context->audioOutChannels)] = constrain(gHvOutputBuffers[ch*context->audioFrames + n],0.0,1.0); chris@552: } else { chris@552: context->audioOut[n * context->audioOutChannels + ch] = gHvOutputBuffers[ch * context->audioFrames + n] * lfo; // MODIFICATION (* lfo) chris@552: } chris@552: } chris@552: } chris@552: } chris@552: } chris@552: chris@552: } chris@552: chris@552: chris@552: void cleanup(BelaContext *context, void *userData) chris@552: { chris@552: chris@552: hv_bela_free(gHeavyContext); chris@552: if(gHvInputBuffers != NULL) chris@552: free(gHvInputBuffers); chris@552: if(gHvOutputBuffers != NULL) chris@552: free(gHvOutputBuffers); chris@552: delete[] gScopeOut; chris@552: }