Skip to content

Commit 7a614e9

Browse files
authored
Feature/pdnam (#365)
* migrate to pathlib * type hints; vendor C-extern * choose model implementation based on number of weights; fail early * use upstream MicroNAM directly * missing pdnam~ impl * start on ADR * add pdnam~ to supported objects * use custom exception
1 parent 8e876c0 commit 7a614e9

20 files changed

Lines changed: 826 additions & 6 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "tests/src/tinywav"]
55
path = tests/src/tinywav
66
url = https://github.com/mhroth/tinywav.git
7+
[submodule "hvcc/generators/ir2c/static/MicroNam"]
8+
path = hvcc/generators/ir2c/static/MicroNam
9+
url = https://github.com/jaffco/MicroNAM.git

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features:
1010
* Add `flash_time` to pd2gui Bang parser
1111
* Only parse GUI with `--gui` flag
1212
* Port objects from cyclone
13+
* Port `pdnam~` external using `MicroNAM`
1314

1415
Bugfixes:
1516

docs/adr/ADR-005-PDNAM-External.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# ADR-005: PDNAM External
2+
3+
Date: 2026-04-20
4+
5+
PR: https://github.com/Wasted-Audio/hvcc/pull/365
6+
7+
## Context
8+
9+
Recently someone posted a very small NAM implementation, called [MicroNAM](https://github.com/jaffco/MicroNAM), that can be embedded and run on STM32 via ie. the Daisy platform. Immediately the thought came up that this would be very nice to integrate into Heavy.
10+
11+
Because until now there was no basic NAM loader for PD first an external, based on NeuralAudio library, called [PDNAM](https://github.com/wasted-Audio/pdnam) was created. It takes only a single argument to a NAM model json file. Due to its simplicity a port to Heavy should be relatively easy.
12+
13+
### Capabilities
14+
15+
MicroNAM supports loading 4 WaveNet model types:
16+
17+
- Nano
18+
- Feather
19+
- Lite
20+
- Standard
21+
22+
We should be able to load all of these in our implementation as well.
23+
24+
Included in MicroNAM is a python script that converts a WaveNet model file to a C header. We should be able to call this code, or a derivative, from HVCC.
25+
26+
Since MicroNAM is written in C++ we will need to have a C extern wrapper so it can be loaded by Heavy C code.
27+
28+
## Decision
29+
30+
A new internal object `__nam~f` will be defined that defers the implementation of `[pdnam~]` using an abstraction. During the IR stage we will then parse the first argument - path to the NAM file - to detect which model type is being used. At this stage we will then overload the specific object type, depending on the model. All types are defined in the core `heavy.ir.json`.
31+
32+
Subsequently the `ir2c` stage will then handle these new object types returning the specific `c_struct`, initialization, free, and process methods to use. The implementations for these are defined in `HvSignalNam.h` and `HvSignalNam.c`. During initialization the size of the model weights is returned to include in the Heavy memory accounting logic.
33+
34+
Because MicroNAM is written in C++ we need to create `MicroNAM_C.h/cpp` C externs that handle all the initialization, processing and freeing in our Heavy implementation. MicroNAM is added as a submodule as part of the `static/` files. The C externs and MicroNAM code will be included in the output when the `SignalNam` objects are used.
35+
36+
In order to include the model weights as a header we will extend `HeavyObject` with a new method `get_C_gen_header_code` that is currently only used by our `SignalNam` class. Here we can then convert the NAM file to a newly generated header. The `ir2c` stage will then collect any such generated headers and write them to the output directory.
37+
38+
For inferring the model type and creating the model weights name and header file we will modify and include the original MicroNAM python script and use it as a local `SignalNamHeader` module, for use in the various mentioned processing stages.
39+
40+
## MVP Definition
41+
42+
The user should be able to create a patch using `[pdnam~ <path_to_nam_file>]` and have it converted to C and run during processing. The resulting output should be very comparable to the `pdnam~` runtime.
43+
44+
## Future Improvements
45+
46+
We can possibly load NAM models more dynamically by parsing the JSON and updating the model weights. This would be limited to a specific model type and adds other complexity, so is currently unwanted.
47+
48+
We will stick to a pure static implementation for the time being.
49+
50+
Right now SIMD implementations are missing, these could possibly be added in the future.

docs/reference/objects/supported.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ Objects ported from [cyclone](https://github.com/porres/pd-cyclone) library.
229229
| tanh~ | |
230230
| tanx~ | |
231231

232+
## Other externals
233+
234+
Objects ported from other PD externals
235+
236+
| object | limitations |
237+
| --- | --- |
238+
| pdnam~ | only supports WaveNet models Nano, Feather, Lite, and Standard |
239+
232240
## Supported Abstractions
233241

234242
These are commonly used - or built in - abstractions that consist of compatible vanilla objects.

hvcc/core/hv2ir/BufferPool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def retain_buffer(self, b: List, count: int = 1) -> int:
7575
return k + count # return the new retain count
7676
raise HeavyException(f"{b} not found in BufferPool!")
7777

78-
def release_buffer(self, b: List, count: int = 1) -> int:
78+
def release_buffer(self, b: tuple[str, int], count: int = 1) -> int:
7979
""" Reduces the retain count of the buffer. Returns the new count.
8080
"""
8181
# adc~, ZERO_BUFFER, send~ buffers are special. They can not be released.

hvcc/core/hv2ir/HIrNam.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (C) 2026 Wasted Audio
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
from typing import Dict, Optional
17+
from pathlib import Path
18+
19+
from .HeavyIrObject import HeavyIrObject
20+
from .HeavyGraph import HeavyGraph
21+
22+
from hvcc.generators.ir2c.SignalNamHeader import ModelNet, load_nam_file
23+
24+
25+
class HIrNam(HeavyIrObject):
26+
""" __nam~f
27+
"""
28+
29+
def __init__(
30+
self,
31+
obj_type: str,
32+
args: Optional[Dict] = None,
33+
graph: Optional[HeavyGraph] = None,
34+
annotations: Optional[Dict] = None
35+
) -> None:
36+
assert obj_type == "__nam~f"
37+
38+
# load the nam file to retrieve the model type
39+
assert args is not None
40+
model_net, _, _ = load_nam_file(Path(args["nam"]))
41+
42+
# overload the object type based on the model
43+
if model_net == ModelNet.Nano:
44+
obj_type = "__nam_nano~f"
45+
elif model_net == ModelNet.Feather:
46+
obj_type = "__nam_feather~f"
47+
elif model_net == ModelNet.Lite:
48+
obj_type = "__nam_lite~f"
49+
elif model_net == ModelNet.Standard:
50+
obj_type = "__nam_standard~f"
51+
52+
super().__init__(obj_type, args=args, graph=graph, annotations=annotations)

hvcc/core/hv2ir/HeavyIrObject.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def __init__(
6868

6969
# the list of signal buffers at the inlets and outlets
7070
# these are filled in by HeavyGraph.assign_signal_buffers()
71-
self.inlet_buffers = [("zero", 0)] * self.num_inlets
72-
self.outlet_buffers = [("zero", 0)] * self.num_outlets
71+
self.inlet_buffers: list[tuple[str, int]] = [("zero", 0)] * self.num_inlets
72+
self.outlet_buffers: list[tuple[str, int]] = [("zero", 0)] * self.num_outlets
7373

7474
# True if this object has already been ordered in the signal chain
7575
self.__is_ordered = False
@@ -152,7 +152,7 @@ def assign_signal_buffers(self, buffer_pool: Optional[BufferPool]) -> None:
152152
if len(cc) == 0:
153153
continue
154154
if len(cc) == 1:
155-
c = cc[0] # get the connection
155+
c: Connection = cc[0] # get the connection
156156

157157
# get the buffer at the outlet of the connected object
158158
buf = c.from_object.outlet_buffers[c.outlet_index]

hvcc/core/hv2ir/HeavyParser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .HIrExpr import HIrExpr
2525
from .HIrInlet import HIrInlet
2626
from .HIrLorenz import HIrLorenz
27+
from .HIrNam import HIrNam
2728
from .HIrOutlet import HIrOutlet
2829
from .HIrPack import HIrPack
2930
from .HIrSwitchcase import HIrSwitchcase
@@ -293,6 +294,7 @@ def reduce(self) -> Tuple[Set, List]:
293294
"system": HLangSystem,
294295
"phasor": HLangPhasor,
295296
"line": HLangLine,
297+
"__nam~f": HIrNam,
296298
"random": HLangRandom,
297299
"delay": HLangDelay,
298300
"table": HLangTable,

hvcc/core/json/heavy.ir.json

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,111 @@
19821982
"required": false
19831983
}]
19841984
},
1985+
"__nam~f": {
1986+
"inlets": [
1987+
"~f>"
1988+
],
1989+
"ir": {
1990+
"control": false,
1991+
"signal": true,
1992+
"init": true
1993+
},
1994+
"outlets": [
1995+
"~f>"
1996+
],
1997+
"args": [{
1998+
"name": "nam",
1999+
"value_type": "string",
2000+
"description": "path to NAM file",
2001+
"default": "",
2002+
"required": true
2003+
}],
2004+
"perf": {}
2005+
},
2006+
"__nam_nano~f": {
2007+
"inlets": [
2008+
"~f>"
2009+
],
2010+
"ir": {
2011+
"control": false,
2012+
"signal": true,
2013+
"init": true
2014+
},
2015+
"outlets": [
2016+
"~f>"
2017+
],
2018+
"args": [{
2019+
"name": "nam",
2020+
"value_type": "string",
2021+
"description": "path to NAM file",
2022+
"default": "",
2023+
"required": true
2024+
}],
2025+
"perf": {}
2026+
},
2027+
"__nam_feather~f": {
2028+
"inlets": [
2029+
"~f>"
2030+
],
2031+
"ir": {
2032+
"control": false,
2033+
"signal": true,
2034+
"init": true
2035+
},
2036+
"outlets": [
2037+
"~f>"
2038+
],
2039+
"args": [{
2040+
"name": "nam",
2041+
"value_type": "string",
2042+
"description": "path to NAM file",
2043+
"default": "",
2044+
"required": true
2045+
}],
2046+
"perf": {}
2047+
},
2048+
"__nam_lite~f": {
2049+
"inlets": [
2050+
"~f>"
2051+
],
2052+
"ir": {
2053+
"control": false,
2054+
"signal": true,
2055+
"init": true
2056+
},
2057+
"outlets": [
2058+
"~f>"
2059+
],
2060+
"args": [{
2061+
"name": "nam",
2062+
"value_type": "string",
2063+
"description": "path to NAM file",
2064+
"default": "",
2065+
"required": true
2066+
}],
2067+
"perf": {}
2068+
},
2069+
"__nam_standard~f": {
2070+
"inlets": [
2071+
"~f>"
2072+
],
2073+
"ir": {
2074+
"control": false,
2075+
"signal": true,
2076+
"init": true
2077+
},
2078+
"outlets": [
2079+
"~f>"
2080+
],
2081+
"args": [{
2082+
"name": "nam",
2083+
"value_type": "string",
2084+
"description": "path to NAM file",
2085+
"default": "",
2086+
"required": true
2087+
}],
2088+
"perf": {}
2089+
},
19852090
"__neq": {
19862091
"inlets": [
19872092
"-->",

hvcc/generators/ir2c/HeavyObject.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
from struct import unpack, pack
18-
from typing import Callable, Dict, List, Union
18+
from typing import Callable, Dict, List, Union, Optional
1919

2020
from hvcc.types.IR import IROnMessage, IRSignalList, IRBuffer, IRObjectdict
2121

@@ -123,6 +123,10 @@ def get_C_class_header_code(cls, obj_type: str, args: Dict) -> List[str]:
123123
def get_C_class_impl_code(cls, obj_type: str, args: Dict) -> List[str]:
124124
return []
125125

126+
@classmethod
127+
def get_C_gen_header_code(cls, obj_type: str, obj_id: str, args: Dict) -> Optional[tuple[str, str]]:
128+
return None
129+
126130
@classmethod
127131
def get_C_process(cls, process_dict: IRSignalList, obj_type: str, obj_id: str, args: Dict) -> List[str]:
128132
raise NotImplementedError("method get_C_process not implemented")

0 commit comments

Comments
 (0)