diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ccbf75a2..f2e3f053 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.11.0 +current_version = 0.12.0 [bumpversion:file:setup.cfg] diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7d185c..699ee97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ===== +0.12.0 +----- + +* Core: parse remote/send messages +* Core: MIDI i/o added to memoryPool +* Core: support `[else/knob]` as `[float]` +* Daisy: set heavy context after hw.init() +* OWL: add Polytouchin and Polytouchout +* JS: webmidi input +* Docs: add instructions for loading custom samples in JS +* Small Bugfixes: + * MIDI out objects in output Paremeters + * JS: AudioWorklet fillTableWithFloatBuffer + 0.11.0 ----- diff --git a/docs/02.getting_started.md b/docs/02.getting_started.md index 5e0496f2..f3d5a931 100644 --- a/docs/02.getting_started.md +++ b/docs/02.getting_started.md @@ -102,6 +102,8 @@ This list will be continuously epanded to document differences in object behavio * Sliders and number inputs are converted to `[f ]` and thus do not store send/receive/initialization/etc. settings. * Heavy does not accept arguments and control connections to: `[rzero~]`, `[rzero_rev~]`, `[czero~]`, `[czero_rev~]`. In Heavy, these objects accept only signal inputs. Arguments and control connections are ignored. * On the `[select]` object it is currently not possible to set the arguments via the right inlet (internally a hardcoded switch_case is used). +* Heavy supports remote/send messages, however empty messages are currently removed. So the typical `[; bla 1(` multiline message needs to contain at least something on the first line: `[_; bla 1(`. * `[metro]` and `[timer]` objects do not accept tempo messages or unit arguments. -* Certain filters are sensitive to ‘blowing up’ at very low or very high cutoff frequencies and/or resonances, due to the filter coefficients not being perfectly represented with a finite number of bits. While Pure data natively uses 64 bits, platforms like `OWL` and `Daisy` that use 32 bit float are more sensitive to this. For example, the Pure data `[lp~]`, `[bp~]` and `[hp~]` filters are implemented with biquads which are prone to fail or distort with cutoff frequencies less than around 200 Hz (at 48kHz sample rate). +* `[snapshot~]` does not respond within the same control flow as it executes in signal context. Its output happens on the next audio cycle, so additional care for this control flow needs to be taken into account if you depend on synchronous execution. +* Certain filters are sensitive to ‘blowing up’ at very low or very high cutoff frequencies and/or resonances, due to the filter coefficients not being perfectly represented with a finite number of bits. While Pure data natively uses 64 bits, platforms like `OWL` and `Daisy` that use 32 bit float are more sensitive to this. For example, the Pure data `[bp~]` filter is implemented with a biquad which is prone to fail or distort with cutoff frequencies less than around 200 Hz (at 48kHz sample rate). * Heavy does not support multichannel connections. diff --git a/docs/03.gen.javascript.md b/docs/03.gen.javascript.md index 04fad9d6..e7f2b3f0 100644 --- a/docs/03.gen.javascript.md +++ b/docs/03.gen.javascript.md @@ -144,3 +144,34 @@ The JS target supports [exposing event and parameter](02.getting_started#exposin ``` Note: these are calls directly to the `AudioLib` so make sure to include `.audiolib` when sending events or messages. + +## Loading Custom Samples + +If you have a table that is externed, using the `@hv_param` annotation, it can be used to load audio files from the web page. The table will be resized to fit this sample data. + +`[table array1 100 @hv_table]` + +The webAudioContext can load any url, but typically this will be a local path. Modify your html using the following: + +```js + // Sample loading + function loadAudio(url) { + var rq = new XMLHttpRequest(); + rq.open("GET", url, true); + rq.responseType = "arraybuffer"; + rq.send(); + + rq.onload = function() { + var audioData = rq.response; + loader.webAudioContext.decodeAudioData(audioData, function(buffer){ + loader.fillTableWithFloatBuffer("array1", buffer.getChannelData(0)); + }); + } + } +``` + +This can then be called from a user action or any other mechanism that works for you: + +```html + +``` diff --git a/docs/09.supported_vanilla_objects.md b/docs/09.supported_vanilla_objects.md index 6a98bc30..b7b98cf9 100644 --- a/docs/09.supported_vanilla_objects.md +++ b/docs/09.supported_vanilla_objects.md @@ -45,6 +45,7 @@ del delay div exp +else/knob f float floatatom @@ -129,6 +130,7 @@ biquad~ bp~ catch~ clip~ +complex-mod~ cos~ cpole~ czero_rev~ @@ -142,6 +144,7 @@ delwrite~ env~ exp~ ftom~ +hilbert~ hip~ inlet~ line~ diff --git a/docs/10.unsupported_vanilla_objects.md b/docs/10.unsupported_vanilla_objects.md index 783c8a68..6a8ae7eb 100644 --- a/docs/10.unsupported_vanilla_objects.md +++ b/docs/10.unsupported_vanilla_objects.md @@ -70,12 +70,10 @@ value block~ bob~ bonk~ -complex-mod~ expr~ fexpr~ fft~ framp~ -hilbert~ ifft~ log~ loop~ diff --git a/hvcc/__init__.py b/hvcc/__init__.py index ae9e6c90..aac9e5bd 100644 --- a/hvcc/__init__.py +++ b/hvcc/__init__.py @@ -80,6 +80,57 @@ def check_extern_name_conflicts(extern_type: str, extern_list: List, results: Or "capital letters are not the only difference.") +def check_midi_objects(hvir: Dict) -> Dict: + in_midi = [] + out_midi = [] + + midi_in_objs = [ + '__hv_bendin', + '__hv_ctlin', + '__hv_midiin', + '__hv_midirealtimein', + '__hv_notein', + '__hv_pgmin', + '__hv_polytouchin', + '__hv_touchin', + ] + + midi_out_objs = [ + '__hv_bendout', + '__hv_ctlout', + '__hv_midiout', + '__hv_midioutport', + '__hv_noteout', + '__hv_pgmout', + '__hv_polytouchout', + '__hv_touchout', + ] + + for key in hvir['control']['receivers'].keys(): + if key in midi_in_objs: + in_midi.append(key) + + for key in hvir['control']['sendMessage']: + if key.get('name'): + if key['name'] in midi_out_objs: + out_midi.append(key['name']) + + return { + 'in': in_midi, + 'out': out_midi + } + + +def filter_midi_from_out_parameters(output_parameter_list: List, midi_out_objects: List) -> List: + new_out_list = [] + + for item in output_parameter_list: + if not item[0] in midi_out_objects: + new_out_list.append(item) + + return new_out_list + + def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: """ Simplifies the receiver/send and table lists by only containing values externed with @hv_param, @hv_event or @hv_table @@ -113,6 +164,12 @@ def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: table_list.sort(key=lambda x: x[0]) check_extern_name_conflicts("table", table_list, results) + # Exposed midi objects + midi_objects = check_midi_objects(hvir) + + # filter midi objects from the output parameters list + out_parameter_list = filter_midi_from_out_parameters(out_parameter_list, midi_objects['out']) + return { "parameters": { "in": in_parameter_list, @@ -122,12 +179,24 @@ def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: "in": in_event_list, "out": out_event_list }, + "midi": { + "in": midi_objects['in'], + "out": midi_objects['out'] + }, "tables": table_list, # generate patch heuristics to ensure enough memory allocated for the patch "memoryPoolSizesKb": { "internal": 10, # TODO(joe): should this increase if there are a lot of internal connections? - "inputQueue": max(2, int(len(in_parameter_list) + len(in_event_list) / 4)), - "outputQueue": max(2, int(len(out_parameter_list) + len(out_event_list) / 4)), + "inputQueue": max(2, int( + len(in_parameter_list) + + (len(in_event_list) / 4) + + len(midi_objects['in']) # TODO(dreamer): should this depend on the MIDI type? + )), + "outputQueue": max(2, int( + len(out_parameter_list) + + (len(out_event_list) / 4) + + len(midi_objects['out']) + )), } } diff --git a/hvcc/generators/c2daisy/c2daisy.py b/hvcc/generators/c2daisy/c2daisy.py index f09747f5..df0b3431 100644 --- a/hvcc/generators/c2daisy/c2daisy.py +++ b/hvcc/generators/c2daisy/c2daisy.py @@ -89,6 +89,7 @@ def compile( component_glue['displayprocess'] = board_info['displayprocess'] component_glue['debug_printing'] = daisy_meta.get('debug_printing', False) component_glue['usb_midi'] = daisy_meta.get('usb_midi', False) + component_glue['pool_sizes_kb'] = externs["memoryPoolSizesKb"] # samplerate samplerate = daisy_meta.get('samplerate', 48000) diff --git a/hvcc/generators/c2daisy/templates/HeavyDaisy.cpp b/hvcc/generators/c2daisy/templates/HeavyDaisy.cpp index 72190bfe..a67444a8 100644 --- a/hvcc/generators/c2daisy/templates/HeavyDaisy.cpp +++ b/hvcc/generators/c2daisy/templates/HeavyDaisy.cpp @@ -37,7 +37,7 @@ using namespace daisy; json2daisy::Daisy{{ class_name|capitalize }} hardware; -Heavy_{{patch_name}} hv(SAMPLE_RATE); +Heavy_{{patch_name}}* hv; void audiocallback(daisy::AudioHandle::InputBuffer in, daisy::AudioHandle::OutputBuffer out, size_t size); static void sendHook(HeavyContextInterface *c, const char *receiverName, uint32_t receiverHash, const HvMessage * m); @@ -50,8 +50,8 @@ daisy::MidiUsbHandler midiusb; {% endif %} // int midiOutCount; // uint8_t* midiOutData; -void CallbackWriteIn(Heavy_{{patch_name}}& hv); -void LoopWriteIn(Heavy_{{patch_name}}& hv); +void CallbackWriteIn(Heavy_{{patch_name}}* hv); +void LoopWriteIn(Heavy_{{patch_name}}* hv); void CallbackWriteOut(); void LoopWriteOut(); void PostProcess(); @@ -100,7 +100,7 @@ DaisyHvParamOut DaisyOutputParameters[DaisyNumOutputParameters] = { void HandleMidiMessage(MidiEvent m) { for (int i = 0; i <= 2; ++i) { - hv.sendMessageToReceiverV(HV_HASH_MIDIIN, 0, "ff", + hv->sendMessageToReceiverV(HV_HASH_MIDIIN, 0, "ff", (float) m.data[i], (float) m.channel); } @@ -132,13 +132,13 @@ void HandleMidiMessage(MidiEvent m) break; } - hv.sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, "ff", + hv->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, "ff", (float) srtType); break; } case NoteOff: { NoteOnEvent p = m.AsNoteOn(); - hv.sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", + hv->sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", (float) p.note, // pitch (float) 0, // velocity (float) p.channel); @@ -146,7 +146,7 @@ void HandleMidiMessage(MidiEvent m) } case NoteOn: { NoteOnEvent p = m.AsNoteOn(); - hv.sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", + hv->sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", (float) p.note, // pitch (float) p.velocity, // velocity (float) p.channel); @@ -154,7 +154,7 @@ void HandleMidiMessage(MidiEvent m) } case PolyphonicKeyPressure: { // polyphonic aftertouch PolyphonicKeyPressureEvent p = m.AsPolyphonicKeyPressure(); - hv.sendMessageToReceiverV(HV_HASH_POLYTOUCHIN, 0, "fff", + hv->sendMessageToReceiverV(HV_HASH_POLYTOUCHIN, 0, "fff", (float) p.pressure, // pressure (float) p.note, // note (float) p.channel); @@ -162,7 +162,7 @@ void HandleMidiMessage(MidiEvent m) } case ControlChange: { ControlChangeEvent p = m.AsControlChange(); - hv.sendMessageToReceiverV(HV_HASH_CTLIN, 0, "fff", + hv->sendMessageToReceiverV(HV_HASH_CTLIN, 0, "fff", (float) p.value, // value (float) p.control_number, // cc number (float) p.channel); @@ -170,14 +170,14 @@ void HandleMidiMessage(MidiEvent m) } case ProgramChange: { ProgramChangeEvent p = m.AsProgramChange(); - hv.sendMessageToReceiverV(HV_HASH_PGMIN, 0, "ff", + hv->sendMessageToReceiverV(HV_HASH_PGMIN, 0, "ff", (float) p.program, (float) p.channel); break; } case ChannelPressure: { ChannelPressureEvent p = m.AsChannelPressure(); - hv.sendMessageToReceiverV(HV_HASH_TOUCHIN, 0, "ff", + hv->sendMessageToReceiverV(HV_HASH_TOUCHIN, 0, "ff", (float) p.pressure, (float) p.channel); break; @@ -186,7 +186,7 @@ void HandleMidiMessage(MidiEvent m) PitchBendEvent p = m.AsPitchBend(); // combine 7bit lsb and msb into 32bit int hv_uint32_t value = (((hv_uint32_t) m.data[1]) << 7) | ((hv_uint32_t) m.data[0]); - hv.sendMessageToReceiverV(HV_HASH_BENDIN, 0, "ff", + hv->sendMessageToReceiverV(HV_HASH_BENDIN, 0, "ff", (float) value, (float) p.channel); break; @@ -200,6 +200,8 @@ void HandleMidiMessage(MidiEvent m) int main(void) { hardware.Init(true); + hv = new Heavy_{{patch_name}}(SAMPLE_RATE, {{pool_sizes_kb.internal}}, {{pool_sizes_kb.inputQueue}}, {{pool_sizes_kb.outputQueue}}); + {% if samplerate %} hardware.SetAudioSampleRate({{samplerate}}); {% endif %} @@ -220,12 +222,12 @@ int main(void) hardware.StartAudio(audiocallback); {% if debug_printing %} hardware.som.StartLog(); - hv.setPrintHook(printHook); + hv->setPrintHook(printHook); uint32_t now = System::GetNow(); uint32_t log_time = System::GetNow(); {% endif %} - hv.setSendHook(sendHook); + hv->setSendHook(sendHook); for(;;) { @@ -288,7 +290,7 @@ void audiocallback(daisy::AudioHandle::InputBuffer in, daisy::AudioHandle::Outpu hardware.ProcessAllControls(); CallbackWriteIn(hv); {% endif %} - hv.process((float**)in, (float**)out, size); + hv->process((float**)in, (float**)out, size); {% if output_parameters|length > 0 %} CallbackWriteOut(); {% endif %} @@ -472,14 +474,14 @@ static void printHook(HeavyContextInterface *c, const char *printLabel, const ch /** Sends signals from the Daisy hardware to the PD patch via the receive objects during the main loop * */ -void LoopWriteIn(Heavy_{{patch_name}}& hv) +void LoopWriteIn(Heavy_{{patch_name}}* hv) { {% for param in loop_write_in %} {% if param.bool %} if ({{param.process}}) - hv.sendBangToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}); + hv->sendBangToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}); {% else %} - hv.sendFloatToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}, {{param.process}}); + hv->sendFloatToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}, {{param.process}}); {% endif %} {% endfor %} } @@ -487,14 +489,14 @@ void LoopWriteIn(Heavy_{{patch_name}}& hv) /** Sends signals from the Daisy hardware to the PD patch via the receive objects during the audio callback * */ -void CallbackWriteIn(Heavy_{{patch_name}}& hv) +void CallbackWriteIn(Heavy_{{patch_name}}* hv) { {% for param in callback_write_in %} {% if param.bool %} if ({{param.process}}) - hv.sendBangToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}); + hv->sendBangToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}); {% else %} - hv.sendFloatToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}, {{param.process}}); + hv->sendFloatToReceiver((uint32_t) HV_{{patch_name|upper}}_PARAM_IN_{{param.hash_enum|upper}}, {{param.process}}); {% endif %} {% endfor %} } diff --git a/hvcc/generators/c2dpf/templates/Makefile_plugin b/hvcc/generators/c2dpf/templates/Makefile_plugin index 97cf6a5d..30e11d7c 100644 --- a/hvcc/generators/c2dpf/templates/Makefile_plugin +++ b/hvcc/generators/c2dpf/templates/Makefile_plugin @@ -31,9 +31,9 @@ BUILD_CXX_FLAGS += -I ../../{{dpf_path}}{{dependency}} {%- endfor %} {%- endif %} -LINK_FLAGS += -lpthread -CFLAGS += -Wno-unused-parameter -std=c11 -CXXFLAGS += -Wno-unused-parameter +BUILD_C_FLAGS += -Wno-unused-parameter -std=c11 -fno-strict-aliasing -pthread +BUILD_CXX_FLAGS += -Wno-unused-parameter -fno-strict-aliasing -pthread +LINK_FLAGS += -pthread {% if meta.plugin_formats is defined %} {%- for format in meta.plugin_formats %} diff --git a/hvcc/generators/c2js/c2js.py b/hvcc/generators/c2js/c2js.py index 6f8a80d6..75fdd943 100644 --- a/hvcc/generators/c2js/c2js.py +++ b/hvcc/generators/c2js/c2js.py @@ -55,6 +55,8 @@ class c2js: "_hv_table_setLength", "_hv_table_getBuffer", "_hv_sendMessageToReceiverV", + "_hv_sendMessageToReceiverFF", + "_hv_sendMessageToReceiverFFF", "_malloc" # Rationale: https://github.com/emscripten-core/emscripten/issues/6882#issuecomment-406745898 ] @@ -117,7 +119,6 @@ def run_emscripten( linker_flags = [ "-O3", - "--memory-init-file", "0", "-s", "RESERVED_FUNCTION_POINTERS=2", "-s", "DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=$addFunction", "-s", f"EXPORTED_FUNCTIONS=[{hv_api_defs.format(patch_name)}]", @@ -172,6 +173,9 @@ def compile( event_list = externs["events"]["in"] event_out_list = externs["events"]["out"] + midi_list = externs["midi"]["in"] + midi_out_list = externs["midi"]["out"] + out_dir = os.path.join(out_dir, "js") patch_name = patch_name or "heavy" @@ -222,6 +226,8 @@ def compile( parameters_out=parameter_out_list, events=event_list, events_out=event_out_list, + midi=midi_list, + midi_out=midi_out_list, copyright=copyright_html)) # generate heavy js worklet from template diff --git a/hvcc/generators/c2js/template/hv_worklet.js b/hvcc/generators/c2js/template/hv_worklet.js index 929af0fa..c043f1a4 100644 --- a/hvcc/generators/c2js/template/hv_worklet.js +++ b/hvcc/generators/c2js/template/hv_worklet.js @@ -27,7 +27,6 @@ class {{name}}_AudioLibWorklet extends AudioWorkletProcessor { Module._malloc(lengthInSamples * Float32Array.BYTES_PER_ELEMENT), lengthInSamples); - this.port.onmessage = (e) => { console.log(e.data); switch(e.data.type){ @@ -37,6 +36,12 @@ class {{name}}_AudioLibWorklet extends AudioWorkletProcessor { case 'sendEvent': this.sendEvent(e.data.name); break; + case 'sendMidi': + this.sendMidi(e.data.message); + break; + case 'fillTableWithFloatBuffer': + this.fillTableWithFloatBuffer(e.data.name, e.data.buffer); + break; default: console.error('No handler for message of type: ', e.data.type); } @@ -126,6 +131,76 @@ class {{name}}_AudioLibWorklet extends AudioWorkletProcessor { } } + sendMidi(message) { + if (this.heavyContext) { + var command = message[0] & 0xF0; + var channel = message[0] & 0x0F; + var data1 = message[1]; + var data2 = message[2]; + + // all events to [midiin] + for (var i = 1; i <= 2; i++) { + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_MIDIIN, 0, + message[i], + channel + ); + } + + // realtime events to [midirealtimein] + if (MIDI_REALTIME.includes(message[0])) { + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_MIDIREALTIMEIN, 0, + message[0] + ); + } + + switch(command) { + case 0x80: // note off + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_NOTEIN, 0, + data1, + 0, + channel); + break; + case 0x90: // note on + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_NOTEIN, 0, + data1, + data2, + channel); + break; + case 0xA0: // polyphonic aftertouch + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_POLYTOUCHIN, 0, + data2, // pressure + data1, // note + channel); + break; + case 0xB0: // control change + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_CTLIN, 0, + data2, // value + data1, // cc number + channel); + break; + case 0xC0: // program change + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_PGMIN, 0, + data1, + channel); + break; + case 0xD0: // aftertouch + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_TOUCHIN, 0, + data1, + channel); + break; + case 0xE0: // pitch bend + // combine 7bit lsb and msb into 32bit int + var value = (data2 << 7) | data1; + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_BENDIN, 0, + value, + channel); + break; + default: + // console.error('No handler for midi message: ', message); + } + } + } + sendStringToReceiver(name, message) { // Note(joe): it's not a good idea to call this frequently it is possible for // the stack memory to run out over time. @@ -144,7 +219,7 @@ class {{name}}_AudioLibWorklet extends AudioWorkletProcessor { _hv_table_setLength(this.heavyContext, tableHash, buffer.length); // access internal float buffer from table - tableBuffer = new Float32Array( + let tableBuffer = new Float32Array( Module.HEAPF32.buffer, _hv_table_getBuffer(this.heavyContext, tableHash), buffer.length); @@ -188,3 +263,19 @@ var tableHashes = { }; registerProcessor("{{name}}_AudioLibWorklet", {{name}}_AudioLibWorklet); + + +/* + * MIDI Constants + */ + +const HV_HASH_NOTEIN = 0x67E37CA3; +const HV_HASH_CTLIN = 0x41BE0f9C; +const HV_HASH_POLYTOUCHIN = 0xBC530F59; +const HV_HASH_PGMIN = 0x2E1EA03D; +const HV_HASH_TOUCHIN = 0x553925BD; +const HV_HASH_BENDIN = 0x3083F0F7; +const HV_HASH_MIDIIN = 0x149631bE; +const HV_HASH_MIDIREALTIMEIN = 0x6FFF0BCF; + +const MIDI_REALTIME = [0xF8, 0xFA, 0xFB, 0xFC, 0xFE, 0xFF]; diff --git a/hvcc/generators/c2js/template/hv_wrapper.js b/hvcc/generators/c2js/template/hv_wrapper.js index 0eeef43f..3b2f85e0 100644 --- a/hvcc/generators/c2js/template/hv_wrapper.js +++ b/hvcc/generators/c2js/template/hv_wrapper.js @@ -87,19 +87,50 @@ AudioLibLoader.prototype.stop = function() { } AudioLibLoader.prototype.sendFloatParameterToWorklet = function(name, value) { - this.webAudioWorklet.port.postMessage({ - type:'setFloatParameter', - name, - value - }); + if (this.audiolib) { + this.audiolib.sendEvent(name, value); + } else { + this.webAudioWorklet.port.postMessage({ + type:'setFloatParameter', + name, + value + }); + } } AudioLibLoader.prototype.sendEvent = function(name, value) { - this.webAudioWorklet.port.postMessage({ - type:'sendEvent', - name, - value - }); + if (this.audiolib) { + this.audiolib.sendEvent(name, value); + } else { + this.webAudioWorklet.port.postMessage({ + type:'sendEvent', + name, + value + }); + } +} + +AudioLibLoader.prototype.sendMidi = function(message) { + if (this.audiolib) { + this.audiolib.sendMidi(message); + } else { + this.webAudioWorklet.port.postMessage({ + type:'sendMidi', + message:message + }); + } +} + +AudioLibLoader.prototype.fillTableWithFloatBuffer = function(name, buffer) { + if (this.audiolib) { + this.audiolib.fillTableWithFloatBuffer(name, buffer); + } else { + this.webAudioWorklet.port.postMessage({ + type:'fillTableWithFloatBuffer', + name, + buffer + }); + } } Module.AudioLibLoader = AudioLibLoader; @@ -228,6 +259,76 @@ var tableHashes = { } } +{{name}}_AudioLib.prototype.sendMidi = function(message) { + if (this.heavyContext) { + var command = message[0] & 0xF0; + var channel = message[0] & 0x0F; + var data1 = message[1]; + var data2 = message[2]; + + // all events to [midiin] + for (var i = 1; i <= 2; i++) { + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_MIDIIN, 0, + message[i], + channel + ); + } + + // realtime events to [midirealtimein] + if (MIDI_REALTIME.includes(message[0])) { + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_MIDIREALTIMEIN, 0, + message[0] + ); + } + + switch(command) { + case 0x80: // note off + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_NOTEIN, 0, + data1, + 0, + channel); + break; + case 0x90: // note on + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_NOTEIN, 0, + data1, + data2, + channel); + break; + case 0xA0: // polyphonic aftertouch + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_POLYTOUCHIN, 0, + data2, // pressure + data1, // note + channel); + break; + case 0xB0: // control change + _hv_sendMessageToReceiverFFF(this.heavyContext, HV_HASH_CTLIN, 0, + data2, // value + data1, // cc number + channel); + break; + case 0xC0: // program change + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_PGMIN, 0, + data1, + channel); + break; + case 0xD0: // aftertouch + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_TOUCHIN, 0, + data1, + channel); + break; + case 0xE0: // pitch bend + // combine 7bit lsb and msb into 32bit int + var value = (data2 << 7) | data1; + _hv_sendMessageToReceiverFF(this.heavyContext, HV_HASH_BENDIN, 0, + value, + channel); + break; + default: + // console.error('No handler for midi message: ', message); + } + } +} + {{name}}_AudioLib.prototype.setFloatParameter = function(name, floatValue) { if (this.heavyContext) { _hv_sendFloatToReceiver(this.heavyContext, parameterInHashes[name], parseFloat(floatValue)); @@ -265,3 +366,19 @@ var tableHashes = { } Module.{{name}}_AudioLib = {{name}}_AudioLib; + + +/* + * MIDI Constants + */ + +const HV_HASH_NOTEIN = 0x67E37CA3; +const HV_HASH_CTLIN = 0x41BE0f9C; +const HV_HASH_POLYTOUCHIN = 0xBC530F59; +const HV_HASH_PGMIN = 0x2E1EA03D; +const HV_HASH_TOUCHIN = 0x553925BD; +const HV_HASH_BENDIN = 0x3083F0F7; +const HV_HASH_MIDIIN = 0x149631bE; +const HV_HASH_MIDIREALTIMEIN = 0x6FFF0BCF; + +const MIDI_REALTIME = [0xF8, 0xFA, 0xFB, 0xFC, 0xFE, 0xFF]; diff --git a/hvcc/generators/c2js/template/index.html b/hvcc/generators/c2js/template/index.html index 9eb0b5b2..da6cbf82 100644 --- a/hvcc/generators/c2js/template/index.html +++ b/hvcc/generators/c2js/template/index.html @@ -73,6 +73,34 @@ function onPrint(message) { console.log(message); } + {%- if midi | length or midi_out | length %} + if (navigator.requestMIDIAccess) { + navigator.requestMIDIAccess() + .then(onMIDISuccess, onMIDIFailure); + } + + function onMIDISuccess(midiAccess) { + console.log("MIDI ready!"); + var inputs = midiAccess.inputs.values(); + + for (var input = inputs.next(); input && !input.done; input = inputs.next()) { + // each time there is a midi message call the onMIDIMessage function + input.value.onmidimessage = onMIDIMessage; + } + } + + function onMIDIFailure(msg) { + console.error(`Failed to get MIDI access - ${msg}`); + } + + function onMIDIMessage(message) { + if(loader.webAudioWorklet) { + loader.sendMidi(message.data); + } else { + loader.audiolib.sendMidi(message.data); + } + } + {%- endif %} function onFloatMessage(sendName, floatValue) { diff --git a/hvcc/generators/c2owl/templates/HeavyOwl.hpp b/hvcc/generators/c2owl/templates/HeavyOwl.hpp index 3079d617..9b62d341 100644 --- a/hvcc/generators/c2owl/templates/HeavyOwl.hpp +++ b/hvcc/generators/c2owl/templates/HeavyOwl.hpp @@ -21,6 +21,7 @@ #define HV_HASH_NOTEIN 0x67E37CA3 #define HV_HASH_CTLIN 0x41BE0f9C +#define HV_HASH_POLYTOUCHIN 0xBC530F59 #define HV_HASH_PGMIN 0x2E1EA03D #define HV_HASH_TOUCHIN 0x553925BD #define HV_HASH_BENDIN 0x3083F0F7 @@ -29,6 +30,7 @@ #define HV_HASH_NOTEOUT 0xD1D4AC2 #define HV_HASH_CTLOUT 0xE5e2A040 +#define HV_HASH_POLYTOUCHOUT 0xD5ACA9D1 #define HV_HASH_PGMOUT 0x8753E39E #define HV_HASH_TOUCHOUT 0x476D4387 #define HV_HASH_BENDOUT 0xE8458013 @@ -50,10 +52,7 @@ extern "C" { else getProgramVector()->buttons &= ~(1<setUserData(this); context->setPrintHook(&owlPrintHook); context->setSendHook(&owlSendHook); @@ -103,6 +96,7 @@ class HeavyPatch : public Patch { uint8_t note = hv_msg_getFloat(m, 0); uint8_t velocity = hv_msg_getFloat(m, 1); uint8_t ch = hv_msg_getFloat(m, 2); + ch %= 16; // drop any pd "ports" // debugMessage("noteout", note, velocity, ch); sendMidi(MidiMessage::note(ch, note, velocity)); } @@ -112,6 +106,7 @@ class HeavyPatch : public Patch { uint8_t value = hv_msg_getFloat(m, 0); uint8_t cc = hv_msg_getFloat(m, 1); uint8_t ch = hv_msg_getFloat(m, 2); + ch %= 16; // debugMessage("ctlout", value, cc, ch); sendMidi(MidiMessage::cc(ch, cc, value)); } @@ -120,27 +115,41 @@ class HeavyPatch : public Patch { { uint16_t value = hv_msg_getFloat(m, 0); uint8_t ch = hv_msg_getFloat(m, 1); + ch %= 16; // debugMessage("bendout", value, ch); sendMidi(MidiMessage::pb(ch, value)); } break; + case HV_HASH_POLYTOUCHOUT: + uint8_t value = hv_msg_getFloat(m, 0); + uint8_t note = hv_msg_getFloat(m, 1); + uint8_t ch = hv_msg_getFloat(m, 2); + ch %= 16; + sendMidi(MidiMessage::kp(ch, note, value)); + break; case HV_HASH_TOUCHOUT: - sendMidi(MidiMessage::cp((uint8_t)hv_msg_getFloat(m, 1), (uint8_t)hv_msg_getFloat(m, 0))); + uint8_t value = hv_msg_getFloat(m, 0); + uint8_t ch = hv_msg_getFloat(m, 1); + ch %= 16; + sendMidi(MidiMessage::cp(ch, value)); break; case HV_HASH_PGMOUT: - sendMidi(MidiMessage::pc((uint8_t)hv_msg_getFloat(m, 1), (uint8_t)hv_msg_getFloat(m, 0))); + uint8_t value = hv_msg_getFloat(m, 0); + uint8_t ch = hv_msg_getFloat(m, 1); + ch %= 16; + sendMidi(MidiMessage::pc(ch, value)); break; {% for param, name, typ, namehash, minvalue, maxvalue, defvalue, button in jdata if typ == 'SEND'%} {% if button == True %} // Button {{name}} case HV_HASH_{{typ}}_CHANNEL_{{param}}: setButton(BUTTON_{{param}}, (hv_msg_getFloat(m, 0)-HV_MIN_CHANNEL_{{param}})/ - (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}}) > 0.5); + (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}}) > 0.5); {% else %} // Parameter {{name}} case HV_HASH_{{typ}}_CHANNEL_{{param}}: setParameterValue(PARAMETER_{{param}}, (hv_msg_getFloat(m, 0)-HV_MIN_CHANNEL_{{param}})/ - (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})); + (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})); {% endif %} break; {% endfor %} @@ -153,43 +162,43 @@ class HeavyPatch : public Patch { // sendMessageToReceiverV parses format and loops over args, see HeavyContext.cpp switch(msg.getStatus()){ case CONTROL_CHANGE: - context->sendMessageToReceiverV - (HV_HASH_CTLIN, 0, "fff", - (float)msg.getControllerValue(), // value - (float)msg.getControllerNumber(), // controller number - (float)msg.getChannel()); + context->sendMessageToReceiverV(HV_HASH_CTLIN, 0, "fff", + (float)msg.getControllerValue(), // value + (float)msg.getControllerNumber(), // controller number + (float)msg.getChannel()); break; case NOTE_ON: - context->sendMessageToReceiverV - (HV_HASH_NOTEIN, 0, "fff", - (float)msg.getNote(), // pitch - (float)msg.getVelocity(), // velocity - (float)msg.getChannel()); + context->sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", + (float)msg.getNote(), // pitch + (float)msg.getVelocity(), // velocity + (float)msg.getChannel()); break; case NOTE_OFF: - context->sendMessageToReceiverV - (HV_HASH_NOTEIN, 0, "fff", - (float)msg.getNote(), // pitch - 0.0f, // velocity - (float)msg.getChannel()); - break; + context->sendMessageToReceiverV(HV_HASH_NOTEIN, 0, "fff", + (float)msg.getNote(), // pitch + 0.0f, // velocity + (float)msg.getChannel()); + break; + case POLY_KEY_PRESSURE: + context->sendMessageToReceiverV(HV_HASH_POLYTOUCHIN, 0, "fff" + (float)msg.getPolyKeyPressure(), + (float)msg.getNote(), + (float)msg.getChannel()); + break case CHANNEL_PRESSURE: - context->sendMessageToReceiverV - (HV_HASH_TOUCHIN, 0, "ff", - (float)msg.getChannelPressure(), - (float)msg.getChannel()); + context->sendMessageToReceiverV(HV_HASH_TOUCHIN, 0, "ff", + (float)msg.getChannelPressure(), + (float)msg.getChannel()); break; case PITCH_BEND_CHANGE: - context->sendMessageToReceiverV - (HV_HASH_BENDIN, 0, "ff", - (float)msg.getPitchBend(), - (float)msg.getChannel()); + context->sendMessageToReceiverV(HV_HASH_BENDIN, 0, "ff", + (float)msg.getPitchBend(), + (float)msg.getChannel()); break; case PROGRAM_CHANGE: - context->sendMessageToReceiverV - (HV_HASH_PGMIN, 0, "ff", - (float)msg.getProgramChange(), - (float)msg.getChannel()); + context->sendMessageToReceiverV(HV_HASH_PGMIN, 0, "ff", + (float)msg.getProgramChange(), + (float)msg.getChannel()); break; default: break; @@ -204,7 +213,7 @@ class HeavyPatch : public Patch { // {{name}} case BUTTON_{{param}}: context->sendFloatToReceiver(HV_HASH_{{typ}}_CHANNEL_{{param}}, isButtonPressed(BUTTON_{{param}})* - (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})+HV_MIN_CHANNEL_{{param}}); + (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})+HV_MIN_CHANNEL_{{param}}); break; {% endfor %} default: @@ -217,7 +226,7 @@ class HeavyPatch : public Patch { {% for param, name, typ, namehash, minvalue, maxvalue, defvalue, button in jdata if typ == 'RECV' and button == False %} // {{name}} context->sendFloatToReceiver(HV_HASH_{{typ}}_CHANNEL_{{param}}, getParameterValue(PARAMETER_{{param}})* - (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})+HV_MIN_CHANNEL_{{param}}); + (HV_MAX_CHANNEL_{{param}}-HV_MIN_CHANNEL_{{param}})+HV_MIN_CHANNEL_{{param}}); {% endfor %} _msgLock = false; @@ -229,12 +238,9 @@ class HeavyPatch : public Patch { HeavyContext* context; }; -static void owlSendHook(HeavyContextInterface* ctxt, - const char *receiverName, - uint32_t sendHash, - const HvMessage *m){ - HeavyPatch* patch = (HeavyPatch*)ctxt->getUserData(); - patch->sendCallback(sendHash, m); +static void owlSendHook(HeavyContextInterface* ctxt, const char *receiverName, uint32_t sendHash, const HvMessage *m){ + HeavyPatch* patch = (HeavyPatch*)ctxt->getUserData(); + patch->sendCallback(sendHash, m); } #endif // __HeavyPatch_hpp__ diff --git a/hvcc/generators/ir2c/ir2c.py b/hvcc/generators/ir2c/ir2c.py index fb23277c..c822c007 100644 --- a/hvcc/generators/ir2c/ir2c.py +++ b/hvcc/generators/ir2c/ir2c.py @@ -331,7 +331,7 @@ def main() -> None: parser.add_argument("-v", "--verbose", action="count") args = parser.parse_args() - externs = { + externs: Dict = { "parameters": { "in": {}, "out": {} diff --git a/hvcc/generators/ir2c/static/HvHeavy.cpp b/hvcc/generators/ir2c/static/HvHeavy.cpp index 19ca4128..0d24a068 100644 --- a/hvcc/generators/ir2c/static/HvHeavy.cpp +++ b/hvcc/generators/ir2c/static/HvHeavy.cpp @@ -206,6 +206,35 @@ HV_EXPORT bool hv_sendMessageToReceiverV( return c->sendMessageToReceiver(receiverHash, delayMs, m); } +HV_EXPORT bool hv_sendMessageToReceiverFF( + HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, double data1, double data2) { + hv_assert(c != nullptr); + hv_assert(delayMs >= 0.0); + + const int numElem = (int) 2; + HvMessage *m = HV_MESSAGE_ON_STACK(numElem); + msg_init(m, numElem, c->getCurrentSample() + (hv_uint32_t) (hv_max_d(0.0, delayMs)*c->getSampleRate()/1000.0)); + msg_setFloat(m, 0, (float) data1); + msg_setFloat(m, 1, (float) data2); + + return c->sendMessageToReceiver(receiverHash, delayMs, m); +} + +HV_EXPORT bool hv_sendMessageToReceiverFFF( + HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, double data1, double data2, double data3) { + hv_assert(c != nullptr); + hv_assert(delayMs >= 0.0); + + const int numElem = (int) 3; + HvMessage *m = HV_MESSAGE_ON_STACK(numElem); + msg_init(m, numElem, c->getCurrentSample() + (hv_uint32_t) (hv_max_d(0.0, delayMs)*c->getSampleRate()/1000.0)); + msg_setFloat(m, 0, (float) data1); + msg_setFloat(m, 1, (float) data2); + msg_setFloat(m, 2, (float) data3); + + return c->sendMessageToReceiver(receiverHash, delayMs, m); +} + HV_EXPORT bool hv_sendMessageToReceiver( HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, HvMessage *m) { hv_assert(c != nullptr); diff --git a/hvcc/generators/ir2c/static/HvHeavy.h b/hvcc/generators/ir2c/static/HvHeavy.h index cb1aecfa..5cc820fc 100644 --- a/hvcc/generators/ir2c/static/HvHeavy.h +++ b/hvcc/generators/ir2c/static/HvHeavy.h @@ -189,6 +189,26 @@ bool hv_sendSymbolToReceiver(HeavyContextInterface *c, hv_uint32_t receiverHash, */ bool hv_sendMessageToReceiverV(HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, const char *format, ...); +/** + * Sends a fixed formatted message of two floats to a receiver that can be scheduled for the future. + * The receiver is addressed with its hash, which can also be determined using hv_stringToHash(). + * This function is thread-safe. + * + * @return True if the message was accepted. False if the message could not fit onto + * the message queue to be processed this block. + */ +bool hv_sendMessageToReceiverFF(HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, double data1, double data2); + +/** + * Sends a fixed formatted message of three floats to a receiver that can be scheduled for the future. + * The receiver is addressed with its hash, which can also be determined using hv_stringToHash(). + * This function is thread-safe. + * + * @return True if the message was accepted. False if the message could not fit onto + * the message queue to be processed this block. + */ +bool hv_sendMessageToReceiverFFF(HeavyContextInterface *c, hv_uint32_t receiverHash, double delayMs, double data1, double data2, double data3); + /** * Sends a message to a receiver that can be scheduled for the future. * The receiver is addressed with its hash, which can also be determined using hv_stringToHash(). diff --git a/hvcc/interpreters/pd2hv/Connection.py b/hvcc/interpreters/pd2hv/Connection.py index 8fae7b08..34e6c684 100644 --- a/hvcc/interpreters/pd2hv/Connection.py +++ b/hvcc/interpreters/pd2hv/Connection.py @@ -14,22 +14,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict, Union, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING if TYPE_CHECKING: - from .HeavyObject import HeavyObject - from .PdBinopObject import PdBinopObject - from .PdLibSignalGraph import PdLibSignalGraph + from .PdObject import PdObject class Connection: def __init__( self, - from_obj: 'HeavyObject', + from_obj: 'PdObject', outlet_index: int, - to_obj: Union['PdBinopObject', 'PdLibSignalGraph', 'HeavyObject'], + to_obj: 'PdObject', inlet_index: int, - conn_type: str + conn_type: Optional[str] # not actually Optional. This is due to the requirement in HeavyObject. ) -> None: assert from_obj is not None assert to_obj is not None @@ -50,7 +48,7 @@ def __init__( } @property - def from_obj(self) -> 'HeavyObject': + def from_obj(self) -> 'PdObject': return self.__from_obj @property @@ -62,7 +60,7 @@ def outlet_index(self) -> int: return self.__hv_json["from"]["outlet"] @property - def to_obj(self) -> Union['PdBinopObject', 'PdLibSignalGraph', 'HeavyObject']: + def to_obj(self) -> 'PdObject': return self.__to_obj @property diff --git a/hvcc/interpreters/pd2hv/PdGraph.py b/hvcc/interpreters/pd2hv/PdGraph.py index ef7eaa9f..e5a1b478 100644 --- a/hvcc/interpreters/pd2hv/PdGraph.py +++ b/hvcc/interpreters/pd2hv/PdGraph.py @@ -37,8 +37,8 @@ def __init__( # file location of this graph self.__pd_path = pd_path - self.__objs: List = [] - self.__connections: List = [] + self.__objs: List[PdObject] = [] + self.__connections: List[Connection] = [] self.__inlet_objects: List = [] self.__outlet_objects: List = [] @@ -72,7 +72,7 @@ def is_subpatch(self) -> bool: else: return self.parent_graph.__pd_path == self.__pd_path if not self.is_root else False - def add_object(self, obj: PdObject) -> None: + def add_object(self, obj: PdObject) -> int: obj.parent_graph = self self.__objs.append(obj) @@ -88,6 +88,8 @@ def add_object(self, obj: PdObject) -> None: for i, o in enumerate(self.__outlet_objects): o.let_index = i + return (len(self.__objs) - 1) + def add_parsed_connection(self, from_index: int, from_outlet: int, to_index: int, to_inlet: int) -> None: """ Add a connection to the graph which has been parsed externally. """ @@ -132,6 +134,12 @@ def add_hv_arg(self, arg_index: int, name: str, value_type: str, default_value: "required": required } + def get_object(self, obj_index: int) -> PdObject: + return self.__objs[obj_index] + + def get_objects(self) -> List[PdObject]: + return self.__objs + def get_inlet_connection_type(self, inlet_index: int) -> str: return self.__inlet_objects[inlet_index].get_inlet_connection_type(inlet_index) diff --git a/hvcc/interpreters/pd2hv/PdMessageObject.py b/hvcc/interpreters/pd2hv/PdMessageObject.py index 3b415ff9..e069434a 100644 --- a/hvcc/interpreters/pd2hv/PdMessageObject.py +++ b/hvcc/interpreters/pd2hv/PdMessageObject.py @@ -37,6 +37,7 @@ def __init__( super().__init__("msg", obj_args, pos_x, pos_y) self.obj_dict: Dict = {} + semi_split: List = [] # parse messages # remove prepended slash from $. Heavy does not use that. @@ -72,11 +73,6 @@ def __init__( "message": l_split[1:] }) - if len(self.obj_dict["remote"]) > 0: - self.add_warning( - "Message boxes don't yet support remote messages. " - "These messages will be ignored.") - def to_hv(self) -> Dict: return { "type": "message", diff --git a/hvcc/interpreters/pd2hv/PdObject.py b/hvcc/interpreters/pd2hv/PdObject.py index 1a08c5a8..0eb1e5a5 100644 --- a/hvcc/interpreters/pd2hv/PdObject.py +++ b/hvcc/interpreters/pd2hv/PdObject.py @@ -112,6 +112,9 @@ def get_notices(self) -> Dict: ] } + def get_inlet_connections(self) -> Dict: + return self._inlet_connections + def get_inlet_connection_type(self, inlet_index: int) -> Optional[str]: """ Returns the inlet connection type of this Pd object. For the sake of convenience, the connection type is reported in diff --git a/hvcc/interpreters/pd2hv/PdParser.py b/hvcc/interpreters/pd2hv/PdParser.py index c786e052..f0818de4 100644 --- a/hvcc/interpreters/pd2hv/PdParser.py +++ b/hvcc/interpreters/pd2hv/PdParser.py @@ -45,9 +45,11 @@ class PdParser: # library search paths + __LIB_DIR = os.path.join(os.path.dirname(__file__), "libs") __HVLIB_DIR = os.path.join(os.path.dirname(__file__), "libs", "heavy") __HVLIB_CONVERTED_DIR = os.path.join(os.path.dirname(__file__), "libs", "heavy_converted") __PDLIB_DIR = os.path.join(os.path.dirname(__file__), "libs", "pd") + __ELSELIB_DIR = os.path.join(os.path.dirname(__file__), "libs", "else") __PDLIB_CONVERTED_DIR = os.path.join(os.path.dirname(__file__), "libs", "pd_converted") # detect a dollar argument in a string @@ -72,6 +74,7 @@ def get_supported_objects(cls) -> List: """ Returns a set of all pd objects names supported by the parser. """ pd_objects = [os.path.splitext(f)[0] for f in os.listdir(cls.__PDLIB_DIR) if f.endswith(".pd")] + pd_objects += [f"else/{os.path.splitext(f)[0]}" for f in os.listdir(cls.__ELSELIB_DIR) if f.endswith(".pd")] pd_objects.extend(cls.__PD_CLASSES.keys()) return pd_objects @@ -218,13 +221,14 @@ def graph_from_canvas( g = pd_graph_class(graph_args, pd_path, pos_x, pos_y) + remotes: Dict = {} + # parse and add all Heavy arguments to the graph for li in file_hv_arg_dict[canvas_line]: line = li.split() assert line[4] == "@hv_arg" is_required = (line[9] == "true") - default_value = HeavyObject.force_arg_type(line[8], line[7]) \ - if not is_required else None + default_value = HeavyObject.force_arg_type(line[8], line[7]) if not is_required else None g.add_hv_arg( arg_index=int(line[5][2:]) - 1, # strip off the leading "\$" and make index zero-based name=line[6], @@ -415,6 +419,26 @@ def graph_from_canvas( pos_x=int(line[2]), pos_y=int(line[3]), is_root=False) + # is this object in lib/else? + elif os.path.isfile(os.path.join(self.__ELSELIB_DIR, f"{obj_type}.pd")): + self.obj_counter[obj_type] += 1 + hvlib_path = os.path.join(self.__ELSELIB_DIR, f"{obj_type}.pd") + x = self.graph_from_file( + file_path=hvlib_path, + obj_args=obj_args, + pos_x=int(line[2]), pos_y=int(line[3]), + is_root=False) + + # is this object in lib? (sub-directory) + elif os.path.isfile(os.path.join(self.__LIB_DIR, f"{obj_type}.pd")): + self.obj_counter[obj_type] += 1 + hvlib_path = os.path.join(self.__LIB_DIR, f"{obj_type}.pd") + x = self.graph_from_file( + file_path=hvlib_path, + obj_args=obj_args, + pos_x=int(line[2]), pos_y=int(line[3]), + is_root=False) + # is this an object that must be programatically parsed? elif obj_type in self.__PD_CLASSES: self.obj_counter[obj_type] += 1 @@ -473,11 +497,19 @@ def graph_from_canvas( elif line[1] == "msg": self.obj_counter["msg"] += 1 - g.add_object(PdMessageObject( + msg = PdMessageObject( obj_type="msg", obj_args=[" ".join(line[4:])], pos_x=int(line[2]), - pos_y=int(line[3]))) + pos_y=int(line[3])) + + index = g.add_object(msg) + + if len(msg.obj_dict) > 0: + remotes[index] = [] + + for remote in msg.obj_dict['remote']: + remotes[index].append(remote) elif line[1] == "connect": g.add_parsed_connection( @@ -531,6 +563,28 @@ def graph_from_canvas( # Sometimes it's all that we have, so perhaps it's a good idea. g.add_error(str(e), NotificationEnum.ERROR_EXCEPTION) + # parse remote messages + for index in remotes.keys(): + first_msg = g.get_object(index) + conns = first_msg.get_inlet_connections() + + for remote in remotes[index]: + self.obj_counter["msg"] += 1 + msg = PdMessageObject('msg', [' '.join(msg for msg in remote['message'])]) + msg_index = g.add_object(msg) + + self.obj_counter["send"] += 1 + send = PdSendObject('send', [remote['receiver']]) + send_index = g.add_object(send) + + # connect new message to upstream objects of first message + for conn in conns['0']: + up_obj = conn.from_obj + up_index = g.get_objects().index(up_obj) + g.add_parsed_connection(up_index, 0, msg_index, 0) + + g.add_parsed_connection(msg_index, 0, send_index, 0) + return g @classmethod diff --git a/hvcc/interpreters/pd2hv/libs/else/knob.pd b/hvcc/interpreters/pd2hv/libs/else/knob.pd new file mode 100644 index 00000000..faffacc0 --- /dev/null +++ b/hvcc/interpreters/pd2hv/libs/else/knob.pd @@ -0,0 +1,6 @@ +#N canvas 0 149 210 157 10; +#X obj 24 35 inlet; +#X obj 24 57 float 0; +#X obj 24 79 outlet; +#X connect 0 0 1 0; +#X connect 1 0 2 0; diff --git a/hvcc/interpreters/pd2hv/libs/pd/poly.pd b/hvcc/interpreters/pd2hv/libs/pd/poly.pd index ce304b26..bf2db920 100644 --- a/hvcc/interpreters/pd2hv/libs/pd/poly.pd +++ b/hvcc/interpreters/pd2hv/libs/pd/poly.pd @@ -1,4 +1,4 @@ -#N canvas 1049 344 762 544 10; +#N canvas 313 155 762 544 10; #X obj 448 121 s \$0-shouldSteal; #X obj 478 208 + 1; #X obj 448 281 s \$0-currentVoiceId; @@ -512,7 +512,7 @@ #X connect 87 0 39 0; #X restore 260 381 pd NoteOn; #X text 447 466 @hv_arg \$1 NumVoices int 1 false; -#X text 447 486 @hv_arg \$2 VoiceStealing boolean 0 false; +#X text 447 486 @hv_arg \$2 VoiceStealing int 0 false; #X connect 1 0 10 1; #X connect 5 0 15 0; #X connect 6 0 31 0; diff --git a/setup.cfg b/setup.cfg index 02dc854d..6b8e2844 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [metadata] name = hvcc -version = 0.11.0 +version = 0.12.0 license = GPLv3 author = Enzien Audio, Wasted Audio description = `hvcc` is a python-based dataflow audio programming language compiler that generates C/C++ code and a variety of specific framework wrappers. url = https://github.com/Wasted-Audio/hvcc -download_url = https://github.com/Wasted-Audio/hvcc/archive/refs/tags/v0.11.0.tar.gz +download_url = https://github.com/Wasted-Audio/hvcc/archive/refs/tags/v0.12.0.tar.gz classifiers = Development Status :: 4 - Beta Intended Audience :: Developers diff --git a/tests/pd/control/test-remote-msg.golden.txt b/tests/pd/control/test-remote-msg.golden.txt new file mode 100644 index 00000000..72ade429 --- /dev/null +++ b/tests/pd/control/test-remote-msg.golden.txt @@ -0,0 +1,4 @@ +[@ 0.000] print: 1 +[@ 0.000] print: 3 2 +[@ 0.000] print: 4 +[@ 0.000] print: 2 1 diff --git a/tests/pd/control/test-remote-msg.pd b/tests/pd/control/test-remote-msg.pd new file mode 100644 index 00000000..3fe71249 --- /dev/null +++ b/tests/pd/control/test-remote-msg.pd @@ -0,0 +1,15 @@ +#N canvas 591 80 450 300 12; +#X obj 281 37 loadbang; +#X obj 56 108 r bla; +#X obj 56 175 print; +#X obj 104 107 r die; +#X obj 156 108 r blub; +#X msg 281 72 3 2; +#X obj 251 38 bng 20 250 50 0 empty empty empty 0 -10 0 12 #fcfcfc #000000 #000000; +#X msg 281 108 _ \; bla 1 \; die \$1 2 \; blub 4 \, \$2 1; +#X connect 0 0 5 0; +#X connect 1 0 2 0; +#X connect 3 0 2 0; +#X connect 4 0 2 0; +#X connect 5 0 7 0; +#X connect 6 0 5 0; diff --git a/tests/test_control.py b/tests/test_control.py index f579b0ad..2a2f0054 100755 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -174,9 +174,11 @@ def test_moses(self): def test_msg(self): self._test_control_patch("test-msg.pd") + def test_remote_msg(self): + self._test_control_patch("test-remote-msg.pd") + def test_msg_remote_args(self): self._test_control_patch("test-msg_remote_args.pd") - self._test_control_patch_expect_warning("test-msg_remote_args.pd", NotificationEnum.WARNING_GENERIC) def test_mtof(self): self._test_control_patch("test-mtof.pd")