diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9d2d9db5..3f6c1649 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,11 @@ [bumpversion] -current_version = 0.12.1 +current_version = 0.13.0 -[bumpversion:file:setup.cfg] +[bumpversion:file:pyproject.toml] +search = {current_version} [bumpversion:file:CHANGELOG.md] search = Next Release + +[bumpversion:file:hvcc/version.py] +search = {current_version} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7cf8353..e4e8bc10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,16 +21,42 @@ jobs: - name: Initialize lfs run: git lfs pull - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip setuptools wheel pip install tox tox-gh-actions - name: Run tox run: tox + binary: + + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + with: + lfs: true + submodules: true + - name: Initialize lfs + run: git lfs pull + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install poetry poetry-pyinstaller-plugin + - name: Build binaries + run: poetry build + + - uses: actions/upload-artifact@v4 + with: + name: heavy-binary-linux-x86_64 + path: dist/pyinstaller/manylinux_2_31_x86_64/Heavy + dispatch: needs: build strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 39887ddb..75c6fcc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ CHANGELOG ===== +0.13.0 +----- + +Features: + +* Migrating to poetry for project management +* Standalone binary (only Linux for now) +* DPF: Allow modgui on desktop +* DPF: Enable host transport events without midi input +* JS: midi out and device select by @Reinissance +* Docs: general updates/corrections and improvements +* Allow loading external generator python module @eu-ch +* Meta: additional global setting to automatically set `HV_SIMD_NONE` +* Add version info to cli and IR result. + +Bugfixes: + +* Daisy template newline + +Typing: + +* HeavyLang and HeavyIR objects +* Compiler results +* Extern info +* Heavy IR Graph + 0.12.1 ----- @@ -11,7 +37,7 @@ Features: * DPF: CV flag in portgroups * DPF: flag to disable scoped denormals -Bugfixes +Bugfixes: * wwise: allow Bang messages in OnSendMessageCallback() * Improve `[cos~]` precision. diff --git a/docs/02.getting_started.md b/docs/02.getting_started.md index 087e2412..36851e50 100644 --- a/docs/02.getting_started.md +++ b/docs/02.getting_started.md @@ -101,7 +101,8 @@ This list will be continuously epanded to document differences in object behavio * 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(`. +* Remote/send messages with `sinesum` argument to fill tables are not supported. * `[metro]` and `[timer]` objects do not accept tempo messages or unit arguments. -* `[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. +* `[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. It also doesn't accept `[set(` messages. * 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.dpf.md b/docs/03.gen.dpf.md index 7ce5d0b4..7b1e5f4e 100644 --- a/docs/03.gen.dpf.md +++ b/docs/03.gen.dpf.md @@ -14,29 +14,44 @@ In order to receive MIDI note on and off events, as well as control change messa DPF supports all note/ctl/pgm/touch/bend I/O events. The implementation is further discussed in the [midi docs](04.midi.md) -We can also use `[midirealtimein]` to receive realtime midi messages like clock/start/continue/stop/reset (no active sense). It is up to the user to create the appropriate handling inside the patch. +![notein](img/docs_notein.png) -Additionally you can use the special `[r __hv_dpf_bpm]` receiver to get the current transport BPM value directly. +## Host Transport Events -![notein](img/docs_notein.png) +We can use `[midirealtimein]` to receive host transport events in the form of realtime midi messages like clock/start/continue/stop/reset (no active sense). It is up to the user to create the appropriate handling inside the patch. + +This object does not require the plugin to have MIDI input enabled. + +Additionally you can use the special `[r __hv_dpf_bpm]` receiver to get the current transport BPM value directly. ## Parameter Types -In DPF a parameter can get an optional type configured. The default type is `float`. Other assignable types are `bool` - for toggling a value - and `trig` - for momentary signals. +In DPF a parameter can get an optional type configured. The default type is `float`. + +Other assignable types are `int` - or whole numbers, `bool` - for toggling a value, and `trig` - for momentary signals. ![dpf](img/docs_param_type.png) Using jinja the `v.attributes.type` can be evaluated for a specific string and different templating applied to the parameter. In DPF the extra types `bool` and `trig` result in the following plugin code: ```c++ - parameter.hints = kParameterIsAutomable | kParameterIsBoolean; + parameter.hints = kParameterIsInteger // or - parameter.hints = kParameterIsAutomable | kParameterIsTrigger; + parameter.hints = kParameterIsBoolean; +// or + parameter.hints = kParameterIsTrigger; ``` +Other special types can give additional information to the host: + +* `dB` - unit `dB` - min_value `-inf` label (assumes `0.0f`) +* `Hz` - unit `Hz` +* `log` - hints `kParameterIsLogarithmic` +* `log_hz` - unit `Hz` - hints `kParameterIsLogarithmic` + ## Metadata -An accomponying metadata.json file can be included to set additional plugin settings. +An accompanying metadata.json file can be included to set additional plugin settings. The `project` flag creates a `README.md` and `Makefile` in the root of the project output, but may conflict with other generators. @@ -55,7 +70,7 @@ Each of these are optional and have either a default value or are entirely optio "midi_input": 1, "midi_output": 0, "plugin_formats": [ - "lv2_dsp", + "lv2_sep", "vst2", "vst3", "clap", @@ -67,7 +82,7 @@ Each of these are optional and have either a default value or are entirely optio Other fields that the DPF metadata supports are: -* `port_groups` - If your plugin has more audio i/o that need to be grouped together. +* `port_groups` - If your plugin has more audio i/o that need to be grouped together or given Control Voltage status * `enumerators` - Configure a set of parameters that cycle over `: ` * `enable_ui` - Boolean that creates a generic GUI. Requires `dpf-widgets` on the same level as `dpf`. * `enable_modgui` - Boolean for use in MOD audio based systems. @@ -77,6 +92,19 @@ Other fields that the DPF metadata supports are: * `vst3_info` - String describing the VST3 plugin type. * `clap_info` - List of strings describing the CLAP plugin type. +You can also fully disable SIMD optimizations using the global `nosimd` flag: + +```json +{ + "nosimd": true, + "DPF": { + ... + } +} +``` + +The full type specification can be found [here](https://github.com/Wasted-Audio/hvcc/blob/develop/hvcc/types/meta.py). + An example plugin that uses some of these extended metadata is [WSTD 3Q](https://github.com/Wasted-Audio/wstd-3q). ## Notes diff --git a/docs/03.gen.external.md b/docs/03.gen.external.md new file mode 100644 index 00000000..7e72b66a --- /dev/null +++ b/docs/03.gen.external.md @@ -0,0 +1,59 @@ +# External Generators + +It's possible to implement custom generators without modifying HVCC. For this, you need to: + +* subclass `hvcc.types.compiler.Generator` abstract class in your module (e.g. a Python file) +* add `-G your_module_name` command-line argument when executing `hvcc` + +It's recommended to have only one Generator subclass per module, otherwise any one of them can be executed. + +Check out `hvcc.generators.c2daisy.c2daisy` or `hvcc.generators.c2dpf.c2dpf` modules for reference implementations. + +## Example + +This example demonstrates how to create a custom generator that prints a message into stdout. + +This is `example_hvcc_generator.py`: + +```python +import time + +from typing import Dict, Optional + +from hvcc.types.compiler import CompilerResp, ExternInfo, Generator +from hvcc.types.meta import Meta + + +class ExampleHvccGenerator(Generator): + @classmethod + def compile( + cls, + c_src_dir: str, + out_dir: str, + externs: ExternInfo, + patch_name: Optional[str] = None, + patch_meta: Meta = Meta(), + num_input_channels: int = 0, + num_output_channels: int = 0, + copyright: Optional[str] = None, + verbose: Optional[bool] = False + ) -> CompilerResp: + begin_time = time.time() + print("--> Invoking ExampleHvccGenerator") + time.sleep(1) + end_time = time.time() + + # Please see code example on how CompilerResp class is used and adapt to your case. + return CompilerResp( + stage='example_hvcc_generator', # module name + compile_time=end_time - begin_time, + in_dir=c_src_dir, + out_dir=out_dir + ) +``` + +With this file in your current directory, execute the following command: + +```bash +hvcc patch.pd -G example_hvcc_generator +``` diff --git a/docs/03.gen.javascript.md b/docs/03.gen.javascript.md index e7f2b3f0..91f41b44 100644 --- a/docs/03.gen.javascript.md +++ b/docs/03.gen.javascript.md @@ -147,7 +147,7 @@ Note: these are calls directly to the `AudioLib` so make sure to include `.audio ## 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. +If you have a table that is externed, using the `@hv_table` 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]` diff --git a/docs/03.generators.md b/docs/03.generators.md index 8b2fd104..1fd0149e 100644 --- a/docs/03.generators.md +++ b/docs/03.generators.md @@ -9,3 +9,5 @@ HVCC supports a number of dedicated generators that can help to wrap the heavy c * [Pdext](03.gen.pdext.md) * [Wwise](03.gen.wwise.md) * [Unity](03.gen.unity.md) + +You can also create your own generator without modifying HVCC, see [External Generators](03.gen.external.md). \ No newline at end of file diff --git a/docs/04.midi.md b/docs/04.midi.md index 0f66212b..6599092e 100644 --- a/docs/04.midi.md +++ b/docs/04.midi.md @@ -1,6 +1,6 @@ -# MIDI I/O +# MIDI I/O -In PureData there are objects to handle interfacing with a machines MIDI device. +In PureData there are several objects to handle interfacing with MIDI. **heavy** doesn't provide cross-platform implementation for MIDI I/O as the requirements tend to change depending on the platform or framework being used. @@ -50,138 +50,11 @@ Here's the `DPF` implementation as an example. ## Handling MIDI Input -The MIDI input is called during the DPF `run()` loop where it receives `MidiEvent` messages: +The MIDI input is called during the DPF `run()` loop where it receives `MidiEvent` messages. -```cpp -#if DISTRHO_PLUGIN_WANT_MIDI_INPUT -// ------------------------------------------------------------------- -// Midi Input handler - -void {{class_name}}::handleMidiInput(uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount) -{ - // Realtime events - // TODO: Continue and Reset - - const TimePosition& timePos(getTimePosition()); - const bool playing = timePos.playing; - if (playing != wasPlaying) - { - if (playing) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_START); - } else { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_STOP); - } - wasPlaying = playing; - } - - if (playing && timePos.bbt.valid) - { - float samplesPerBeat = 60 * getSampleRate() / timePos.bbt.beatsPerMinute; - float samplesPerTick = samplesPerBeat / 24.0; - - int i = 1; - while (samplesProcessed > samplesPerTick) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, i * 1000.0*samplesPerTick/getSampleRate(), - "ff", (float) MIDI_RT_CLOCK); - samplesProcessed -= samplesPerTick; - i++; - } - samplesProcessed += frames; - // printf("> ticks: %f - samples: %f \n", samplesPerTick, samplesProcessed); - } - - // Midi events - for (uint32_t i=0; i < midiEventCount; ++i) - { - int status = midiEvents[i].data[0]; - int command = status & 0xF0; - int channel = status & 0x0F; - int data1 = midiEvents[i].data[1]; - int data2 = midiEvents[i].data[2]; +[Source code (run loop)](https://github.com/Wasted-Audio/hvcc/blob/develop/hvcc/generators/c2dpf/templates/HeavyDPF.cpp#L201-L205) - // raw [midiin] messages - int dataSize = *(&midiEvents[i].data + 1) - midiEvents[i].data; - - for (int i = 0; i < dataSize; ++i) { - _context->sendMessageToReceiverV(HV_HASH_MIDIIN, 1000.0*timePos.frame/getSampleRate(), "ff", - (float) midiEvents[i].data[i], - (float) channel); - } - - if(mrtSet.find(status) != mrtSet.end()) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 1000.0*timePos.frame/getSampleRate(), - "ff", (float) status); - } - - // typical midi messages - switch (command) { - case 0x80: { // note off - _context->sendMessageToReceiverV(HV_HASH_NOTEIN, 1000.0*timePos.frame/getSampleRate(), "fff", - (float) data1, // pitch - (float) 0, // velocity - (float) channel); - break; - } - case 0x90: { // note on - _context->sendMessageToReceiverV(HV_HASH_NOTEIN, 1000.0*timePos.frame/getSampleRate(), "fff", - (float) data1, // pitch - (float) data2, // velocity - (float) channel); - break; - } - case 0xB0: { // control change - _context->sendMessageToReceiverV(HV_HASH_CTLIN, 1000.0*timePos.frame/getSampleRate(), "fff", - (float) data2, // value - (float) data1, // cc number - (float) channel); - break; - } - case 0xC0: { // program change - _context->sendMessageToReceiverV(HV_HASH_PGMIN, 1000.0*timePos.frame/getSampleRate(), "ff", - (float) data1, - (float) channel); - break; - } - case 0xD0: { // aftertouch - _context->sendMessageToReceiverV(HV_HASH_TOUCHIN, 1000.0*timePos.frame/getSampleRate(), "ff", - (float) data1, - (float) channel); - break; - } - case 0xE0: { // pitch bend - // combine 7bit lsb and msb into 32bit int - hv_uint32_t value = (((hv_uint32_t) data2) << 7) | ((hv_uint32_t) data1); - _context->sendMessageToReceiverV(HV_HASH_BENDIN, 1000.0*timePos.frame/getSampleRate(), "ff", - (float) value, - (float) channel); - break; - } - default: break; - } - } -} -#endif - - -// ------------------------------------------------------------------- -// DPF Plugin run() loop - -#if DISTRHO_PLUGIN_WANT_MIDI_INPUT -void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount) -{ - handleMidiInput(frames, midiEvents, midiEventCount); -#else -void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames) -{ -#endif - _context->process((float**)inputs, outputs, frames); -} -``` +[Source code (handleMidiInput)](https://github.com/Wasted-Audio/hvcc/blob/develop/hvcc/generators/c2dpf/templates/midiInput.cpp) ## Handling MIDI Output @@ -213,125 +86,4 @@ Pd does not have specific Note Off events, so velocity 0 is assumed to be Note O Bend assumes input values ranged `0 - 16383` for `[bendin]` (normal bend range), however as mentioned before `[bendout]` uses `-8192 to 8191` to stay compatible with pd-vanilla. -```cpp -#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT -// ------------------------------------------------------------------- -// Midi Send handler - -void {{class_name}}::handleMidiSend(uint32_t sendHash, const HvMessage *m) -{ - MidiEvent midiSendEvent; - midiSendEvent.frame = 0; - midiSendEvent.dataExt = nullptr; - - switch(sendHash){ - case HV_HASH_NOTEOUT: // __hv_noteout - { - 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" - - midiSendEvent.size = 3; - if (velocity > 0){ - midiSendEvent.data[0] = 0x90 | ch; // noteon - } else { - midiSendEvent.data[0] = 0x80 | ch; // noteoff - } - midiSendEvent.data[1] = note; - midiSendEvent.data[2] = velocity; - - writeMidiEvent(midiSendEvent); - break; - } - case HV_HASH_CTLOUT: - { - 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; - - midiSendEvent.size = 3; - midiSendEvent.data[0] = 0xB0 | ch; // send CC - midiSendEvent.data[1] = cc; - midiSendEvent.data[2] = value; - - writeMidiEvent(midiSendEvent); - break; - } - case HV_HASH_PGMOUT: - { - uint8_t pgm = hv_msg_getFloat(m, 0); - uint8_t ch = hv_msg_getFloat(m, 1); - ch %= 16; - - midiSendEvent.size = 2; - midiSendEvent.data[0] = 0xC0 | ch; // send Program Change - midiSendEvent.data[1] = pgm; - - writeMidiEvent(midiSendEvent); - break; - } - case HV_HASH_TOUCHOUT: - { - uint8_t value = hv_msg_getFloat(m, 0); - uint8_t ch = hv_msg_getFloat(m, 1); - ch %= 16; - - midiSendEvent.size = 2; - midiSendEvent.data[0] = 0xD0 | ch; // send Touch - midiSendEvent.data[1] = value; - - writeMidiEvent(midiSendEvent); - break; - } - case HV_HASH_BENDOUT: - { - uint16_t value = hv_msg_getFloat(m, 0); - uint8_t lsb = value & 0x7F; - uint8_t msb = (value >> 7) & 0x7F; - uint8_t ch = hv_msg_getFloat(m, 1); - ch %= 16; - - midiSendEvent.size = 3; - midiSendEvent.data[0] = 0xE0 | ch; // send Bend - midiSendEvent.data[1] = lsb; - midiSendEvent.data[2] = msb; - - writeMidiEvent(midiSendEvent); - break; - } - case HV_HASH_MIDIOUT: // __hv_midiout - { - const uint8_t numElements = m->numElements; - if (numElements <=4 ) - { - for (int i = 0; i < numElements; ++i) - { - midiSendEvent.data[i] = hv_msg_getFloat(m, i); - } - } - else - { - printf("> we do not support sysex yet \n"); - break; - } - - // unsigned char* rawData = new unsigned char; - // for (int i = 0; i < numElements; ++i) { - // rawData[i] = (uint8_t) hv_msg_getFloat(m, i); - // printf("> data: %d \n", rawData[i]); - // } - - midiSendEvent.size = numElements; - // midiSendEvent.dataExt = (const uint8_t *) rawData; - - writeMidiEvent(midiSendEvent); - break; - } - default: - break; - } -} -#endif -``` +[Source code (handleMidiSend)](https://github.com/Wasted-Audio/hvcc/blob/develop/hvcc/generators/c2dpf/templates/midiOutput.cpp) \ No newline at end of file diff --git a/examples/daisy/README.md b/examples/daisy/README.md deleted file mode 100644 index 6f3e7212..00000000 --- a/examples/daisy/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Daisy Generator example help - -To build these test patches: - -Install `arm-none-eabi-gcc` on your respective OS. - -Then convert one of the examples to daisy: -`hvcc seed_test.pd -m seed_meta.json -n seed_test -g daisy -o .` - -Then pull in, and compile, libdaisy: - -```bash -git clone https://github.com/electro-smith/libDaisy.git libdaisy -cd libdaisy -git checkout d47eb0f -make -``` - -When it's done building, from the examples dir: - -``` -cd daisy/source/ -make -``` - -This should result in a `build/` dir with some artifacts: - -```ls --rw-r--r-- 1 dreamer dreamer 3.5K Jul 15 19:35 HeavyDaisy_seed_test.d --rw-r--r-- 1 dreamer dreamer 9.9K Jul 15 19:35 HeavyDaisy_seed_test.o --rw-r--r-- 1 dreamer dreamer 42K Jul 15 19:35 HeavyDaisy_seed_test.lst --rw-r--r-- 1 dreamer dreamer 957K Jul 15 19:35 HeavyDaisy_seed_test.map --rwxr-xr-x 1 dreamer dreamer 1.2M Jul 15 19:35 HeavyDaisy_seed_test.elf --rw-r--r-- 1 dreamer dreamer 171K Jul 15 19:35 HeavyDaisy_seed_test.hex --rwxr-xr-x 1 dreamer dreamer 61K Jul 15 19:35 HeavyDaisy_seed_test.bin -``` diff --git a/examples/daisy/patch_meta.json b/examples/daisy/patch_meta.json deleted file mode 100644 index bf59ce41..00000000 --- a/examples/daisy/patch_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "daisy": { - "board": "patch" - } -} diff --git a/examples/daisy/patch_test.pd b/examples/daisy/patch_test.pd deleted file mode 100644 index 2d4f5f21..00000000 --- a/examples/daisy/patch_test.pd +++ /dev/null @@ -1,82 +0,0 @@ -#N canvas 351 118 960 926 10; -#X obj 336 445 dac~ 1 2 3 4; -#X obj 213 350 *~; -#X obj 363 369 *~; -#X obj 404 371 *~; -#X obj 559 366 *~; -#X obj 336 291 r Ctrl2 @hv_param; -#X obj 471 296 r Ctrl3 @hv_param; -#X obj 617 292 r Ctrl4 @hv_param; -#X obj 412 239 osc~; -#X obj 354 143 loadbang; -#X msg 354 164 440; -#X obj 373 194 f; -#X obj 413 196 +; -#X obj 416 145 r Encoder @hv_param; -#X obj 413 169 * 20; -#X msg 548 143 1; -#X msg 589 144 -1; -#X floatatom 459 218 5 0 0 0 - - -, f 5; -#X text 594 119 turn encoder to set pitch; -#X text 603 268 ctrls set volume per channel; -#X obj 329 111 r EncSwitch @hv_param; -#X text 110 116 press encoder to reset osc pitch; -#X obj 125 291 r Ctrl1 @hv_param; -#X obj 128 321 * 0.8; -#X obj 333 316 * 0.8; -#X obj 472 323 * 0.8; -#X obj 606 328 * 0.8; -#X obj 100 485 r Gate1 @hv_param; -#X obj 142 513 t b b; -#X msg 174 543 1; -#X msg 130 543 0 500; -#X obj 227 381 *~; -#X obj 272 514 t b b; -#X msg 295 540 1; -#X msg 250 540 0 500; -#X obj 282 567 line; -#X obj 227 492 r Gate2 @hv_param; -#X obj 364 395 *~; -#X text 389 486 gate ins trigger env for outs 1-2; -#X obj 154 569 line; -#X connect 1 0 31 0; -#X connect 2 0 37 0; -#X connect 3 0 0 2; -#X connect 4 0 0 3; -#X connect 5 0 24 0; -#X connect 6 0 25 0; -#X connect 7 0 26 0; -#X connect 8 0 2 0; -#X connect 8 0 3 0; -#X connect 8 0 4 0; -#X connect 8 0 1 0; -#X connect 9 0 10 0; -#X connect 10 0 11 0; -#X connect 11 0 12 1; -#X connect 12 0 11 0; -#X connect 12 0 17 0; -#X connect 12 0 8 0; -#X connect 13 0 14 0; -#X connect 14 0 12 0; -#X connect 15 0 14 0; -#X connect 16 0 14 0; -#X connect 20 0 10 0; -#X connect 22 0 23 0; -#X connect 23 0 1 1; -#X connect 24 0 2 1; -#X connect 25 0 3 1; -#X connect 26 0 4 1; -#X connect 27 0 28 0; -#X connect 28 0 30 0; -#X connect 28 1 29 0; -#X connect 29 0 39 0; -#X connect 30 0 39 0; -#X connect 31 0 0 0; -#X connect 32 0 34 0; -#X connect 32 1 33 0; -#X connect 33 0 35 0; -#X connect 34 0 35 0; -#X connect 35 0 37 1; -#X connect 36 0 32 0; -#X connect 37 0 0 1; -#X connect 39 0 31 1; diff --git a/examples/daisy/petal_meta.json b/examples/daisy/petal_meta.json deleted file mode 100644 index 35fffa1c..00000000 --- a/examples/daisy/petal_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "daisy": { - "board": "petal" - } -} diff --git a/examples/daisy/petal_test.pd b/examples/daisy/petal_test.pd deleted file mode 100644 index 67e82fd6..00000000 --- a/examples/daisy/petal_test.pd +++ /dev/null @@ -1,43 +0,0 @@ -#N canvas 754 73 659 567 12; -#X obj 29 30 adc~; -#X obj 29 375 dac~; -#X obj 43 105 delwrite~ \$0-delay 1000; -#X obj 28 252 *~; -#X obj 256 132 r Knob3 @hv_param; -#X obj 254 184 + 1; -#X obj 254 158 * -1; -#X obj 31 307 +~; -#X obj 221 275 *~; -#X obj 96 249 line~; -#X obj 43 142 r Knob4 @hv_param; -#X obj 43 168 * 999; -#X obj 96 275 vd~ \$0-delay; -#X obj 96 374 s~ feedback; -#X obj 96 326 *~; -#X obj 143 324 r Knob5 @hv_param; -#X obj 43 77 +~; -#X obj 70 30 r~ feedback; -#X obj 96 187 t f 5; -#X obj 96 218 pack f f; -#X connect 0 0 3 0; -#X connect 0 0 16 0; -#X connect 3 0 7 0; -#X connect 4 0 6 0; -#X connect 4 0 8 1; -#X connect 5 0 3 1; -#X connect 6 0 5 0; -#X connect 7 0 1 0; -#X connect 7 0 1 1; -#X connect 8 0 7 1; -#X connect 9 0 12 0; -#X connect 10 0 11 0; -#X connect 11 0 18 0; -#X connect 12 0 8 0; -#X connect 12 0 14 0; -#X connect 14 0 13 0; -#X connect 15 0 14 1; -#X connect 16 0 2 0; -#X connect 17 0 16 1; -#X connect 18 0 19 0; -#X connect 18 1 19 1; -#X connect 19 0 9 0; diff --git a/examples/daisy/pod_meta.json b/examples/daisy/pod_meta.json deleted file mode 100644 index 8ec39632..00000000 --- a/examples/daisy/pod_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "daisy": { - "board": "pod" - } -} diff --git a/examples/daisy/pod_test.pd b/examples/daisy/pod_test.pd deleted file mode 100644 index 9e5545c6..00000000 --- a/examples/daisy/pod_test.pd +++ /dev/null @@ -1,108 +0,0 @@ -#N canvas -430 210 1440 679 10; -#X obj 785 449 dac~; -#X obj 863 354 osc~; -#X obj 588 372 osc~; -#X obj 695 357 line; -#X obj 658 389 *~; -#X obj 972 356 line; -#X msg 1016 325 1; -#X obj 922 390 *~; -#X obj 977 271 r Button2 @hv_param; -#X obj 708 274 r Button1 @hv_param; -#X obj 978 299 t b b; -#X msg 968 326 0 500; -#X obj 707 299 t b b; -#X msg 740 326 1; -#X msg 697 326 0 500; -#X obj 592 276 r Knob1 @hv_param; -#X obj 861 273 r Knob2 @hv_param; -#X obj 591 301 * 2000; -#X obj 861 298 * 2000; -#X obj 762 54 r EncSwitch @hv_param; -#X text 253 292 knob1 adjusts left pitch; -#X text 253 313 button1 triggers left envelope; -#X text 1135 273 knob2 adjusts right pitch; -#X text 1135 292 button2 triggers right envelope; -#X obj 509 290 vsl 15 128 0 1 0 0 empty empty empty 0 -9 0 10 -262144 --1 -1 0 1; -#X text 485 30 pressing encoder toggles vibrato; -#X obj 859 134 osc~; -#X obj 859 86 loadbang; -#X obj 590 341 +~; -#X obj 861 331 +~; -#X obj 859 198 *~; -#X obj 708 234 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 --1 -1; -#X obj 754 83 f; -#X obj 784 84 + 1; -#X obj 813 84 % 2; -#X obj 711 33 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 --1; -#X floatatom 755 115 5 0 0 0 - - -, f 5; -#X msg 858 109 10; -#X obj 982 232 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 --1 -1; -#X obj 1088 241 vsl 15 128 0 1 0 0 empty empty empty 0 -9 0 10 -262144 --1 -1 0 1; -#X obj 933 84 r Encoder @hv_param; -#X obj 930 134 f; -#X obj 958 133 +; -#X floatatom 958 162 5 0 0 0 - - -, f 5; -#X msg 1093 82 1; -#X msg 1142 82 -1; -#X obj 857 166 *~; -#X obj 932 109 * 25; -#X text 1196 85 rotating encoder sets vibrato depth; -#X msg 827 108 0; -#X msg 891 109 100; -#X connect 1 0 7 0; -#X connect 2 0 4 0; -#X connect 3 0 4 1; -#X connect 4 0 0 0; -#X connect 5 0 7 1; -#X connect 6 0 5 0; -#X connect 7 0 0 1; -#X connect 8 0 10 0; -#X connect 9 0 12 0; -#X connect 10 0 11 0; -#X connect 10 1 6 0; -#X connect 11 0 5 0; -#X connect 12 0 14 0; -#X connect 12 1 13 0; -#X connect 13 0 3 0; -#X connect 14 0 3 0; -#X connect 15 0 17 0; -#X connect 16 0 18 0; -#X connect 17 0 28 0; -#X connect 18 0 29 0; -#X connect 19 0 32 0; -#X connect 24 0 17 0; -#X connect 26 0 46 0; -#X connect 27 0 37 0; -#X connect 27 0 49 0; -#X connect 27 0 50 0; -#X connect 28 0 2 0; -#X connect 29 0 1 0; -#X connect 30 0 28 1; -#X connect 30 0 29 1; -#X connect 31 0 12 0; -#X connect 32 0 33 0; -#X connect 32 0 30 1; -#X connect 32 0 36 0; -#X connect 33 0 34 0; -#X connect 34 0 32 1; -#X connect 35 0 32 0; -#X connect 37 0 26 0; -#X connect 38 0 10 0; -#X connect 39 0 18 0; -#X connect 40 0 47 0; -#X connect 41 0 42 1; -#X connect 42 0 43 0; -#X connect 42 0 41 0; -#X connect 42 0 46 1; -#X connect 44 0 47 0; -#X connect 45 0 47 0; -#X connect 46 0 30 0; -#X connect 47 0 42 0; -#X connect 49 0 32 0; -#X connect 50 0 41 0; diff --git a/examples/daisy/seed_meta.json b/examples/daisy/seed_meta.json deleted file mode 100644 index 31ff99a7..00000000 --- a/examples/daisy/seed_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "daisy": { - "board": "seed" - } -} diff --git a/examples/daisy/seed_test.pd b/examples/daisy/seed_test.pd deleted file mode 100644 index 964a0839..00000000 --- a/examples/daisy/seed_test.pd +++ /dev/null @@ -1,132 +0,0 @@ -#N canvas 624 80 1066 837 10; -#X obj 226 56 hsl 128 15 0 1 0 0 empty empty Parameter_B -2 -8 0 10 --262144 -1 -1 7700 1; -#X obj 434 56 hsl 128 15 0 1 0 0 empty empty Parameter_C -2 -8 0 10 --262144 -1 -1 4900 1; -#X obj 644 51 hsl 128 15 0 1 0 0 empty empty Parameter_D -2 -8 0 10 --262144 -1 -1 11000 1; -#X obj 21 55 hsl 128 15 0 1 0 0 empty empty Parameter_A -2 -8 0 10 --262144 -1 -1 3800 1; -#X text 72 4 OWL Puredata Template; -#X obj 831 47 hsl 128 15 0 1 0 0 empty empty Parameter_E -2 -8 0 10 --262144 -1 -1 1400 1; -#X obj 826 114 + 0; -#X obj 512 771 dac~; -#X obj 326 568 phasor~; -#X obj 328 614 *~ 2; -#X obj 330 647 -~ 1; -#X obj 330 282 hsl 128 15 0 1000 0 0 empty empty empty -2 -8 0 10 -262144 --1 -1 1400 1; -#X obj 507 546 phasor~; -#X obj 509 592 *~ 2; -#X obj 511 625 -~ 1; -#X obj 644 599 phasor~; -#X obj 646 645 *~ 2; -#X obj 648 678 -~ 1; -#X obj 753 621 phasor~; -#X obj 755 667 *~ 2; -#X obj 757 700 -~ 1; -#X floatatom 428 526 5 0 0 0 - - -; -#X floatatom 579 569 5 0 0 0 - - -; -#X floatatom 699 572 5 0 0 0 - - -; -#X obj 510 510 +, f 4; -#X obj 617 532 +, f 4; -#X obj 733 528 +, f 4; -#X obj 732 296 vsl 15 128 -100 100 0 0 empty empty empty 0 -9 0 10 --262144 -1 -1 7700 1; -#X obj 563 445 t b f; -#X obj 647 463 t b f; -#X obj 728 481 t b f; -#X floatatom 278 440 5 0 0 0 - - -; -#X obj 757 295 vsl 15 128 -100 100 0 0 empty empty empty 0 -9 0 10 --262144 -1 -1 4900 1; -#X obj 783 294 vsl 15 128 -100 100 0 0 empty empty empty 0 -9 0 10 --262144 -1 -1 11000 1; -#X floatatom 381 400 5 0 0 0 - - -; -#X obj 460 379 +, f 4; -#X obj 516 319 t b f; -#X obj 597 266 hsl 64 15 -10 10 0 0 empty empty empty -2 -8 0 10 -262144 --1 -1 1885 1; -#X obj 532 274 / 1; -#X obj 662 226 loadbang; -#X msg 701 246 0; -#X obj 828 94 * 1000; -#X obj 662 142 - 100; -#X obj 655 112 * 200; -#X obj 430 123 - 100; -#X obj 221 124 - 100; -#X obj 223 103 * 200; -#X obj 430 103 * 200; -#X obj 18 102 * 20; -#X obj 17 123 - 10; -#X obj 508 735 *~ 0.75; -#X obj 28 76 r ChannelA @hv_param 0 1 0.5; -#X obj 441 77 r ChannelC @hv_param 0 1 0.5; -#X obj 650 72 r ChannelD @hv_param 0 1 0.5; -#X obj 837 68 r ChannelE @hv_param 0 1 0.5; -#X obj 236 77 r ChannelB @hv_param 0 1 0.5; -#X connect 0 0 46 0; -#X connect 1 0 47 0; -#X connect 2 0 43 0; -#X connect 3 0 48 0; -#X connect 5 0 41 0; -#X connect 6 0 11 0; -#X connect 8 0 9 0; -#X connect 9 0 10 0; -#X connect 10 0 50 0; -#X connect 11 0 8 0; -#X connect 11 0 31 0; -#X connect 11 0 35 0; -#X connect 12 0 13 0; -#X connect 13 0 14 0; -#X connect 14 0 50 0; -#X connect 15 0 16 0; -#X connect 16 0 17 0; -#X connect 17 0 50 0; -#X connect 18 0 19 0; -#X connect 19 0 20 0; -#X connect 20 0 50 0; -#X connect 24 0 12 0; -#X connect 24 0 21 0; -#X connect 25 0 15 0; -#X connect 25 0 22 0; -#X connect 26 0 18 0; -#X connect 26 0 23 0; -#X connect 27 0 28 0; -#X connect 28 0 24 0; -#X connect 28 1 24 1; -#X connect 29 0 25 0; -#X connect 29 1 25 1; -#X connect 30 0 26 0; -#X connect 30 1 26 1; -#X connect 32 0 29 0; -#X connect 33 0 30 0; -#X connect 35 0 34 0; -#X connect 35 0 24 0; -#X connect 35 0 25 0; -#X connect 35 0 26 0; -#X connect 36 0 35 0; -#X connect 36 1 35 1; -#X connect 37 0 38 0; -#X connect 38 0 36 0; -#X connect 39 0 40 0; -#X connect 40 0 27 0; -#X connect 40 0 32 0; -#X connect 40 0 33 0; -#X connect 40 0 37 0; -#X connect 41 0 6 0; -#X connect 42 0 33 0; -#X connect 43 0 42 0; -#X connect 44 0 32 0; -#X connect 45 0 27 0; -#X connect 46 0 45 0; -#X connect 47 0 44 0; -#X connect 48 0 49 0; -#X connect 49 0 37 0; -#X connect 50 0 7 0; -#X connect 50 0 7 1; -#X connect 51 0 48 0; -#X connect 52 0 47 0; -#X connect 53 0 43 0; -#X connect 54 0 41 0; -#X connect 55 0 46 0; diff --git a/examples/dpf/README.md b/examples/dpf/README.md deleted file mode 100644 index 726cb37e..00000000 --- a/examples/dpf/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Distrho Plugin Format Generator example help - -To build the sample run hvcc with the DPF generator and metadata option on an output directory: - -```bash -$ mkdir dpf_example -$ hvcc dpf_example.pd -o dpf_example/ -g dpf -n dpf_example -m dpf_example.json -$ cd dpf_example/ -$ git clone https://github.com/DISTRHO/DPF.git dpf -$ make -``` - -The binaries will be in `bin/` \ No newline at end of file diff --git a/examples/dpf/dpf_bend.pd b/examples/dpf/dpf_bend.pd deleted file mode 100644 index c8e3f324..00000000 --- a/examples/dpf/dpf_bend.pd +++ /dev/null @@ -1,16 +0,0 @@ -#N canvas 769 445 438 195 12; -#X obj 30 25 bendin; -#X obj 192 42 hsl 128 15 -8192 8192 0 0 empty empty empty -2 -8 0 10 --262144 -1 -1 8900 1; -#X obj 121 9 r bend @hv_param 0 16383 8192; -#X obj 30 58 print; -#X obj 121 75 bendout; -#X obj 121 40 - 8192; -#X text 17 106 For compatibility reasons we reproduce the inconsistent -[bendin] and [bendout] behaviour. While [bendin] outputs values from -0 to 16383 \, [bendout] takes values from -8192 to 8191 - this likely -won't change., f 55; -#X connect 0 0 3 0; -#X connect 1 0 4 0; -#X connect 2 0 5 0; -#X connect 5 0 4 0; diff --git a/examples/dpf/dpf_example.json b/examples/dpf/dpf_example.json deleted file mode 100644 index bd081d27..00000000 --- a/examples/dpf/dpf_example.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "dpf": { - "project": true, - "description": "super simple test patch", - "maker": "nobody", - "homepage": "https://wasted.audio/plugin/dpf_example", - "plugin_uri": "lv2://wasted.audio/lv2/dpf_example", - "version": "6, 6, 6", - "license": "WTFPL", - "midi_input": 1, - "midi_output": 0, - "plugin_formats": [ - "lv2_sep", - "vst2", - "jack" - ] - } -} diff --git a/examples/dpf/dpf_example.pd b/examples/dpf/dpf_example.pd deleted file mode 100644 index 18b3bc2c..00000000 --- a/examples/dpf/dpf_example.pd +++ /dev/null @@ -1,21 +0,0 @@ -#N canvas 268 61 512 452 12; -#X obj 89 84 r freq @hv_param 100 2000 500; -#X obj 89 167 osc~; -#X obj 89 275 *~; -#X obj 89 308 dac~; -#X obj 150 111 r gain @hv_param 0 1 0.5; -#X msg 150 140 \$1 100; -#X obj 150 169 line~; -#X obj 89 194 *~; -#X obj 150 215 r mute_toggle @hv_param 0 1 1 bool; -#X obj 150 244 r mute_momentary @hv_param 0 1 0 trig; -#X connect 0 0 1 0; -#X connect 1 0 7 0; -#X connect 2 0 3 0; -#X connect 2 0 3 1; -#X connect 4 0 5 0; -#X connect 5 0 6 0; -#X connect 6 0 7 1; -#X connect 7 0 2 0; -#X connect 8 0 2 1; -#X connect 9 0 2 1; diff --git a/examples/dpf/dpf_midi_thru.json b/examples/dpf/dpf_midi_thru.json deleted file mode 100644 index be1df31a..00000000 --- a/examples/dpf/dpf_midi_thru.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "dpf": { - "project": true, - "description": "super simple midi thru patch", - "maker": "dreamer", - "version": "6, 6, 6", - "license": "WTFPL", - "midi_input": 1, - "midi_output": 1, - "plugin_formats": [ - "lv2_dsp", - "vst2", - "vst3", - "jack" - ] - } -} diff --git a/examples/dpf/dpf_midi_thru.pd b/examples/dpf/dpf_midi_thru.pd deleted file mode 100644 index 0cb4105b..00000000 --- a/examples/dpf/dpf_midi_thru.pd +++ /dev/null @@ -1,29 +0,0 @@ -#N canvas 559 367 576 125 12; -#X obj 22 19 notein; -#X obj 22 59 noteout; -#X obj 98 19 ctlin; -#X obj 98 59 ctlout; -#X obj 172 19 pgmin; -#X obj 172 59 pgmout; -#X obj 236 19 touchin; -#X obj 236 59 touchout; -#X obj 318 19 bendin; -#X obj 318 69 bendout; -#X obj 318 44 - 8192; -#X obj 417 22 midirealtimein; -#X obj 417 59 midiout; -#X connect 0 0 1 0; -#X connect 0 1 1 1; -#X connect 0 2 1 2; -#X connect 2 0 3 0; -#X connect 2 1 3 1; -#X connect 2 2 3 2; -#X connect 4 0 5 0; -#X connect 4 1 5 1; -#X connect 6 0 7 0; -#X connect 6 1 7 1; -#X connect 8 0 10 0; -#X connect 8 1 9 1; -#X connect 10 0 9 0; -#X connect 11 0 12 0; -#X connect 11 1 12 1; diff --git a/examples/dpf/dpf_pgm.pd b/examples/dpf/dpf_pgm.pd deleted file mode 100644 index 222d4691..00000000 --- a/examples/dpf/dpf_pgm.pd +++ /dev/null @@ -1,10 +0,0 @@ -#N canvas 1070 197 355 151 12; -#X obj 144 52 hsl 128 15 0 127 0 0 empty empty empty -2 -8 0 10 -262144 --1 -1 0 1; -#X obj 19 56 print; -#X obj 110 84 pgmout; -#X obj 19 23 pgmin; -#X obj 110 22 r program @hv_param 0 127 0; -#X connect 0 0 2 0; -#X connect 3 0 1 0; -#X connect 4 0 2 0; diff --git a/examples/dpf/dpf_touch.pd b/examples/dpf/dpf_touch.pd deleted file mode 100644 index 50addb0a..00000000 --- a/examples/dpf/dpf_touch.pd +++ /dev/null @@ -1,10 +0,0 @@ -#N canvas 1067 404 356 143 12; -#X obj 150 53 hsl 128 15 0 127 0 0 empty empty empty -2 -8 0 10 -262144 --1 -1 0 1; -#X obj 25 57 print; -#X obj 25 24 touchin; -#X obj 116 100 touchout; -#X obj 116 23 r touch @hv_param 0 127 0; -#X connect 0 0 3 0; -#X connect 2 0 1 0; -#X connect 4 0 3 0; diff --git a/examples/example.pd b/examples/example.pd deleted file mode 100644 index bdb4f72f..00000000 --- a/examples/example.pd +++ /dev/null @@ -1,15 +0,0 @@ -#N canvas 441 234 462 370 12; -#X obj 85 85 r freq @hv_param 100 2000 500; -#X obj 85 177 osc~; -#X obj 85 211 *~; -#X obj 86 250 dac~; -#X obj 147 122 r gain @hv_param 0 1 0.5; -#X msg 147 151 \$1 100; -#X obj 147 176 line~; -#X connect 0 0 1 0; -#X connect 1 0 2 0; -#X connect 2 0 3 0; -#X connect 2 0 3 1; -#X connect 4 0 5 0; -#X connect 5 0 6 0; -#X connect 6 0 2 1; diff --git a/hvcc/__init__.py b/hvcc/__init__.py index d7434baf..345ea4d7 100644 --- a/hvcc/__init__.py +++ b/hvcc/__init__.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2021-2023 Wasted Audio +# Copyright (C) 2021-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,12 +16,14 @@ import argparse from collections import OrderedDict +import importlib +import inspect import json import os import re -import time import sys -from typing import List, Dict, Optional +import time +from typing import Any, List, Dict, Optional from hvcc.interpreters.pd2hv import pd2hv from hvcc.core.hv2ir import hv2ir @@ -34,7 +36,13 @@ from hvcc.generators.c2pdext import c2pdext from hvcc.generators.c2wwise import c2wwise from hvcc.generators.c2unity import c2unity -from hvcc.generators.types.meta import Meta +from hvcc.types.compiler import ( + CompilerResp, CompilerNotif, CompilerMsg, Generator, + ExternInfo, ExternMemoryPool, ExternMidi, ExternEvents, ExternParams +) +from hvcc.types.IR import IRGraph +from hvcc.types.meta import Meta +from hvcc.version import VERSION class Colours: @@ -50,19 +58,20 @@ class Colours: end = "\033[0m" -def add_error(results: OrderedDict, error: str) -> OrderedDict: +def add_error( + results: Dict[str, CompilerResp], + error: str +) -> Dict[str, CompilerResp]: if "hvcc" in results: - results["hvcc"]["notifs"]["errors"].append({"message": error}) + results["hvcc"].notifs.errors.append(CompilerMsg(message=error)) else: - results["hvcc"] = { - "stage": "hvcc", - "notifs": { - "has_error": True, - "exception": None, - "errors": [{"message": error}], - "warnings": [] - } - } + results["hvcc"] = CompilerResp( + stage="hvcc", + notifs=CompilerNotif( + has_error=True, + errors=[CompilerMsg(message=error)] + ) + ) return results @@ -81,7 +90,7 @@ 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: +def check_midi_objects(hvir: IRGraph) -> Dict[str, List[str]]: in_midi = [] out_midi = [] @@ -107,14 +116,13 @@ def check_midi_objects(hvir: Dict) -> Dict: '__hv_touchout', ] - for key in hvir['control']['receivers'].keys(): - if key in midi_in_objs: - in_midi.append(key) + for recv in hvir.control.receivers.keys(): + if recv in midi_in_objs: + in_midi.append(recv) - for key in hvir['control']['sendMessage']: - if key.get('name'): - if key['name'] in midi_out_objs: - out_midi.append(key['name']) + for msg in hvir.control.sendMessage: + if msg.name in midi_out_objs: + out_midi.append(msg.name) return { 'in': in_midi, @@ -132,22 +140,22 @@ def filter_midi_from_out_parameters(output_parameter_list: List, midi_out_object return new_out_list -def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: +def generate_extern_info(hvir: IRGraph, results: OrderedDict) -> ExternInfo: """ Simplifies the receiver/send and table lists by only containing values externed with @hv_param, @hv_event or @hv_table """ # Exposed input parameters - in_parameter_list = [(k, v) for k, v in hvir["control"]["receivers"].items() if v.get("extern", None) == "param"] + in_parameter_list = [(k, v) for k, v in hvir.control.receivers.items() if v.extern == "param"] in_parameter_list.sort(key=lambda x: x[0]) check_extern_name_conflicts("input parameter", in_parameter_list, results) # Exposed input events - in_event_list = [(k, v) for k, v in hvir["control"]["receivers"].items() if v.get("extern", None) == "event"] + in_event_list = [(k, v) for k, v in hvir.control.receivers.items() if v.extern == "event"] in_event_list.sort(key=lambda x: x[0]) check_extern_name_conflicts("input event", in_event_list, results) # Exposed output parameters - out_parameter_list = [(v["name"], v) for v in hvir["control"]["sendMessage"] if v.get("extern", None) == "param"] + out_parameter_list = [(v.name, v) for v in hvir.control.sendMessage if v.extern == "param"] # remove duplicate output parameters/events # NOTE(joe): is the id argument important here? We'll only take the first one in this case. out_parameter_list = list(dict(out_parameter_list).items()) @@ -155,13 +163,13 @@ def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: check_extern_name_conflicts("output parameter", out_parameter_list, results) # Exposed output events - out_event_list = [(v["name"], v) for v in hvir["control"]["sendMessage"] if v.get("extern", None) == "event"] + out_event_list = [(v.name, v) for v in hvir.control.sendMessage if v.extern == "event"] out_event_list = list(dict(out_event_list).items()) out_event_list.sort(key=lambda x: x[0]) check_extern_name_conflicts("output event", out_event_list, results) # Exposed tables - table_list = [(k, v) for k, v in hvir["tables"].items() if v.get("extern", None)] + table_list = [(k, v) for k, v in hvir.tables.items() if v.extern] table_list.sort(key=lambda x: x[0]) check_extern_name_conflicts("table", table_list, results) @@ -171,35 +179,48 @@ def generate_extern_info(hvir: Dict, results: OrderedDict) -> Dict: # 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, - "out": out_parameter_list - }, - "events": { - "in": in_event_list, - "out": out_event_list - }, - "midi": { - "in": midi_objects['in'], - "out": midi_objects['out'] - }, - "tables": table_list, + return ExternInfo( + parameters=ExternParams( + inParam=in_parameter_list, + outParam=out_parameter_list + ), + events=ExternEvents( + inEvent=in_event_list, + outEvent=out_event_list + ), + midi=ExternMidi( + inMidi=midi_objects['in'], + outMidi=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) + - 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']) - )), - } - } + memoryPoolSizesKb=ExternMemoryPool( + 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) + + 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']) + )) + ) + ) + + +def load_ext_generator(module_name, verbose) -> Optional[Generator]: + try: + module = importlib.import_module(module_name) + for _, member in inspect.getmembers(module): + if inspect.isclass(member) and not inspect.isabstract(member) and issubclass(member, Generator): + return member() + if verbose: + print(f"---> Module {module_name} does not contain a class derived from hvcc.types.Compiler") + return None + except ModuleNotFoundError: + return None def compile_dataflow( @@ -209,12 +230,12 @@ def compile_dataflow( patch_meta_file: Optional[str] = None, search_paths: Optional[List] = None, generators: Optional[List] = None, + ext_generators: Optional[List] = None, verbose: bool = False, copyright: Optional[str] = None, nodsp: Optional[bool] = False -) -> OrderedDict: - - results: OrderedDict = OrderedDict() # default value, empty dictionary +) -> Dict[str, CompilerResp]: + results: OrderedDict[str, CompilerResp] = OrderedDict() # default value, empty dictionary patch_meta = Meta() # basic error checking on input @@ -250,47 +271,56 @@ def compile_dataflow( verbose=verbose) # check for errors - if list(results.values())[0]["notifs"].get("has_error", False): + + response: CompilerResp = list(results.values())[0] + + if response.notifs.has_error: return results subst_name = re.sub(r'\W', '_', patch_name) results["hv2ir"] = hv2ir.hv2ir.compile( - hv_file=os.path.join(list(results.values())[0]["out_dir"], list(results.values())[0]["out_file"]), + hv_file=os.path.join(response.out_dir, response.out_file), # ensure that the ir filename has no funky characters in it ir_file=os.path.join(out_dir, "ir", f"{subst_name}.heavy.ir.json"), patch_name=patch_name, verbose=verbose) # check for errors - if results["hv2ir"]["notifs"].get("has_error", False): + if results["hv2ir"].notifs.has_error: return results # get the hvir data - hvir = results["hv2ir"]["ir"] - patch_name = hvir["name"]["escaped"] + hvir = results["hv2ir"].ir + assert hvir is not None + patch_name = hvir.name.escaped externs = generate_extern_info(hvir, results) + # get application path + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + application_path = os.path.join(sys._MEIPASS, 'hvcc') + elif __file__: + application_path = os.path.dirname(__file__) + c_src_dir = os.path.join(out_dir, "c") results["ir2c"] = ir2c.ir2c.compile( - hv_ir_path=os.path.join(results["hv2ir"]["out_dir"], results["hv2ir"]["out_file"]), - static_dir=os.path.join(os.path.dirname(__file__), "generators/ir2c/static"), + hv_ir_path=os.path.join(results["hv2ir"].out_dir, results["hv2ir"].out_file), + static_dir=os.path.join(application_path, "generators/ir2c/static"), output_dir=c_src_dir, externs=externs, copyright=copyright, nodsp=nodsp) # check for errors - if results["ir2c"]["notifs"].get("has_error", False): + if results["ir2c"].notifs.has_error: return results # ir2c_perf - results["ir2c_perf"] = { - "stage": "ir2c_perf", - "obj_counter": ir2c_perf.ir2c_perf.perf(results["hv2ir"]["ir"], verbose=verbose), - "in_dir": results["hv2ir"]["out_dir"], - "in_file": results["hv2ir"]["out_file"], - "notifs": {} - } + results["ir2c_perf"] = CompilerResp( + stage="ir2c_perf", + obj_perf=ir2c_perf.ir2c_perf.perf(hvir, verbose=verbose), + in_dir=results["hv2ir"].out_dir, + in_file=results["hv2ir"].out_file, + ) # reconfigure such that next stage is triggered in_path = c_src_dir @@ -306,19 +336,20 @@ def compile_dataflow( hvir_path = os.path.join(hvir_dir, os.listdir(hvir_dir)[0]) if os.path.isfile(hvir_path): with open(hvir_path, "r") as f: - hvir = json.load(f) - patch_name = hvir["name"]["escaped"] + hvir = IRGraph(**json.load(f)) + patch_name = hvir.name.escaped externs = generate_extern_info(hvir, results) else: return add_error(results, "Cannot find hvir file.") except Exception as e: return add_error(results, f"ir could not be found or loaded: {e}.") + assert hvir is not None # run the c2x generators, merge the results - num_input_channels = hvir["signal"]["numInputBuffers"] - num_output_channels = hvir["signal"]["numOutputBuffers"] + num_input_channels = hvir.signal.numInputBuffers + num_output_channels = hvir.signal.numOutputBuffers - gen_args = { + gen_args: Dict[str, Any] = { 'c_src_dir': c_src_dir, 'out_dir': out_dir, 'patch_name': patch_name, @@ -365,6 +396,14 @@ def compile_dataflow( print("--> Generating Wwise plugin") results["c2wwise"] = c2wwise.c2wwise.compile(**gen_args) + if ext_generators: + for module_name in ext_generators: + generator = load_ext_generator(module_name, verbose) + if generator is not None: + if verbose: + print(f"--> Executing custom generator from module {module_name}") + results[module_name] = generator.compile(**gen_args) + return results @@ -401,6 +440,11 @@ def main() -> bool: nargs="+", default=["c"], help="List of generator outputs: c, unity, wwise, js, pdext, daisy, dpf, fabric, owl") + parser.add_argument( + "-G", + "--ext-gen", + nargs="*", + help="List of external generator modules, see 'External Generators' docs page.") parser.add_argument( "--results_path", help="Write results dictionary to the given path as a JSON-formatted string." @@ -418,6 +462,13 @@ def main() -> bool: parser.add_argument( "--copyright", help="A string indicating the owner of the copyright.") + parser.add_argument( + "-V", + "--version", + action='version', + help="Print version and exit.", + version=VERSION + ) args = parser.parse_args() in_path = os.path.abspath(args.in_path) @@ -428,6 +479,7 @@ def main() -> bool: patch_meta_file=args.meta, search_paths=args.search_paths, generators=args.gen, + ext_generators=args.ext_gen, verbose=args.verbose, copyright=args.copyright, nodsp=args.nodsp @@ -436,25 +488,25 @@ def main() -> bool: errorCount = 0 for r in list(results.values()): # print any errors - if r["notifs"].get("has_error", False): - for i, error in enumerate(r["notifs"].get("errors", [])): + if r.notifs.has_error: + for i, error in enumerate(r.notifs.errors): errorCount += 1 print("{4:3d}) {2}Error{3} {0}: {1}".format( - r["stage"], error["message"], Colours.red, Colours.end, i + 1)) + r.stage, error.message, Colours.red, Colours.end, i + 1)) # only print exception if no errors are indicated - if len(r["notifs"].get("errors", [])) == 0 and r["notifs"].get("exception", None) is not None: + if len(r.notifs.errors) == 0 and r.notifs.exception is not None: errorCount += 1 print("{2}Error{3} {0} exception: {1}".format( - r["stage"], r["notifs"]["exception"], Colours.red, Colours.end)) + r.stage, r.notifs.exception, Colours.red, Colours.end)) # clear any exceptions such that results can be JSONified if necessary - r["notifs"]["exception"] = [] + r.notifs.exception = None # print any warnings - for i, warning in enumerate(r["notifs"].get("warnings", [])): + for i, warning in enumerate(r.notifs.warnings): print("{4:3d}) {2}Warning{3} {0}: {1}".format( - r["stage"], warning["message"], Colours.yellow, Colours.end, i + 1)) + r.stage, warning.message, Colours.yellow, Colours.end, i + 1)) if args.results_path: results_path = os.path.realpath(os.path.abspath(args.results_path)) diff --git a/hvcc/core/hv2ir/HIrConvolution.py b/hvcc/core/hv2ir/HIrConvolution.py index 77766993..a4167577 100644 --- a/hvcc/core/hv2ir/HIrConvolution.py +++ b/hvcc/core/hv2ir/HIrConvolution.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict, Optional +from typing import Dict, Optional, List, Set, Tuple from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph @@ -34,7 +34,7 @@ def __init__( assert obj_type == "__conv~f" super().__init__(obj_type, args=args, graph=graph, annotations=annotations) - def reduce(self) -> Optional[tuple]: + def reduce(self) -> Optional[Tuple[Set, List]]: if self.graph is not None: table_obj = self.graph.resolve_object_for_name( self.args["table"], diff --git a/hvcc/core/hv2ir/HIrInlet.py b/hvcc/core/hv2ir/HIrInlet.py index ea272437..3980f8e0 100644 --- a/hvcc/core/hv2ir/HIrInlet.py +++ b/hvcc/core/hv2ir/HIrInlet.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,8 @@ from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph +from hvcc.types.Lang import LangLetType + class HIrInlet(HeavyIrObject): """ A specific implementation of the inlet object. @@ -34,7 +36,7 @@ def __init__( ) -> None: super().__init__("__inlet", args=args, graph=graph, annotations=annotations) - def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[str]: + def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[LangLetType]: if self.graph is not None: connections = self.graph.inlet_connections[self.args["index"]] connection_type_set = {c.type for c in connections} diff --git a/hvcc/core/hv2ir/HIrOutlet.py b/hvcc/core/hv2ir/HIrOutlet.py index 06c146a6..a67d2f10 100644 --- a/hvcc/core/hv2ir/HIrOutlet.py +++ b/hvcc/core/hv2ir/HIrOutlet.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,6 +19,8 @@ from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph +from hvcc.types.IR import IROnMessage + class HIrOutlet(HeavyIrObject): """ A specific implementation of the outlet object. @@ -33,7 +35,7 @@ def __init__( ) -> None: super().__init__("__outlet", args=args, graph=graph, annotations=annotations) - def get_ir_on_message(self, inlet_index: int = 0) -> List: + def get_ir_on_message(self, inlet_index: int = 0) -> List[IROnMessage]: x = [] if self.graph is not None: for c in self.graph.outlet_connections[self.args["index"]]: diff --git a/hvcc/core/hv2ir/HIrReceive.py b/hvcc/core/hv2ir/HIrReceive.py index 2b28671b..9019e958 100644 --- a/hvcc/core/hv2ir/HIrReceive.py +++ b/hvcc/core/hv2ir/HIrReceive.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -39,5 +39,5 @@ def __init__( # externed receivers must contain only alphanumeric characters or underscores, # so that the names can be easily and transparently turned into code if re.search(r"\W", args["name"]): - self.add_error("Parameter and Event names may only contain" - f"alphanumeric characters or underscore: '{args['name']}'") + self.add_error(f"Parameter and Event names may only contain \ + alphanumeric characters or underscore: '{args['name']}'") diff --git a/hvcc/core/hv2ir/HIrSend.py b/hvcc/core/hv2ir/HIrSend.py index a71b63a6..9fe6b633 100644 --- a/hvcc/core/hv2ir/HIrSend.py +++ b/hvcc/core/hv2ir/HIrSend.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,8 @@ from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph +from hvcc.types.IR import IRSendMessage + class HIrSend(HeavyIrObject): """ A specific implementation of the __send object. @@ -40,19 +42,20 @@ def __init__( self.add_error(f"Parameter and Event names may only contain \ alphanumeric characters or underscore: '{args['name']}'") - def get_ir_control_list(self) -> List: + def get_ir_control_list(self) -> List[IRSendMessage]: if self.graph is not None and self.name is not None: receive_objs = self.graph.resolve_objects_for_name(self.name, "__receive") on_message_list = [x for o in receive_objs for x in o.get_ir_on_message(inlet_index=0)] - return [{ - "id": self.id, - "type": "send", - "onMessage": [on_message_list], - "extern": self.args["extern"], - "attributes": self.args["attributes"], - "hash": self.args["hash"], - "display": self.args["name"], - "name": ((f"_{self.args['name']}") if re.match(r"\d", self.args["name"]) else self.args["name"]) - }] + + return [IRSendMessage( + id=self.id, + type="send", + onMessage=[on_message_list], + extern=self.args["extern"], + attributes=self.args["attributes"], + hash=self.args["hash"], + display=self.args["name"], + name=((f"_{self.args['name']}") if re.match(r"\d", self.args["name"]) else self.args["name"]) + )] else: return [] diff --git a/hvcc/core/hv2ir/HIrTabhead.py b/hvcc/core/hv2ir/HIrTabhead.py index 46f61427..106ffc0d 100644 --- a/hvcc/core/hv2ir/HIrTabhead.py +++ b/hvcc/core/hv2ir/HIrTabhead.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict, Optional +from typing import Dict, Optional, List, Set, Tuple from .HeavyIrObject import HeavyIrObject @@ -35,7 +35,7 @@ def __init__( assert obj_type in {"__tabhead~f", "__tabhead"} super().__init__(obj_type, args=args, graph=graph, annotations=annotations) - def reduce(self) -> Optional[tuple]: + def reduce(self) -> Optional[Tuple[Set, List]]: if self.graph is not None: table_obj = self.graph.resolve_object_for_name( self.args["table"], diff --git a/hvcc/core/hv2ir/HIrTabread.py b/hvcc/core/hv2ir/HIrTabread.py index 2969d90f..f3d8a5ec 100644 --- a/hvcc/core/hv2ir/HIrTabread.py +++ b/hvcc/core/hv2ir/HIrTabread.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict, Optional +from typing import Dict, Optional, List, Set, Tuple from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph @@ -34,7 +34,7 @@ def __init__( assert obj_type in {"__tabread~if", "__tabread~f", "__tabread_stoppable~f", "__tabreadu~f", "__tabread"} super().__init__(obj_type, args=args, graph=graph, annotations=annotations) - def reduce(self) -> Optional[tuple]: + def reduce(self) -> Optional[Tuple[Set, List]]: if self.graph is not None: table_obj = self.graph.resolve_object_for_name( self.args["table"], diff --git a/hvcc/core/hv2ir/HIrTabwrite.py b/hvcc/core/hv2ir/HIrTabwrite.py index 3f2bd1b4..518c04d3 100644 --- a/hvcc/core/hv2ir/HIrTabwrite.py +++ b/hvcc/core/hv2ir/HIrTabwrite.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict, Optional +from typing import Dict, Optional, List, Set, Tuple from .HeavyIrObject import HeavyIrObject from .HeavyGraph import HeavyGraph @@ -34,7 +34,7 @@ def __init__( assert obj_type in {"__tabwrite~f", "__tabwrite_stoppable~f", "__tabwrite"} super().__init__(obj_type, args=args, graph=graph, annotations=annotations) - def reduce(self) -> Optional[tuple]: + def reduce(self) -> Optional[Tuple[Set, List]]: if self.graph is not None: table_obj = self.graph.resolve_object_for_name( self.args["table"], diff --git a/hvcc/core/hv2ir/HLangAdc.py b/hvcc/core/hv2ir/HLangAdc.py index 25fe8220..58dc9780 100644 --- a/hvcc/core/hv2ir/HLangAdc.py +++ b/hvcc/core/hv2ir/HLangAdc.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,8 @@ from .HeavyLangObject import HeavyLangObject from .HeavyGraph import HeavyGraph +from hvcc.types.Lang import LangLetType + class HLangAdc(HeavyLangObject): """ adc @@ -35,10 +37,10 @@ def __init__( assert obj_type == "adc" super().__init__(obj_type, args, graph, num_inlets=0, - num_outlets=len(args[HeavyLangObject._HEAVY_LANG_DICT[obj_type]["args"][0]["name"]]), + num_outlets=len(args[self._HEAVY_LANG_DICT[obj_type].args[0].name]), annotations=annotations) - def _resolved_outlet_type(self, outlet_index: int = 0) -> str: + def _resolved_outlet_type(self, outlet_index: int = 0) -> LangLetType: return "~f>" def reduce(self) -> tuple: diff --git a/hvcc/core/hv2ir/HLangDac.py b/hvcc/core/hv2ir/HLangDac.py index 100ab9e1..7fa565f1 100644 --- a/hvcc/core/hv2ir/HLangDac.py +++ b/hvcc/core/hv2ir/HLangDac.py @@ -1,4 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,7 +34,7 @@ def __init__( ) -> None: assert obj_type == "dac" super().__init__(obj_type, args, graph, - num_inlets=len(args[HeavyLangObject._HEAVY_LANG_DICT["dac"]["args"][0]["name"]]), + num_inlets=len(args[self._HEAVY_LANG_DICT[obj_type].args[0].name]), num_outlets=0, annotations=annotations) diff --git a/hvcc/core/hv2ir/HLangSequence.py b/hvcc/core/hv2ir/HLangSequence.py index 2b99ef93..7d9886cf 100644 --- a/hvcc/core/hv2ir/HLangSequence.py +++ b/hvcc/core/hv2ir/HLangSequence.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,7 +33,7 @@ def __init__( annotations: Optional[Dict] = None ) -> None: # get the number of outlets that this object has - num_outlets = len(args[HeavyLangObject._HEAVY_LANG_DICT[obj_type]["args"][0]["name"]]) + num_outlets = len(args[self._HEAVY_LANG_DICT[obj_type].args[0].name]) super().__init__(obj_type, args, graph, num_inlets=1, num_outlets=num_outlets, diff --git a/hvcc/core/hv2ir/HLangTable.py b/hvcc/core/hv2ir/HLangTable.py index 3e50537d..2a534050 100644 --- a/hvcc/core/hv2ir/HLangTable.py +++ b/hvcc/core/hv2ir/HLangTable.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -44,8 +44,8 @@ def __init__( # externed tables must contain only alphanumeric characters or underscores, # so that the names can be easily and transparently turned into code if re.search(r"\W", args["name"]): - self.add_error("Table names may only contain alphanumeric characters" - f"or underscore: '{args['name']}'") + self.add_error(f"Table names may only contain alphanumeric characters \ + or underscore: '{args['name']}'") def reduce(self) -> tuple: x = HeavyIrObject("__table", self.args) diff --git a/hvcc/core/hv2ir/HeavyException.py b/hvcc/core/hv2ir/HeavyException.py index 409027ea..021debcc 100644 --- a/hvcc/core/hv2ir/HeavyException.py +++ b/hvcc/core/hv2ir/HeavyException.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Dict +from hvcc.types.compiler import CompilerNotif class HeavyException(Exception): @@ -24,4 +24,4 @@ class HeavyException(Exception): def __init__(self, message: str = "") -> None: super(Exception, self).__init__(message) self.message = message - self.notes: Dict = {} + self.notes: CompilerNotif = CompilerNotif() diff --git a/hvcc/core/hv2ir/HeavyGraph.py b/hvcc/core/hv2ir/HeavyGraph.py index 3093998f..ff60f496 100644 --- a/hvcc/core/hv2ir/HeavyGraph.py +++ b/hvcc/core/hv2ir/HeavyGraph.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ import os import re from collections import Counter -from typing import Optional, Union, Dict, List +from typing import Optional, Union, Dict, List, Set, Tuple from .BufferPool import BufferPool from .Connection import Connection @@ -27,6 +27,13 @@ from .HIrReceive import HIrReceive from .HeavyLangObject import HeavyLangObject +from hvcc.types.compiler import CompilerNotif +from hvcc.types.IR import ( + IRObjectdict, IRGraph, IRName, IRInit, IRControl, IRReceiver, + IRSendMessage, IROnMessage, IRSignalList, IRSignal, IRTable, IRNumTempBuffer, +) +from hvcc.types.Lang import LangLetType + class HeavyGraph(HeavyIrObject): """ Represents a graph. Subclasses HeavyIrObject for functionality. @@ -377,12 +384,12 @@ def get_output_channel_set(self, recursive: bool = False) -> set: else: return self.output_channel_set - def get_notices(self) -> Dict: + def get_notices(self) -> CompilerNotif: notices = HeavyLangObject.get_notices(self) for o in self.objs.values(): n = o.get_notices() - notices["warnings"].extend(n["warnings"]) - notices["errors"].extend(n["errors"]) + notices.warnings.extend(n.warnings) + notices.errors.extend(n.errors) return notices def get_objects_for_type(self, obj_type: str, recursive: bool = False) -> List: @@ -460,8 +467,8 @@ def prepare(self) -> None: self.assign_signal_buffers() except HeavyException as e: e.notes = self.get_notices() - e.notes["has_error"] = True - e.notes["exception"] = e + e.notes.has_error = True + e.notes.exception = e raise e def _remove_unused_inlet_connections(self) -> None: @@ -480,7 +487,7 @@ def _remove_unused_inlet_connections(self) -> None: for o in [o for o in self.objs.values() if (o.type == "__graph")]: o._remove_unused_inlet_connections() - def _resolved_outlet_type(self, outlet_index: int = 0) -> str: + def _resolved_outlet_type(self, outlet_index: int = 0) -> LangLetType: # a graph's outlet type depends on the connections incident on the # corresponding outlet object connection_type_set = {c.type for c in self.outlet_objs[outlet_index].inlet_connections[0]} @@ -492,7 +499,7 @@ def _resolved_outlet_type(self, outlet_index: int = 0) -> str: raise HeavyException(f"{self} has multiple incident connections of differing type.\ The outlet type cannot be explicitly resolved.") - def _resolve_connection_types(self, obj_stack: Optional[set] = None) -> Optional[None]: + def _resolve_connection_types(self, obj_stack: Optional[set] = None) -> None: """ Resolves the type of all connections before reduction to IR object types. If connections incident on an object are incompatible, they are either resolved, potentially by inserting conversion objects, or pruned. @@ -595,7 +602,7 @@ def remap_send_receive(self) -> None: for o in r_list: o.graph.remove_object(o) - def reduce(self) -> tuple: + def reduce(self) -> Tuple[Set, List]: """ Breaks this object into low-level objects. This method returns either the object that it is called on, or a graph. In case of a graph, it contains only low-level objects. Unnecessary connections are pruned. Because @@ -858,7 +865,7 @@ def __repr__(self) -> str: # Intermediate Representation generators # - def to_ir(self) -> Optional[Dict]: + def to_ir(self) -> Optional[IRGraph]: """ Returns Heavy intermediate representation. """ @@ -867,35 +874,35 @@ def to_ir(self) -> Optional[Dict]: output_channel_set = self.get_output_channel_set(recursive=True) if self.buffer_pool is not None: - return { - "name": { - "escaped": re.sub(r"\W", "_", self.xname), - "display": self.xname - }, - "objects": self.get_object_dict(), - "init": { - "order": self.get_ir_init_list() - }, - "tables": self.get_ir_table_dict(), - "control": { - "receivers": self.get_ir_receiver_dict(), - "sendMessage": self.get_ir_control_list() - }, - "signal": { - "numInputBuffers": max(input_channel_set) if len(input_channel_set) > 0 else 0, - "numOutputBuffers": max(output_channel_set) if len(output_channel_set) > 0 else 0, - "numTemporaryBuffers": { - "float": self.buffer_pool.num_buffers("~f>"), - "integer": self.buffer_pool.num_buffers("~i>") - }, - "processOrder": self.get_ir_signal_list() - } - } + return IRGraph( + name=IRName( + escaped=re.sub(r"\W", "_", self.xname), + display=self.xname + ), + objects=self.get_object_dict(), + init=IRInit( + order=self.get_ir_init_list() + ), + tables=self.get_ir_table_dict(), + control=IRControl( + receivers=self.get_ir_receiver_dict(), + sendMessage=self.get_ir_control_list() + ), + signal=IRSignal( + numInputBuffers=max(input_channel_set) if len(input_channel_set) > 0 else 0, + numOutputBuffers=max(output_channel_set) if len(output_channel_set) > 0 else 0, + numTemporaryBuffers=IRNumTempBuffer( + float=self.buffer_pool.num_buffers("~f>"), + integer=self.buffer_pool.num_buffers("~i>") + ), + processOrder=self.get_ir_signal_list() + ) + ) else: # we should never get here raise Exception - def get_object_dict(self) -> Dict: + def get_object_dict(self) -> Dict[str, IRObjectdict]: # d = {o.id: o.get_object_dict() for o in self.objs.values() if o.type not in # ["inlet", "__inlet", "outlet", "__outlet"]} d = {} @@ -905,20 +912,20 @@ def get_object_dict(self) -> Dict: d.update(o.get_object_dict()) return d - def get_ir_init_list(self) -> List: + def get_ir_init_list(self) -> List[str]: """ Init list is returned with all signal objects at the front, in the order that they are processed. This is to reduce cache misses on the signal object state as the process function is executed. """ init_list = [x for o in self.objs.values() for x in o.get_ir_init_list()] signal_list = self.get_ir_signal_list() - s_init_list = [x["id"] for x in signal_list if x["id"] in init_list] + s_init_list = [x.id for x in signal_list if x.id in init_list] i_init_list = [o_id for o_id in init_list if o_id not in s_init_list] ordered_init_list = s_init_list + i_init_list # ordered_init_list = list(OrderedDict.fromkeys(s_init_list + i_init_list)) return ordered_init_list - def get_ir_on_message(self, inlet_index: int = 0) -> List: + def get_ir_on_message(self, inlet_index: int = 0) -> List[IROnMessage]: # pass the method through the inlet object, but only follow control connections x = [] for c in self.inlet_objs[inlet_index].outlet_connections[0]: @@ -926,7 +933,7 @@ def get_ir_on_message(self, inlet_index: int = 0) -> List: x.extend(c.to_object.get_ir_on_message(c.inlet_index)) return x - def get_ir_table_dict(self) -> Dict: + def get_ir_table_dict(self) -> Dict[str, IRTable]: """ Returns a dictionary of all publicly visible tables at the root graph and their ids. """ @@ -941,29 +948,29 @@ def get_ir_table_dict(self) -> Dict: # escape table key to be used as the value for code stubs key = (f"_{k}") if re.match(r"\d", k) else k if key not in e: - e[key] = { - "id": v[0].id, - "display": k, - "hash": f"0x{HeavyLangObject.get_hash(k):X}", - "extern": v[0].args["extern"] - } + e[key] = IRTable( + id=v[0].id, + display=k, + hash=f"0x{HeavyLangObject.get_hash(k):X}", + extern=v[0].args["extern"] + ) return e - def get_ir_control_list(self) -> List: + def get_ir_control_list(self) -> List[IRSendMessage]: return [x for o in self.objs.values() for x in o.get_ir_control_list()] - def get_ir_receiver_dict(self) -> Dict: + def get_ir_receiver_dict(self) -> Dict[str, IRReceiver]: # NOTE(mhroth): this code assumes that v is always an array of length 1, # as the grouping of control receivers should have grouped all same-named # receivers into one logical receiver. # NOTE(mhroth): a code-compatible name is only necessary for externed receivers - return {((f"_{k}") if re.match(r"\d", k) else k): { - "display": k, - "hash": f"0x{HeavyLangObject.get_hash(k):X}", - "extern": v[0].args["extern"], - "attributes": v[0].args["attributes"], - "ids": [v[0].id] - } for k, v in self.local_vars.get_registered_objects_for_type("__receive").items()} - - def get_ir_signal_list(self) -> List: + return {((f"_{k}") if re.match(r"\d", k) else k): IRReceiver( + display=k, + hash=f"0x{HeavyLangObject.get_hash(k):X}", + extern=v[0].args["extern"], + attributes=v[0].args["attributes"], + ids=[v[0].id] + ) for k, v in self.local_vars.get_registered_objects_for_type("__receive").items()} + + def get_ir_signal_list(self) -> List[IRSignalList]: return [x for o in self.signal_order for x in o.get_ir_signal_list()] diff --git a/hvcc/core/hv2ir/HeavyIrObject.py b/hvcc/core/hv2ir/HeavyIrObject.py index e68429fc..21e15ee8 100644 --- a/hvcc/core/hv2ir/HeavyIrObject.py +++ b/hvcc/core/hv2ir/HeavyIrObject.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +24,10 @@ from .HeavyLangObject import HeavyLangObject from .BufferPool import BufferPool +from hvcc.types.IR import HeavyIRType, IRNode, IRObjectdict, IRSendMessage, IROnMessage, IRSignalList, IRBuffer +from hvcc.types.Lang import LangLetType + + if TYPE_CHECKING: from .HeavyGraph import HeavyGraph @@ -36,7 +40,7 @@ class HeavyIrObject(HeavyLangObject): # load the HeavyIR object definitions with open(os.path.join(os.path.dirname(__file__), "../json/heavy.ir.json"), "r") as f: - __HEAVY_OBJS_IR_DICT = json.load(f) + __HEAVY_OBJS_IR_DICT = HeavyIRType(**json.load(f)).root def __init__( self, @@ -48,9 +52,13 @@ def __init__( annotations: Optional[Dict] = None ) -> None: # allow the number of inlets and outlets to be overridden - num_inlets = len(HeavyIrObject.__HEAVY_OBJS_IR_DICT[obj_type]["inlets"]) if num_inlets < 0 else num_inlets + num_inlets = (len(self.__HEAVY_OBJS_IR_DICT[obj_type].inlets) + if num_inlets < 0 + else num_inlets) - num_outlets = len(HeavyIrObject.__HEAVY_OBJS_IR_DICT[obj_type]["outlets"]) if num_outlets < 0 else num_outlets + num_outlets = (len(self.__HEAVY_OBJS_IR_DICT[obj_type].outlets) + if num_outlets < 0 + else num_outlets) super().__init__(obj_type, args, graph, num_inlets, num_outlets, annotations) @@ -69,51 +77,51 @@ def __resolve_default_ir_args(self) -> None: """ Resolves missing default arguments. Also checks to make sure that all required arguments are present. """ - if self.type in HeavyIrObject.__HEAVY_OBJS_IR_DICT: - for arg in self.__obj_desc.get("args", []): - if arg["name"] not in self.args: + if self.type in self.__HEAVY_OBJS_IR_DICT.keys(): + for arg in self.__obj_desc.args: + if arg.name not in self.args: # if a defined argument is not in the argument dictionary - if not arg["required"]: + if not arg.required: # if the argument is not required, use the default - self.args[arg["name"]] = arg["default"] + self.args[arg.name] = arg.default else: - self.add_error(f"Required argument \"{arg['name']}\" not present for object {self}.") + self.add_error(f"Required argument \"{arg.name}\" not present for object {self}.") else: # enforce argument types. # if the default argument is null, don't worry about about the arg - if arg["default"] is not None: - self.args[arg["name"]] = HeavyLangObject.force_arg_type( - self.args[arg["name"]], - arg["value_type"], + if arg.default is not None: + self.args[arg.name] = self.force_arg_type( + self.args[arg.name], + arg.value_type, self.graph) @classmethod def is_ir(cls, obj_type: str) -> bool: """Returns true if the type is an IR object. False otherwise. """ - return obj_type in HeavyIrObject.__HEAVY_OBJS_IR_DICT + return obj_type in cls.__HEAVY_OBJS_IR_DICT.keys() @property def does_process_signal(self) -> bool: """Returns True if this object processes a signal. False otherwise. """ - return self.__obj_desc["ir"]["signal"] + return self.__obj_desc.ir.signal @property - def __obj_desc(self) -> Dict: + def __obj_desc(self) -> IRNode: """ Returns the original HeavyIR object description. """ - return HeavyIrObject.__HEAVY_OBJS_IR_DICT[self.type] + return self.__HEAVY_OBJS_IR_DICT[self.type] def inlet_requires_signal(self, inlet_index: int = 0) -> bool: """ Returns True if the indexed inlet requires a signal connection. False otherwise. """ - return self.__obj_desc["inlets"][inlet_index] in {"~i>", "~f>"} + return self.__obj_desc.inlets[inlet_index] in {"~i>", "~f>"} def outlet_requires_signal(self, inlet_index: int = 0) -> bool: """ Returns True if the indexed outlet requires a signal connection. False otherwise. """ - return self.__obj_desc["outlets"][inlet_index] in {"~i>", "~f>"} + return self.__obj_desc.outlets[inlet_index] in {"~i>", "~f>"} def reduce(self) -> Optional[tuple]: # A Heavy IR object is already reduced. Returns itself and no connection changes. @@ -179,46 +187,46 @@ def assign_signal_buffers(self, buffer_pool: Optional[BufferPool]) -> None: if len(self.outlet_connections[i]) == 0: exclude_set.add(b) - def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[str]: + def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[LangLetType]: """ Returns the connection type at the given outlet. This information is always well-defined for IR objects. """ - return self.__obj_desc["outlets"][outlet_index] + return self.__obj_desc.outlets[outlet_index] # # Intermediate Representation generators # - def get_object_dict(self) -> Dict: + def get_object_dict(self) -> Dict[str, IRObjectdict]: """ Returns a dictionary of all constituent low-level objects, indexed by id, including their arguments and type. """ return { - self.id: { - "args": self.args, - "type": self.type - } + self.id: IRObjectdict( + args=self.args, + type=self.type + ) } - def get_ir_init_list(self) -> List: + def get_ir_init_list(self) -> List[str]: """ Returns a list of all object id for obejcts that need initialisation. """ - return [self.id] if self.__obj_desc["ir"]["init"] else [] + return [self.id] if self.__obj_desc.ir.init else [] - def get_ir_on_message(self, inlet_index: int = 0) -> List: + def get_ir_on_message(self, inlet_index: int = 0) -> List[IROnMessage]: """ Returns an array of dictionaries containing the information for the corresponding on_message call. """ - return [{ - "id": self.id, - "inletIndex": inlet_index - }] + return [IROnMessage( + id=self.id, + inletIndex=inlet_index + )] - def get_ir_control_list(self) -> List: + def get_ir_control_list(self) -> List[IRSendMessage]: """ Returns the intermediate representation for object control functions. Basically, does sendMessage() need to be written? """ - if self.__obj_desc["ir"]["control"]: + if self.__obj_desc.ir.control: on_message_list = [] for connections in self.outlet_connections: on_messages_let = [] @@ -226,26 +234,29 @@ def get_ir_control_list(self) -> List: for c in [c for c in connections if c.is_control]: on_messages_let.extend(c.to_object.get_ir_on_message(c.inlet_index)) on_message_list.append(on_messages_let) - return [{ - "id": self.id, - "onMessage": on_message_list - }] + + return [IRSendMessage( + id=self.id, + onMessage=on_message_list + )] else: return [] - def get_ir_signal_list(self) -> List: + def get_ir_signal_list(self) -> List[IRSignalList]: """ Returns the intermediate representation for object process functions. Only outputs buffer information for lets that require a signal. """ # we assume that this method will only be called on signal objects - assert self.__obj_desc["ir"]["signal"] - - return [{ - "id": self.id, - "inputBuffers": [ - {"type": b[0], "index": b[1]} for i, b in enumerate(self.inlet_buffers) - if self.inlet_requires_signal(i)], - "outputBuffers": [ - {"type": b[0], "index": b[1]} for i, b in enumerate(self.outlet_buffers) - if self.outlet_requires_signal(i)] - }] + assert self.__obj_desc.ir.signal + + return [IRSignalList( + id=self.id, + inputBuffers=[ + IRBuffer(type=b[0], index=b[1]) for i, b in enumerate(self.inlet_buffers) + if self.inlet_requires_signal(i) + ], + outputBuffers=[ + IRBuffer(type=b[0], index=b[1]) for i, b in enumerate(self.outlet_buffers) + if self.outlet_requires_signal(i) + ] + )] diff --git a/hvcc/core/hv2ir/HeavyLangObject.py b/hvcc/core/hv2ir/HeavyLangObject.py index f8d3b40f..69c66bcc 100644 --- a/hvcc/core/hv2ir/HeavyLangObject.py +++ b/hvcc/core/hv2ir/HeavyLangObject.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,6 +26,9 @@ from .Connection import Connection from .HeavyException import HeavyException +from hvcc.types.compiler import CompilerMsg, CompilerNotif +from hvcc.types.Lang import HeavyLangType, LangNode, LangLet, LangLetType, LangValueType + if TYPE_CHECKING: from .HeavyGraph import HeavyGraph from .HeavyIrObject import HeavyIrObject @@ -40,7 +43,7 @@ class HeavyLangObject: # load the Heavy object definitions with open(os.path.join(os.path.dirname(__file__), "../json/heavy.lang.json"), "r") as f: - _HEAVY_LANG_DICT = json.load(f) + _HEAVY_LANG_DICT = HeavyLangType(**json.load(f)).root def __init__( self, @@ -67,18 +70,18 @@ def __init__( self.annotations: Dict = annotations or {} # a list of locally generated warnings and errors (notifications) - self.warnings: List = [] - self.errors: List = [] + self.warnings: List[CompilerMsg] = [] + self.errors: List[CompilerMsg] = [] # resolve arguments and fill in missing defaults for HeavyLang objects self.__resolve_default_lang_args() # the list of connections at each inlet - num_inlets = num_inlets if num_inlets >= 0 else len(self._obj_desc["inlets"]) + num_inlets = num_inlets if num_inlets >= 0 else len(self._obj_desc.inlets) self.inlet_connections: List = [[] for _ in range(num_inlets)] # the list of connections at each outlet - num_outlets = num_outlets if num_outlets >= 0 else len(self._obj_desc["outlets"]) + num_outlets = num_outlets if num_outlets >= 0 else len(self._obj_desc.outlets) self.outlet_connections: List = [[] for _ in range(num_outlets)] @property @@ -107,43 +110,49 @@ def name(self) -> Optional[str]: return self.args.get("name", None) @property - def _obj_desc(self) -> Dict: + def _obj_desc(self) -> LangNode: """ Returns the HeavyLang object description. """ return self._HEAVY_LANG_DICT[self.type] - def inlet_connection_type(self, index: int) -> Dict: - return self._obj_desc["inlets"][index] + def inlet_connection_type(self, index: int) -> LangLet: + return self._obj_desc.inlets[index] - def outlet_connection_type(self, index: int) -> Dict: - return self._obj_desc["outlets"][index] + def outlet_connection_type(self, index: int) -> LangLet: + return self._obj_desc.outlets[index] def name_for_arg(self, index: int = 0) -> str: """ Returns the name of the argument at the given index. """ - return self._obj_desc["args"][index]["name"] + return self._obj_desc.args[index].name def add_warning(self, warning: str) -> None: """ Add a warning to this object. """ - self.warnings.append({"message": warning}) + self.warnings.append(CompilerMsg(message=warning)) def add_error(self, error: str) -> None: """ Add an error to this object and raise an exception. """ - self.errors.append({"message": error}) + self.errors.append(CompilerMsg(message=error)) raise HeavyException(error) - def get_notices(self) -> Dict: + def get_notices(self) -> CompilerNotif: """ Returns a dictionary of all warnings and errors at this object. """ - return { - "warnings": [{"message": f"{self}: {n['message']}"} for n in self.warnings], - "errors": [{"message": f"{self}: {n['message']}"} for n in self.errors], - } + return CompilerNotif( + has_error=len(self.errors) > 0, + warnings=[CompilerMsg(message=f"{self}: {n.message}") for n in self.warnings], + errors=[CompilerMsg(message=f"{self}: {n.message}") for n in self.errors], + ) @classmethod - def force_arg_type(cls, value: Any, value_type: str, graph: Optional['HeavyGraph'] = None) -> Any: + def force_arg_type( + cls, + value: LangValueType, + value_type: Optional[str] = None, + graph: Optional['HeavyGraph'] = None + ) -> Any: """ Attempts to convert a value to a given value type. Raises an Exception otherwise. If the value_type is unknown and a graph is provided, a warning will be registered. """ @@ -156,7 +165,7 @@ def force_arg_type(cls, value: Any, value_type: str, graph: Optional['HeavyGraph return int(decimal.Decimal(value)) elif value_type == "string": return str(value) if value is not None else None - elif value_type == "boolean": + elif value_type == "bool": if isinstance(value, str): return value.strip().lower() not in ["false", "f", "0"] else: @@ -195,20 +204,20 @@ def __resolve_default_lang_args(self) -> None: """ Resolves missing default arguments. Also checks to make sure that all required arguments are present. Does nothing if the object is IR. """ - if self.type in HeavyLangObject._HEAVY_LANG_DICT: - for arg in self._obj_desc["args"]: - if arg["name"] not in self.args: + if self.type in self._HEAVY_LANG_DICT.keys(): + for arg in self._obj_desc.args: + if arg.name not in self.args: # if a defined argument is not in the argument dictionary - if not arg["required"]: + if not arg.required: # if the argument is not required, use the default - self.args[arg["name"]] = arg["default"] + self.args[arg.name] = arg.default else: - self.add_error(f"Required argument \"{arg['name']}\" not present for object {self}.") + self.add_error(f"Required argument \"{arg.name}\" not present for object {self}.") else: # enforce argument types - self.args[arg["name"]] = HeavyLangObject.force_arg_type( - self.args[arg["name"]], - arg["value_type"], + self.args[arg.name] = self.force_arg_type( + self.args[arg.name], + arg.value_type, self.graph) @property @@ -242,7 +251,7 @@ def remove_connection(self, c: Connection) -> None: else: raise HeavyException(f"Connection {c} does not connect to this object {self}.") - def replace_connection(self, c: Connection, n_list: list) -> None: + def replace_connection(self, c: Connection, n_list: List) -> None: """ Replaces connection c with connection list n_list, maintaining connection order """ if c.from_object is self: @@ -314,12 +323,12 @@ def is_root(self) -> bool: """ return all(len(c) == 0 for c in self.inlet_connections) - def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[str]: + def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[LangLetType]: """ Returns the connection type expected at the given outlet. The result may be influenced by the state of the input connections. """ # get the defined connection type - connection_type = self._obj_desc["outlets"][outlet_index]["connectionType"] + connection_type = self._obj_desc.outlets[outlet_index].connectionType if connection_type == "-~>" and self.graph is not None: # if the connection type is defined as mixed, # use the default approach to resolve it @@ -341,7 +350,7 @@ def _resolved_outlet_type(self, outlet_index: int = 0) -> Optional[str]: return None - def _resolve_connection_types(self, obj_stack: Optional[set] = None) -> Optional[None]: + def _resolve_connection_types(self, obj_stack: Optional[set] = None) -> None: """ Resolves the type of all connections before reduction to IR object types. If connections incident on an object are incompatible, they are either resolved, potentially by inserting conversion objects, or pruned. diff --git a/hvcc/core/hv2ir/HeavyParser.py b/hvcc/core/hv2ir/HeavyParser.py index 180de62d..de707d8d 100644 --- a/hvcc/core/hv2ir/HeavyParser.py +++ b/hvcc/core/hv2ir/HeavyParser.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ import random import os -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Set, Tuple from .HIrConvolution import HIrConvolution from .HIrInlet import HIrInlet @@ -194,8 +194,8 @@ def graph_from_object( if g.is_root_graph(): # add the notification dictionary at the top level e.notes = g.get_notices() - e.notes["has_error"] = True - e.notes["exception"] = e + e.notes.has_error = True + e.notes.exception = e raise e if (g.graph is None) or (g.graph.file != g.file): @@ -233,7 +233,7 @@ def __init__( assert obj_type == "if" super().__init__("if", args, graph, num_inlets=2, num_outlets=2, annotations=annotations) - def reduce(self) -> tuple: + def reduce(self) -> Tuple[Set, List]: if self.has_inlet_connection_format(["cc", "_c", "c_", "__"]): x = HeavyIrObject("__if", self.args) elif self.has_inlet_connection_format("ff"): @@ -263,7 +263,7 @@ def __init__( assert obj_type == "noise" super().__init__("noise", args, graph, num_inlets=1, num_outlets=1, annotations=annotations) - def reduce(self) -> tuple: + def reduce(self) -> Tuple[Set, List]: seed = int(random.uniform(1, 2147483647)) # assign a random 32-bit seed noise_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "./hvlib/noise.hv.json") x = HeavyParser.graph_from_file(noise_path, graph_args={"seed": seed}) diff --git a/hvcc/core/hv2ir/hv2ir.py b/hvcc/core/hv2ir/hv2ir.py index 8eaf6535..42170050 100644 --- a/hvcc/core/hv2ir/hv2ir.py +++ b/hvcc/core/hv2ir/hv2ir.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2023 Wasted Audio +# Copyright (C) 2023-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,11 +19,13 @@ import os import time -from typing import Optional, Dict +from typing import Optional from .HeavyException import HeavyException from .HeavyParser import HeavyParser +from hvcc.types.compiler import CompilerResp, CompilerNotif, CompilerMsg + class hv2ir: @@ -34,7 +36,7 @@ def compile( ir_file: str, patch_name: Optional[str] = None, verbose: bool = False - ) -> Dict: + ) -> CompilerResp: """ Compiles a HeavyLang file into a HeavyIR file. Returns a tuple of compile time in seconds, a notification dictionary, and a heavy object counter. @@ -50,21 +52,20 @@ def compile( # parse heavy file hv_graph = HeavyParser.graph_from_file(hv_file=hv_file, xname=patch_name) except HeavyException as e: - return { - "stage": "hv2ir", - "compile_time": time.time() - tick, - "notifs": { - "has_error": True, - "exception": e, - "errors": [{"message": e.message}], - "warnings": [] - }, - "obj_counter": None, - "in_file": os.path.basename(hv_file), - "in_dir": os.path.dirname(hv_file), - "out_file": os.path.basename(ir_file), - "out_dir": os.path.dirname(ir_file) - } + return CompilerResp( + stage="hv2ir", + compile_time=time.time() - tick, + notifs=CompilerNotif( + has_error=True, + exception=e, + errors=[CompilerMsg(message=e.message)], + warnings=[] + ), + in_file=os.path.basename(hv_file), + in_dir=os.path.dirname(hv_file), + out_file=os.path.basename(ir_file), + out_dir=os.path.dirname(ir_file) + ) try: # get a counter of all heavy objects @@ -80,58 +81,59 @@ def compile( # generate Heavy.IR ir = hv_graph.to_ir() except HeavyException as e: - return { - "stage": "hv2ir", - "compile_time": time.time() - tick, - "notifs": { - "has_error": True, - "exception": e, - "errors": [{"message": e.message}], - "warnings": [] - }, - "obj_counter": hv_counter, - "in_file": os.path.basename(hv_file), - "in_dir": os.path.dirname(hv_file), - "out_file": os.path.basename(ir_file), - "out_dir": os.path.dirname(ir_file) - } - - # write the hv.ir file - with open(ir_file, "w") as f: - if verbose: - json.dump( - ir, - f, - sort_keys=True, - indent=2, - separators=(",", ": ")) - else: - json.dump(ir, f) - - if verbose and ir is not None: - if len(ir["signal"]["processOrder"]) > 0: - print("") - print("=== Signal Order ===") - for so in ir["signal"]["processOrder"]: - o = ir["objects"][so["id"]] - if len(o["args"]) > 0: - print("{0} {{{1}}}".format( - o["type"], - " ".join([f"{k}:{v}" for k, v in o["args"].items()]))) - else: - print(o["type"]) - - return { - "stage": "hv2ir", - "compile_time": time.time() - tick, # record the total compile time - "notifs": hv_graph.get_notices(), - "obj_counter": hv_counter, - "in_file": os.path.basename(hv_file), - "in_dir": os.path.dirname(hv_file), - "out_file": os.path.basename(ir_file), - "out_dir": os.path.dirname(ir_file), - "ir": ir - } + return CompilerResp( + stage="hv2ir", + compile_time=time.time() - tick, + notifs=CompilerNotif( + has_error=True, + exception=e, + errors=[CompilerMsg(message=e.message)], + warnings=[] + ), + in_file=os.path.basename(hv_file), + in_dir=os.path.dirname(hv_file), + out_file=os.path.basename(ir_file), + out_dir=os.path.dirname(ir_file), + obj_counter=hv_counter + ) + + if ir is not None: + # write the hv.ir file + with open(ir_file, "w") as f: + if verbose: + json.dump( + ir.model_dump(), + f, + sort_keys=True, + indent=2, + separators=(",", ": ")) + else: + json.dump(ir.model_dump(), f) + + if verbose and ir is not None: + if len(ir.signal.processOrder) > 0: + print("") + print("=== Signal Order ===") + for so in ir.signal.processOrder: + o = ir.objects[so.id] + if len(o.args) > 0: + print("{0} {{{1}}}".format( + o.type, + " ".join([f"{k}:{v}" for k, v in o.args.items()]))) + else: + print(o.type) + + return CompilerResp( + stage="hv2ir", + compile_time=time.time() - tick, # record the total compile time + notifs=hv_graph.get_notices(), + in_file=os.path.basename(hv_file), + in_dir=os.path.dirname(hv_file), + out_file=os.path.basename(ir_file), + out_dir=os.path.dirname(ir_file), + obj_counter=hv_counter, + ir=ir + ) def main() -> None: @@ -158,7 +160,7 @@ def main() -> None: verbose=args.verbose) if args.verbose: - print(f"Total hv2ir time: {(d['compile_time'] * 1000):.2f}ms") + print(f"Total hv2ir time: {(d.compile_time * 1000):.2f}ms") if __name__ == "__main__": diff --git a/hvcc/core/json/heavy.lang.json b/hvcc/core/json/heavy.lang.json index 938da7b7..0d89aed5 100644 --- a/hvcc/core/json/heavy.lang.json +++ b/hvcc/core/json/heavy.lang.json @@ -2122,7 +2122,7 @@ { "name": "extern", "value_type": "bool", - "description": "Determines if the should table should be publicly exposed in a framework.", + "description": "Determines if the table should be publicly exposed in a framework.", "default": false, "required": false } @@ -2304,7 +2304,7 @@ }, { "default": false, - "value_type": "boolean", + "value_type": "bool", "name": "reverse", "description": "True if the value and step increments should be reversed.", "required": false diff --git a/hvcc/generators/c2daisy/c2daisy.py b/hvcc/generators/c2daisy/c2daisy.py index 4ef47117..8565bf1e 100644 --- a/hvcc/generators/c2daisy/c2daisy.py +++ b/hvcc/generators/c2daisy/c2daisy.py @@ -7,9 +7,12 @@ from typing import Any, Dict, Optional from ..copyright import copyright_manager -from ..types.meta import Meta, Daisy from . import parameters +from hvcc.interpreters.pd2hv.NotificationEnum import NotificationEnum +from hvcc.types.compiler import Generator, CompilerResp, CompilerNotif, CompilerMsg, ExternInfo +from hvcc.types.meta import Meta, Daisy + hv_midi_messages = { "__hv_noteout", @@ -23,7 +26,7 @@ } -class c2daisy: +class c2daisy(Generator): """ Generates a Daisy wrapper for a given patch. """ @@ -32,14 +35,14 @@ def compile( cls, c_src_dir: str, out_dir: str, - externs: Dict, + externs: ExternInfo, patch_name: Optional[str] = None, patch_meta: Meta = Meta(), num_input_channels: int = 0, num_output_channels: int = 0, copyright: Optional[str] = None, verbose: Optional[bool] = False - ) -> Dict: + ) -> CompilerResp: tick = time.time() @@ -69,11 +72,11 @@ def compile( header, board_info = json2daisy.generate_header_from_name(board) # remove heavy out params from externs - externs['parameters']['out'] = [ - t for t in externs['parameters']['out'] if not any(x == y for x in hv_midi_messages for y in t)] + externs.parameters.outParam = [ + t for t in externs.parameters.outParam if not any(x == y for x in hv_midi_messages for y in t)] component_glue = parameters.parse_parameters( - externs['parameters'], board_info['components'], board_info['aliases'], 'hardware') + externs.parameters, board_info['components'], board_info['aliases'], 'hardware') component_glue['class_name'] = board_info['name'] component_glue['patch_name'] = patch_name component_glue['header'] = f"HeavyDaisy_{patch_name}.hpp" @@ -83,7 +86,7 @@ def compile( component_glue['displayprocess'] = board_info['displayprocess'] component_glue['debug_printing'] = daisy_meta.debug_printing component_glue['usb_midi'] = daisy_meta.usb_midi - component_glue['pool_sizes_kb'] = externs["memoryPoolSizesKb"] + component_glue['pool_sizes_kb'] = externs.memoryPoolSizesKb # samplerate samplerate = daisy_meta.samplerate @@ -138,36 +141,27 @@ def compile( # ====================================================================================== - return { - "stage": "c2daisy", - "notifs": { - "has_error": False, - "exception": None, - "warnings": [], - "errors": [] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": os.path.basename(daisy_h_path), - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2daisy", + in_dir=c_src_dir, + out_dir=out_dir, + out_file=os.path.basename(daisy_h_path), + compile_time=time.time() - tick + ) except Exception as e: - return { - "stage": "c2daisy", - "notifs": { - "has_error": True, - "exception": e, - "warnings": [], - "errors": [{ - "enum": -1, - "message": str(e) - }] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": "", - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2daisy", + notifs=CompilerNotif( + has_error=True, + exception=e, + warnings=[], + errors=[CompilerMsg( + enum=NotificationEnum.ERROR_EXCEPTION, + message=str(e) + )] + ), + in_dir=c_src_dir, + out_dir=out_dir, + compile_time=time.time() - tick + ) diff --git a/hvcc/generators/c2daisy/parameters.py b/hvcc/generators/c2daisy/parameters.py index bf89b107..5af55fb6 100644 --- a/hvcc/generators/c2daisy/parameters.py +++ b/hvcc/generators/c2daisy/parameters.py @@ -2,6 +2,8 @@ from copy import deepcopy from typing import Any, Dict, Optional +from hvcc.types.compiler import ExternParams + def filter_match( set: Dict, @@ -128,7 +130,7 @@ def de_alias( def parse_parameters( - parameters: Dict, + parameters: ExternParams, components: Dict, aliases: Dict, object_name: str @@ -144,16 +146,16 @@ def parse_parameters( replacements: Dict = {} params_in: Dict = {} params_in_original_names: Dict = {} - for key, item in parameters['in']: + for key, recv in parameters.inParam: de_aliased = de_alias(key, aliases, components) - params_in[de_aliased] = item + params_in[de_aliased] = recv params_in_original_names[de_aliased] = key params_out = {} params_out_original_names = {} - for key, item in parameters['out']: + for key, msg in parameters.outParam: de_aliased = de_alias(key, aliases, components) - params_out[de_aliased] = item + params_out[de_aliased] = msg params_out_original_names[de_aliased] = key [verify_param_exists(key, params_in_original_names[key], diff --git a/hvcc/generators/c2daisy/templates/Makefile b/hvcc/generators/c2daisy/templates/Makefile index e7327c2c..0b454a74 100644 --- a/hvcc/generators/c2daisy/templates/Makefile +++ b/hvcc/generators/c2daisy/templates/Makefile @@ -4,9 +4,9 @@ TARGET = HeavyDaisy_{{name}} # Library Locations LIBDAISY_DIR = {{libdaisy_path}} -{%- if linker_script != '' %} +{% if linker_script != '' %} LDSCRIPT = {{linker_script}} -{%- endif %} +{% endif %} {% if bootloader != None %} APP_TYPE = {{bootloader}} diff --git a/hvcc/generators/c2dpf/c2dpf.py b/hvcc/generators/c2dpf/c2dpf.py index 90f2b508..2e9c09eb 100644 --- a/hvcc/generators/c2dpf/c2dpf.py +++ b/hvcc/generators/c2dpf/c2dpf.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021-2023 Wasted Audio +# Copyright (C) 2021-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,14 +17,17 @@ import shutil import time import jinja2 -from typing import Dict, Optional +from typing import Optional from ..copyright import copyright_manager from ..filters import filter_uniqueid -from ..types.meta import Meta, DPF +from hvcc.interpreters.pd2hv.NotificationEnum import NotificationEnum +from hvcc.types.compiler import Generator, CompilerResp, CompilerMsg, CompilerNotif, ExternInfo +from hvcc.types.meta import Meta, DPF -class c2dpf: + +class c2dpf(Generator): """ Generates a DPF wrapper for a given patch. """ @@ -33,20 +36,20 @@ def compile( cls, c_src_dir: str, out_dir: str, - externs: Dict, + externs: ExternInfo, patch_name: Optional[str] = None, patch_meta: Meta = Meta(), num_input_channels: int = 0, num_output_channels: int = 0, copyright: Optional[str] = None, verbose: Optional[bool] = False - ) -> Dict: + ) -> CompilerResp: tick = time.time() out_dir = os.path.join(out_dir, "plugin") - receiver_list = externs['parameters']['in'] - sender_list = externs["parameters"]["out"] + receiver_list = externs.parameters.inParam + sender_list = externs.parameters.outParam dpf_meta: DPF = patch_meta.dpf dpf_path = dpf_meta.dpf_path @@ -96,7 +99,7 @@ def compile( num_output_channels=num_output_channels, receivers=receiver_list, senders=sender_list, - pool_sizes_kb=externs["memoryPoolSizesKb"], + pool_sizes_kb=externs.memoryPoolSizesKb, copyright=copyright_c)) if dpf_meta.enable_ui: dpf_ui_path = os.path.join(source_dir, f"HeavyDPF_{patch_name}_UI.cpp") @@ -116,7 +119,7 @@ def compile( class_name=f"HeavyDPF_{patch_name}", num_input_channels=num_input_channels, num_output_channels=num_output_channels, - pool_sizes_kb=externs["memoryPoolSizesKb"], + pool_sizes_kb=externs.memoryPoolSizesKb, copyright=copyright_c)) # plugin makefile @@ -124,6 +127,7 @@ def compile( f.write(env.get_template("Makefile_plugin").render( name=patch_name, meta=dpf_meta, + nosimd=patch_meta.nosimd, dpf_path=dpf_path)) # project makefile @@ -133,36 +137,27 @@ def compile( meta=dpf_meta, dpf_path=dpf_path)) - return { - "stage": "c2dpf", - "notifs": { - "has_error": False, - "exception": None, - "warnings": [], - "errors": [] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": os.path.basename(dpf_h_path), - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2dpf", + in_dir=c_src_dir, + out_dir=out_dir, + out_file=os.path.basename(dpf_h_path), + compile_time=time.time() - tick + ) except Exception as e: - return { - "stage": "c2dpf", - "notifs": { - "has_error": True, - "exception": e, - "warnings": [], - "errors": [{ - "enum": -1, - "message": str(e) - }] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": "", - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2dpf", + notifs=CompilerNotif( + has_error=True, + exception=e, + warnings=[], + errors=[CompilerMsg( + enum=NotificationEnum.ERROR_EXCEPTION, + message=str(e) + )] + ), + in_dir=c_src_dir, + out_dir=out_dir, + compile_time=time.time() - tick + ) diff --git a/hvcc/generators/c2dpf/templates/DistrhoPluginInfo.h b/hvcc/generators/c2dpf/templates/DistrhoPluginInfo.h index 16efa07f..a923014f 100644 --- a/hvcc/generators/c2dpf/templates/DistrhoPluginInfo.h +++ b/hvcc/generators/c2dpf/templates/DistrhoPluginInfo.h @@ -52,8 +52,6 @@ {%- endif %} {%- if meta.enable_modgui is sameas true %} -#ifdef __MOD_DEVICES__ #undef DISTRHO_PLUGIN_USES_MODGUI #define DISTRHO_PLUGIN_USES_MODGUI 1 -#endif {%- endif %} diff --git a/hvcc/generators/c2dpf/templates/HeavyDPF.cpp b/hvcc/generators/c2dpf/templates/HeavyDPF.cpp index 0d73340c..39844417 100644 --- a/hvcc/generators/c2dpf/templates/HeavyDPF.cpp +++ b/hvcc/generators/c2dpf/templates/HeavyDPF.cpp @@ -193,6 +193,7 @@ void {{class_name}}::setOutputParameter(uint32_t sendHash, const HvMessage *m) #endif {% endif %} +{% include 'hostTransportEvents.cpp' %} // ------------------------------------------------------------------- @@ -206,6 +207,7 @@ void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames, void {{class_name}}::run(const float** inputs, float** outputs, uint32_t frames) { #endif + hostTransportEvents(frames); {% if meta.denormals is sameas false %} const ScopedDenormalDisable sdd; {% endif %} diff --git a/hvcc/generators/c2dpf/templates/HeavyDPF.hpp b/hvcc/generators/c2dpf/templates/HeavyDPF.hpp index 3ea3aec0..eaeb03d6 100644 --- a/hvcc/generators/c2dpf/templates/HeavyDPF.hpp +++ b/hvcc/generators/c2dpf/templates/HeavyDPF.hpp @@ -47,6 +47,7 @@ class {{class_name}} : public Plugin void handleMidiInput(uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount); void handleMidiSend(uint32_t sendHash, const HvMessage *m); + void hostTransportEvents(uint32_t frames); void setOutputParameter(uint32_t sendHash, const HvMessage *m); protected: diff --git a/hvcc/generators/c2dpf/templates/Makefile_plugin b/hvcc/generators/c2dpf/templates/Makefile_plugin index 6becc0d9..7afffaa5 100644 --- a/hvcc/generators/c2dpf/templates/Makefile_plugin +++ b/hvcc/generators/c2dpf/templates/Makefile_plugin @@ -35,6 +35,11 @@ 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 nosimd is sameas true %} +BUILD_C_FLAGS += -DHV_SIMD_NONE +BUILD_CXX_FLAGS += -DHV_SIMD_NONE +{%- endif %} + {% if meta.plugin_formats|length > 0 %} {%- for format in meta.plugin_formats %} TARGETS += {{format}} diff --git a/hvcc/generators/c2dpf/templates/hostTransportEvents.cpp b/hvcc/generators/c2dpf/templates/hostTransportEvents.cpp new file mode 100644 index 00000000..5e39f313 --- /dev/null +++ b/hvcc/generators/c2dpf/templates/hostTransportEvents.cpp @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------- +// Host Transport Events handler + +void {{class_name}}::hostTransportEvents(uint32_t frames) +{ + // Realtime events + const TimePosition& timePos(getTimePosition()); + bool reset = false; + + if (timePos.playing) + { + if (timePos.frame == 0) + { + _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, + "ff", (float) MIDI_RT_RESET, 0.0); + reset = true; + } + + if (! this->wasPlaying) + { + if (timePos.frame == 0) + { + _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, + "ff", (float) MIDI_RT_START, 0.0); + } + if (! reset) + { + _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, + "ff", (float) MIDI_RT_CONTINUE, 0.0); + } + } + } + else if (this->wasPlaying) + { + _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, + "ff", (float) MIDI_RT_STOP, 0.0); + } + this->wasPlaying = timePos.playing; + + // sending clock ticks + if (timePos.playing && timePos.bbt.valid) + { + float samplesPerBeat = 60 * getSampleRate() / timePos.bbt.beatsPerMinute; + float samplesPerTick = samplesPerBeat / 24.0; + + /* get state */ + double nextClockTick = this->nextClockTick; + double sampleAtCycleStart = this->sampleAtCycleStart; + double sampleAtCycleEnd = sampleAtCycleStart + frames; + + if (nextClockTick >= 0 && sampleAtCycleStart >= 0 && sampleAtCycleEnd > sampleAtCycleStart) { + while (nextClockTick < sampleAtCycleEnd) { + double delayMs = 1000*(nextClockTick - sampleAtCycleStart)/getSampleRate(); + if (delayMs >= 0.0) { + _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, delayMs, + "ff", (float) MIDI_RT_CLOCK, 0.0); + } + nextClockTick += samplesPerTick; + } + } + + /* save variables for next cycle */ + this->sampleAtCycleStart = sampleAtCycleEnd; + this->nextClockTick = nextClockTick; + } +} diff --git a/hvcc/generators/c2dpf/templates/midiInput.cpp b/hvcc/generators/c2dpf/templates/midiInput.cpp index 0fe3ba4c..537333fc 100644 --- a/hvcc/generators/c2dpf/templates/midiInput.cpp +++ b/hvcc/generators/c2dpf/templates/midiInput.cpp @@ -3,67 +3,6 @@ void {{class_name}}::handleMidiInput(uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount) { - // Realtime events - const TimePosition& timePos(getTimePosition()); - bool reset = false; - - if (timePos.playing) - { - if (timePos.frame == 0) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_RESET); - reset = true; - } - - if (! this->wasPlaying) - { - if (timePos.frame == 0) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_START); - } - if (! reset) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_CONTINUE); - } - } - } - else if (this->wasPlaying) - { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, 0, - "ff", (float) MIDI_RT_STOP); - } - this->wasPlaying = timePos.playing; - - // sending clock ticks - if (timePos.playing && timePos.bbt.valid) - { - float samplesPerBeat = 60 * getSampleRate() / timePos.bbt.beatsPerMinute; - float samplesPerTick = samplesPerBeat / 24.0; - - /* get state */ - double nextClockTick = this->nextClockTick; - double sampleAtCycleStart = this->sampleAtCycleStart; - double sampleAtCycleEnd = sampleAtCycleStart + frames; - - if (nextClockTick >= 0 && sampleAtCycleStart >= 0 && sampleAtCycleEnd > sampleAtCycleStart) { - while (nextClockTick < sampleAtCycleEnd) { - double delayMs = 1000*(nextClockTick - sampleAtCycleStart)/getSampleRate(); - if (delayMs >= 0.0) { - _context->sendMessageToReceiverV(HV_HASH_MIDIREALTIMEIN, delayMs, - "ff", (float) MIDI_RT_CLOCK); - } - nextClockTick += samplesPerTick; - } - } - - /* save variables for next cycle */ - this->sampleAtCycleStart = sampleAtCycleEnd; - this->nextClockTick = nextClockTick; - } - // Midi events for (uint32_t i=0; i < midiEventCount; ++i) { diff --git a/hvcc/generators/c2js/c2js.py b/hvcc/generators/c2js/c2js.py index 75fdd943..5acabc39 100644 --- a/hvcc/generators/c2js/c2js.py +++ b/hvcc/generators/c2js/c2js.py @@ -1,5 +1,5 @@ # Copyright (C) 2014-2018 Enzien Audio, Ltd. -# Copyright (C) 2021-2023 Wasted Audio +# Copyright (C) 2021-2024 Wasted Audio # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,13 +20,17 @@ import jinja2 from shutil import which -from typing import Dict, Optional +from typing import Optional from hvcc.core.hv2ir.HeavyException import HeavyException from ..copyright import copyright_manager +from hvcc.interpreters.pd2hv.NotificationEnum import NotificationEnum +from hvcc.types.compiler import Generator, CompilerResp, CompilerNotif, CompilerMsg, ExternInfo +from hvcc.types.meta import Meta -class c2js: + +class c2js(Generator): """Compiles a directory of C source files into javascript. Requires the emscripten library to be installed - https://github.com/kripken/emscripten """ @@ -157,24 +161,24 @@ def compile( cls, c_src_dir: str, out_dir: str, - externs: Dict, + externs: ExternInfo, patch_name: Optional[str] = None, - patch_meta: Optional[Dict] = None, + patch_meta: Meta = Meta(), num_input_channels: int = 0, num_output_channels: int = 0, copyright: Optional[str] = None, verbose: Optional[bool] = False - ) -> Dict: + ) -> CompilerResp: tick = time.time() - parameter_list = externs["parameters"]["in"] - parameter_out_list = externs["parameters"]["out"] - event_list = externs["events"]["in"] - event_out_list = externs["events"]["out"] + parameter_list = externs.parameters.inParam + parameter_out_list = externs.parameters.outParam + event_list = externs.events.inEvent + event_out_list = externs.events.outEvent - midi_list = externs["midi"]["in"] - midi_out_list = externs["midi"]["out"] + midi_list = externs.midi.inMidi + midi_out_list = externs.midi.outMidi out_dir = os.path.join(out_dir, "js") patch_name = patch_name or "heavy" @@ -202,7 +206,7 @@ def compile( name=patch_name, copyright=copyright_js, externs=externs, - pool_sizes_kb=externs["memoryPoolSizesKb"])) + pool_sizes_kb=externs.memoryPoolSizesKb)) js_path = cls.run_emscripten(c_src_dir=c_src_dir, out_dir=out_dir, @@ -239,7 +243,7 @@ def compile( name=patch_name, copyright=copyright_js, externs=externs, - pool_sizes_kb=externs["memoryPoolSizesKb"])) + pool_sizes_kb=externs.memoryPoolSizesKb)) pre_js_path = os.path.join(out_dir, "hv_worklet_start.js") with open(pre_js_path, "w") as f: @@ -247,7 +251,7 @@ def compile( name=patch_name, copyright=copyright_js, externs=externs, - pool_sizes_kb=externs["memoryPoolSizesKb"])) + pool_sizes_kb=externs.memoryPoolSizesKb)) js_path = cls.run_emscripten(c_src_dir=c_src_dir, out_dir=out_dir, @@ -263,36 +267,27 @@ def compile( os.remove(post_js_path) os.remove(pre_js_path) - return { - "stage": "c2js", - "notifs": { - "has_error": False, - "exception": None, - "warnings": [], - "errors": [] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": js_out_file, - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2js", + in_dir=c_src_dir, + out_dir=out_dir, + out_file=js_out_file, + compile_time=time.time() - tick + ) except Exception as e: - return { - "stage": "c2js", - "notifs": { - "has_error": True, - "exception": e, - "warnings": [], - "errors": [{ - "enum": -1, - "message": str(e) - }] - }, - "in_dir": c_src_dir, - "in_file": "", - "out_dir": out_dir, - "out_file": "", - "compile_time": time.time() - tick - } + return CompilerResp( + stage="c2js", + notifs=CompilerNotif( + has_error=True, + exception=e, + warnings=[], + errors=[CompilerMsg( + enum=NotificationEnum.ERROR_EXCEPTION, + message=str(e) + )] + ), + in_dir=c_src_dir, + out_dir=out_dir, + compile_time=time.time() - tick + ) diff --git a/hvcc/generators/c2js/template/hv_worklet.js b/hvcc/generators/c2js/template/hv_worklet.js index c043f1a4..ab7d597b 100644 --- a/hvcc/generators/c2js/template/hv_worklet.js +++ b/hvcc/generators/c2js/template/hv_worklet.js @@ -108,13 +108,22 @@ class {{name}}_AudioLibWorklet extends AudioWorkletProcessor { var self = this; // typedef void (HvSendHook_t) (HeavyContextInterface *context, const char *sendName, hv_uint32_t sendHash, const HvMessage *msg); var sendHook = addFunction(function(context, sendName, sendHash, msg) { + // Filter out MIDI messages + const midiMessage = sendMidiOut(UTF8ToString(sendName), msg); + if (midiMessage.length > 0) { + self.port.postMessage({ + type: 'midiOut', + payload: midiMessage + }); + } else { // Converts sendhook callback to (sendName, float) message self.port.postMessage({ type: 'sendHook', payload: [UTF8ToString(sendName), _hv_msg_getFloat(msg, 0)] }); - }, - "viiii" + } + }, + "viiii" ); _hv_setSendHook(this.heavyContext, sendHook); } @@ -132,73 +141,7 @@ 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); - } - } + sendMidiIn(this.heavyContext, message); } sendStringToReceiver(name, message) { @@ -265,9 +208,145 @@ var tableHashes = { registerProcessor("{{name}}_AudioLibWorklet", {{name}}_AudioLibWorklet); +// midi_utils + +function sendMidiIn(hv_context, message) { + if (hv_context) { + 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(hv_context, HV_HASH_MIDIIN, 0, + message[i], + channel + ); + } + + // realtime events to [midirealtimein] + if (MIDI_REALTIME.includes(message[0])) { + _hv_sendMessageToReceiverFF(hv_context, HV_HASH_MIDIREALTIMEIN, 0, + message[0] + ); + } + + switch(command) { + case 0x80: // note off + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_NOTEIN, 0, + data1, + 0, + channel); + break; + case 0x90: // note on + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_NOTEIN, 0, + data1, + data2, + channel); + break; + case 0xA0: // polyphonic aftertouch + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_POLYTOUCHIN, 0, + data2, // pressure + data1, // note + channel); + break; + case 0xB0: // control change + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_CTLIN, 0, + data2, // value + data1, // cc number + channel); + break; + case 0xC0: // program change + _hv_sendMessageToReceiverFF(hv_context, HV_HASH_PGMIN, 0, + data1, + channel); + break; + case 0xD0: // aftertouch + _hv_sendMessageToReceiverFF(hv_context, 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(hv_context, HV_HASH_BENDIN, 0, + value, + channel); + break; + default: + // console.error('No handler for midi message: ', message); + } + } + } + +function sendMidiOut(sendName, msg) { + switch (sendName) { + case "__hv_noteout": + var note = _hv_msg_getFloat(msg, 0); + var velocity = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return [ + ((velocity > 0) ? 144 : 128) | channel, + note, + velocity + ] + case "__hv_ctlout": + var value = _hv_msg_getFloat(msg, 0); + var cc = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return [ + 176 | channel, + cc, + value + ] + case "__hv_pgmout": + var program = _hv_msg_getFloat(msg, 0); + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 192 | channel, + program + ] + case "__hv_touchout": + var pressure = _hv_msg_getFloat(msg, 0); + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 208 | channel, + pressure, + ] + case "__hv_polytouchout": + var value = _hv_msg_getFloat(msg, 0); + var note = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return[ + 160 | channel, + note, + value + ] + case "__hv_bendout": + var value = _hv_msg_getFloat(msg, 0); + let lsb = value & 0x7F; + let msb = (value >> 7) & 0x7F; + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 224 | channel, + lsb, + msb + ] + case "__hv_midiout": + let firstByte = _hv_msg_getFloat(msg, 0); + return (firstByte === 192 || firstByte === 208) ? + [_hv_msg_getFloat(msg, 0), _hv_msg_getFloat(msg, 1)] : + [_hv_msg_getFloat(msg, 0), _hv_msg_getFloat(msg, 1), _hv_msg_getFloat(msg, 2)]; + default: + console.warn(`Unhandled sendName: ${sendName}`); + return []; + } +} + /* - * MIDI Constants - */ +* MIDI Constants +*/ const HV_HASH_NOTEIN = 0x67E37CA3; const HV_HASH_CTLIN = 0x41BE0f9C; diff --git a/hvcc/generators/c2js/template/hv_worklet_start.js b/hvcc/generators/c2js/template/hv_worklet_start.js index e9cff20d..cb90a8ae 100644 --- a/hvcc/generators/c2js/template/hv_worklet_start.js +++ b/hvcc/generators/c2js/template/hv_worklet_start.js @@ -8,4 +8,4 @@ var self = { function importScripts(){ console.warn('importScripts should not be called in an AudioWorklet', arguments); -} \ No newline at end of file +} diff --git a/hvcc/generators/c2js/template/hv_wrapper.js b/hvcc/generators/c2js/template/hv_wrapper.js index 3b2f85e0..0c4fd981 100644 --- a/hvcc/generators/c2js/template/hv_wrapper.js +++ b/hvcc/generators/c2js/template/hv_wrapper.js @@ -22,6 +22,7 @@ var AudioLibLoader = function() { * @param options.sendHook (Function) callback that gets triggered for messages sent via @hv_param/@hv_event */ AudioLibLoader.prototype.init = function(options) { + // use provided web audio context or create a new one this.webAudioContext = options.webAudioContext || (new (window.AudioContext || window.webkitAudioContext || null)); @@ -43,6 +44,8 @@ AudioLibLoader.prototype.init = function(options) { options.printHook(event.data.payload); } else if (event.data.type === 'sendHook' && options.sendHook) { options.sendHook(event.data.payload[0], event.data.payload[1]); + } else if (event.data.type === 'midiOut' && options.sendHook) { + options.sendHook("midiOutMessage", event.data.payload); } else { console.log('Unhandled message from {{name}}_AudioLibWorklet:', event.data); } @@ -244,8 +247,13 @@ var tableHashes = { if (hook) { // typedef void (HvSendHook_t) (HeavyContextInterface *context, const char *sendName, hv_uint32_t sendHash, const HvMessage *msg); var sendHook = addFunction(function(context, sendName, sendHash, msg) { - // Converts sendhook callback to (sendName, float) message - hook(UTF8ToString(sendName), _hv_msg_getFloat(msg, 0)); + const midiMessage = sendMidiOut(UTF8ToString(sendName), msg); + if (midiMessage.length > 0) { + hook("midiOutMessage", midiMessage); + } else { + // Converts sendhook callback to (sendName, float) message + hook(UTF8ToString(sendName), _hv_msg_getFloat(msg, 0)); + } }, "viiii" ); @@ -260,73 +268,7 @@ 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); - } - } + sendMidiIn(this.heavyContext, message); } {{name}}_AudioLib.prototype.setFloatParameter = function(name, floatValue) { @@ -368,9 +310,146 @@ var tableHashes = { Module.{{name}}_AudioLib = {{name}}_AudioLib; + +// midi_utils + +function sendMidiIn(hv_context, message) { + if (hv_context) { + 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(hv_context, HV_HASH_MIDIIN, 0, + message[i], + channel + ); + } + + // realtime events to [midirealtimein] + if (MIDI_REALTIME.includes(message[0])) { + _hv_sendMessageToReceiverFF(hv_context, HV_HASH_MIDIREALTIMEIN, 0, + message[0] + ); + } + + switch(command) { + case 0x80: // note off + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_NOTEIN, 0, + data1, + 0, + channel); + break; + case 0x90: // note on + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_NOTEIN, 0, + data1, + data2, + channel); + break; + case 0xA0: // polyphonic aftertouch + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_POLYTOUCHIN, 0, + data2, // pressure + data1, // note + channel); + break; + case 0xB0: // control change + _hv_sendMessageToReceiverFFF(hv_context, HV_HASH_CTLIN, 0, + data2, // value + data1, // cc number + channel); + break; + case 0xC0: // program change + _hv_sendMessageToReceiverFF(hv_context, HV_HASH_PGMIN, 0, + data1, + channel); + break; + case 0xD0: // aftertouch + _hv_sendMessageToReceiverFF(hv_context, 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(hv_context, HV_HASH_BENDIN, 0, + value, + channel); + break; + default: + // console.error('No handler for midi message: ', message); + } + } + } + +function sendMidiOut(sendName, msg) { + switch (sendName) { + case "__hv_noteout": + var note = _hv_msg_getFloat(msg, 0); + var velocity = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return [ + ((velocity > 0) ? 144 : 128) | channel, + note, + velocity + ] + case "__hv_ctlout": + var value = _hv_msg_getFloat(msg, 0); + var cc = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return [ + 176 | channel, + cc, + value + ] + case "__hv_pgmout": + var program = _hv_msg_getFloat(msg, 0); + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 192 | channel, + program + ] + case "__hv_touchout": + var pressure = _hv_msg_getFloat(msg, 0); + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 208 | channel, + pressure, + ] + case "__hv_polytouchout": + var value = _hv_msg_getFloat(msg, 0); + var note = _hv_msg_getFloat(msg, 1); + var channel = _hv_msg_getFloat(msg, 2) % 16; // no pd midi ports + return[ + 160 | channel, + note, + value + ] + case "__hv_bendout": + var value = _hv_msg_getFloat(msg, 0); + let lsb = value & 0x7F; + let msb = (value >> 7) & 0x7F; + var channel = _hv_msg_getFloat(msg, 1) % 16; // no pd midi ports + return [ + 224 | channel, + lsb, + msb + ] + case "__hv_midiout": + let firstByte = _hv_msg_getFloat(msg, 0); + return (firstByte === 192 || firstByte === 208) ? + [_hv_msg_getFloat(msg, 0), _hv_msg_getFloat(msg, 1)] : + [_hv_msg_getFloat(msg, 0), _hv_msg_getFloat(msg, 1), _hv_msg_getFloat(msg, 2)]; + default: + console.warn(`Unhandled sendName: ${sendName}`); + return []; + } +} + /* - * MIDI Constants - */ +* MIDI Constants +*/ const HV_HASH_NOTEIN = 0x67E37CA3; const HV_HASH_CTLIN = 0x41BE0f9C; diff --git a/hvcc/generators/c2js/template/index.html b/hvcc/generators/c2js/template/index.html index da6cbf82..aadcfc47 100644 --- a/hvcc/generators/c2js/template/index.html +++ b/hvcc/generators/c2js/template/index.html @@ -10,6 +10,7 @@ {% endfor -%}