diff --git a/csoundengine/config.py b/csoundengine/config.py index 9b0c3b5..23662f8 100644 --- a/csoundengine/config.py +++ b/csoundengine/config.py @@ -157,6 +157,10 @@ def _validateFigsize(cfg: dict, key: str, val) -> bool: _('datafile_format', 'gen23', choices={'gen23', 'wav'}, doc='Format used when saving a table as a datafile') +_('tabargs_method', 'slice', + choices=('slice', 'table'), + doc='Method use to define named table arguments. "slice" uses a big table and allocates' \ + 'a slice for each synth. "table" allocates an entire table for each event') # Plotting _('spectrogram_colormap', 'inferno', choices={'viridis', 'plasma', 'inferno', 'magma', 'cividis'}) diff --git a/csoundengine/csoundlib.py b/csoundengine/csoundlib.py index c4822d5..07b03da 100755 --- a/csoundengine/csoundlib.py +++ b/csoundengine/csoundlib.py @@ -524,7 +524,7 @@ def _getVersionViaApi() -> tuple[int, int, int]: return info['versionTriplet'] -def getVersion(useApi=True) -> tuple[int, int, int]: +def getVersion(useApi=True) -> tuple[int, int, int | str]: """ Returns the csound version as tuple (major, minor, patch) @@ -534,7 +534,7 @@ def getVersion(useApi=True) -> tuple[int, int, int]: differ Returns: - the versions as a tuple (major:int, minor:int, patch:int) + the versions as a tuple (major:int, minor:int, patch:int|str) Raises RuntimeError if csound is not present or its version can't be parsed @@ -550,26 +550,22 @@ def getVersion(useApi=True) -> tuple[int, int, int]: proc.wait() if proc.stderr is None: return (0, 0, 0) - lines = proc.stderr.readlines() - if not lines: + output = proc.stderr.read() + if not output: raise RuntimeError("Could not read csounds output") - for bline in lines: - if bline.startswith(b"Csound version"): - line = bline.decode('utf8') - matches = _re.findall(r"(\d+\.\d+(\.\d+)?)", line) - if matches: - version = matches[0] - if isinstance(version, tuple): - version = version[0] - points = version.count(".") - if points == 1: - major, minor = list(map(int, version.split("."))) - patch = 0 - else: - major, minor, patch = list(map(int, version.split(".")[:3])) - return (major, minor, patch) + lines = output.decode('utf8').splitlines() + for line in lines: + if match := _re.search(r"Csound\s+version\s+(\d+)\.(\d+)(\.\w+)?", line): + major = int(match.group(1)) + minor = int(match.group(2)) + patch = match.group(3) + if patch is None: + patch = 0 + elif patch.isdigit(): + patch = int(patch) + return (major, minor, patch) else: - raise RuntimeError("Did not found a csound version") + raise RuntimeError(f"Did not find a csound version, csound output: '{output}'") def csoundSubproc(args: list[str], piped=True, wait=False) -> _subprocess.Popen: diff --git a/csoundengine/dependencies.py b/csoundengine/dependencies.py index 3106995..8551e4a 100644 --- a/csoundengine/dependencies.py +++ b/csoundengine/dependencies.py @@ -287,13 +287,18 @@ def _checkDependencies(fix=False, updateState=True, quiet=False) -> Optional[str if not csoundInstalled(): return "csound not installed. See https://csound.com/download.html" - version = csoundlib.getVersion() + version = csoundlib.getVersion(useApi=True) if version < (6, 16, 0): return f"Csound version ({version}) is too old, should be >= 6.16" if version[0] >= 7: print(f"WARNING: Csound 7 is not fully supported. Proceed at yout own risk") + binversion = csoundlib.getVersion(useApi=False) + if version[:2] != binversion[:2]: + print(f"WARNING: the csound library found reported a version {version}, different" + f" from the version reported by the csound binary {binversion}") + if not pluginsInstalled(cached=False): if fix: if not quiet: diff --git a/csoundengine/engine.py b/csoundengine/engine.py index 85d152e..32e9bce 100755 --- a/csoundengine/engine.py +++ b/csoundengine/engine.py @@ -1297,6 +1297,8 @@ def getTableData(self, idx: int, flat=False) -> None | np.ndarray: arr: np.ndarray | None = self._tableCache.get(idx) if arr is None: arr = self.csound.table(idx) + if arr is None: + return None if not flat: tabinfo = self.tableInfo(idx) if tabinfo.numChannels > 1: @@ -1840,12 +1842,12 @@ def reserveInstrRange(self, name: str, mininstrnum: int, maxinstrnum: int) -> No """ self._reservedInstrnumRanges.append((name, mininstrnum, maxinstrnum)) - def makeEmptyTable(self, size, numchannels=1, sr=0, instrnum=-1) -> int: + def makeEmptyTable(self, size, numchannels=1, sr=0, instrnum=-1, isaudio=True) -> int: """ Create an empty table, returns the index of the created table Example - ======= + ~~~~~~~ Use a table as an array of buses diff --git a/csoundengine/instr.py b/csoundengine/instr.py index 8669f54..7cb3d05 100644 --- a/csoundengine/instr.py +++ b/csoundengine/instr.py @@ -228,6 +228,12 @@ def __init__(self, assert isinstance(name, str) + if tabargs: + if args: + if not args.keys().isdisjoint(tabargs.keys()): + duplicates = set(args.keys()).intersection(tabargs.keys()) + raise ValueError(f"Duplicate names arguments: {duplicates}") + if errmsg := _checkInstr(body): raise CsoundError(errmsg) @@ -949,6 +955,9 @@ def parseInlineArgs(body: str | list[str] """ Parse an instr body with a possible args declaration (see below). + An exception is raised if there are inline args and also direct + access to args in the form of p4, p5, etc. + Args: body: the body of the instrument as a string or as a list of lines @@ -989,9 +998,17 @@ def parseInlineArgs(body: str | list[str] """ lines = body if isinstance(body, list) else body.splitlines() delimiters, linenum = _detectInlineArgs(lines) + if not delimiters: return None + assert linenum is not None + + # check direct usage of pargs + for i, line in enumerate(lines): + if re.search(r"\b(p[4-9]|p\d{2,})\b", line): + raise ValueError(f"Cannot access pfields directly when using inline args, line " + f"{i}, '{line}'") args = {} line2 = lines[linenum].strip() parts = line2[1:-1].split(",") @@ -1005,6 +1022,26 @@ def parseInlineArgs(body: str | list[str] return InlineArgs(delimiters, args=args, body=bodyWithoutArgs, linenum=linenum) +def _bigtableGenerateCode(tabargs: dict) -> str: + lines = [] + lines.append(fr''' + ; --- start generated table code + islicestart_ = p4 + itabnum_ chnget ".tabargsTabnum" + if itabnum_ == 0 then + initerror sprintf("Params table (%d) does not exist (p1: %f)", itabnum_, p1) + goto __exit + endif + ''') + idx = 0 + for key, value in tabargs.items(): + lines.append(f" {key} tab islicestart_ + {idx}, itabnum_") + idx += 1 + lines.append(" ; --- end generated table code\n") + out = textlib.stripLines(textlib.joinPreservingIndentation(lines)) + return out + + def _tabargsGenerateCode(tabargs: dict, freetable=True) -> str: lines: list[str] = [] idx = 0 diff --git a/csoundengine/internalTools.py b/csoundengine/internalTools.py index fc0245e..313f797 100644 --- a/csoundengine/internalTools.py +++ b/csoundengine/internalTools.py @@ -139,6 +139,21 @@ def determineNumbuffers(backend: str, buffersize: int) -> int: return numbuffers +def distributeNamedArgs(namedargs: dict[str, float], + pargKeys: Sequence[str] | KeysView, + tabargsKeys: Sequence[str] | KeysView + ) -> tuple[dict[str, float], dict[str, float]]: + pargs = {} + tabargs = {} + for k, v in namedargs: + if k in pargKeys: + pargs[k] = v + else: + assert k in tabargsKeys + tabargs[k] = v + return pargs, tabargs + + def instrResolveArgs(instr: Instr, p4: int, pargs: list[float] | dict[str, float] | None = None, diff --git a/csoundengine/paramtable.py b/csoundengine/paramtable.py index 2171137..d779740 100644 --- a/csoundengine/paramtable.py +++ b/csoundengine/paramtable.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: from .engine import Engine + class ParamTable: """ A ParamTable is a csound table used as a multivalue communication channel diff --git a/csoundengine/session.py b/csoundengine/session.py index 7464a15..e6dd7d2 100644 --- a/csoundengine/session.py +++ b/csoundengine/session.py @@ -187,6 +187,7 @@ from __future__ import annotations import os from dataclasses import dataclass +from collections import deque import numpy as np import emlib.dialogs @@ -458,10 +459,18 @@ def __init__(self, name: str, numpriorities=10) -> None: self._schedCallback: Union[Callable, None] = None self._rendering = False - if config['define_builtin_instrs']: - self._defBuiltinInstrs() + self._tabargsSliceSize = 10 + self._tabargsNumSlices = 1000 + self._tabargsTabnum = engine.makeEmptyTable(size=self._tabargsSliceSize * self._tabargsNumSlices) + self._tabargsArray = engine.getTableData(self._tabargsTabnum) + # We don't use slice 0. We use a deque as pool instead of a list, this helps + # debugging + self._tabargsSlicePool: deque[int] = deque(range(1, self._tabargsNumSlices)) + engine.setChannel(".tabargsTable", self._tabargsTabnum) engine.registerOutvalueCallback("__dealloc__", self._deallocCallback) + if config['define_builtin_instrs']: + self._defBuiltinInstrs() mininstr, maxinstr = self._reservedInstrRange() engine.reserveInstrRange('session', mininstr, maxinstr) engine._session = self @@ -503,6 +512,12 @@ def _deallocSynthResources(self, synthid: int | float, delay=0.) -> None: if not synth.instr.instrFreesParamTable: self.engine.freeTable(synth._table.tableIndex, delay=delay) self.engine._releaseTableNumber(synth._table.tableIndex) + elif synth.args[0]: + # The synth has a tableslice + assert config['tabargs_method'] == 'slice' + slicenum = int(synth.args[0]) + self._tabargsReleaseSlice(slicenum) + if callback := self._whenfinished.pop(synthid, None): callback(synthid) @@ -630,7 +645,7 @@ def defInstr(self, Example - ======= + ~~~~~~~ >>> session = Engine().session() # An Instr with named pfields @@ -1004,6 +1019,18 @@ def _exit(r: Renderer, outfile=outfile, schedCallback=schedCallback): renderer._registerExitCallback(_exit) return renderer + def _tabargsAssignSlice(self) -> int: + try: + return self._tabargsSlicePool.pop() + except IndexError: + raise IndexError("Tried to assign a slice for dynamic controls but the pool" + " is empty.") + + def _tabargsReleaseSlice(self, slicenum: int) -> None: + assert 1 <= slicenum < self._tabargsNumSlices + assert slicenum not in self._tabargsSlicePool # Remove this after testing + self._tabargsSlicePool.appendleft(slicenum) + def sched(self, instrname: str, delay=0., @@ -1088,15 +1115,7 @@ def sched(self, instr = self.getInstr(instrname) if instr is None: raise ValueError(f"Instrument {instrname} not defined") - table: ParamTable | None - if instr._tableDefaultValues is not None: - # the instruments have an associated table - tableidx = self._makeInstrTable(instr, overrides=tabargs, wait=True) - table = ParamTable(engine=self.engine, idx=tableidx, - mapping=instr._tableNameToIndex) - else: - tableidx = 0 - table = None + # tableidx is always p4 if pkws: params = instr.namedParams(includeRealNames=True) @@ -1104,9 +1123,42 @@ def sched(self, if k not in params: raise KeyError(f"arg '{k}' not known for instr '{instr.name}'. " f"Possible args: {params}") + kwargs, kwtabargs = _tools.distributeNamedArgs(pkws, + pargKeys=instr.pargsNameToIndex.keys(), + tabargsKeys=instr.tabargs.keys()) + if args: + args.update(kwargs) + else: + args = kwargs + + if tabargs: + tabargs.update(kwtabargs) + else: + tabargs = kwtabargs - p4args = _tools.instrResolveArgs(instr, p4=tableidx, pargs=args, pkws=pkws) rinstr, needssync = self.prepareSched(instrname, priority, block=True) + + table: ParamTable | None = None + + if instr._tableDefaultValues is not None: + # the instruments have an associated table + if config['tabargs_method'] == 'table': + tableidx = self._makeInstrTable(instr, overrides=tabargs, wait=True) + table = ParamTable(engine=self.engine, idx=tableidx, + mapping=instr._tableNameToIndex) + p4 = tableidx + else: + slicenum = self._tabargsAssignSlice() + # table = ParamTable(idx=slicenum, mapping=instr._tableNameToIndex) + p4 = slicenum * self._tabargsSliceSize + values = instr.overrideTable(tabargs) + idx0 = slicenum * self._tabargsSliceSize + self._tabargsArray[idx0:idx0+len(values)] = values + + else: + p4 = 0 + + p4args = _tools.instrResolveArgs(instr, p4=p4, pargs=args, pkws=pkws) if needssync and syncifneeded: self.engine.sync() synthid = self.engine.sched(rinstr.instrnum, delay=delay, dur=dur, args=p4args, diff --git a/csoundengine/synth.py b/csoundengine/synth.py index eca3594..6c8fb2e 100644 --- a/csoundengine/synth.py +++ b/csoundengine/synth.py @@ -162,7 +162,7 @@ class Synth(AbstrSynth): synths[1].automate('ktransp', [0, 0, 10, -1]) """ - __slots__ = ('instr', '_table', 'group', '_playing', 'priority', 'p1', 'args') + __slots__ = ('instr', '_table', 'group', '_playing', 'priority', 'p1', 'args', '_tableslice') def __init__(self, engine: Engine, @@ -174,6 +174,7 @@ def __init__(self, autostop=False, table: ParamTable = None, priority: int = 1, + tableslice: int = -1 ) -> None: AbstrSynth.__init__(self, start=start, dur=dur, engine=engine, autostop=autostop) @@ -196,6 +197,9 @@ def __init__(self, self.args = args """The args used to schedule this synth""" + self._tableslice = tableslice + """Hold the namedargs slice""" + self._playing: bool = True def getInstr(self) -> Instr: diff --git a/notebooks/test-paramtable.ipynb b/notebooks/test-paramtable.ipynb index ab74915..e0a699e 100644 --- a/notebooks/test-paramtable.ipynb +++ b/notebooks/test-paramtable.ipynb @@ -10,6 +10,27 @@ "from csoundengine import *" ] }, + { + "cell_type": "code", + "execution_count": 3, + "id": "96a5c2d5-5743-447f-a31e-6ba4b524c142", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'slice'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "config['tabargs_method']" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -35,7 +56,102 @@ } ], "source": [ - "s = Engine().session()" + "e = Engine()\n", + "s = e.session()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "82bc8016-e86c-4c5d-8be6-18fc0a2c697d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s._tabargsArray is None" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "47eaaa8c-caa7-4329-a6ed-c4ef6dbdb40d", + "metadata": {}, + "outputs": [], + "source": [ + "e.getTableData(s._tabargsTabnum)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9f4f11ee-8206-407a-857a-e764e73d3b5d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "302" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "e.makeEmptyTable(100)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "81e7a969-dfa6-4312-9fe7-6f005b6f2e43", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0., 1., 2., 3.])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "e.getTableData(107)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c758d982-daef-411c-8921-000f5a235170", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0., 1., 2., 3.])" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "e.csound.table(107)" ] }, { @@ -64,7 +180,7 @@ " kamp tab 1, iparams_\n", " ftfree iparams_, 1\n", " ; --- end generated table code\n", - " outch oscili:a(kamp, mtof(kpitch)\n", + " outch 1, oscili:a(kamp, mtof(kpitch))\n", " __exit:\n", "\n", "" @@ -81,16 +197,74 @@ "source": [ "s.defInstr(\"foo\", r\"\"\"\n", "{kpitch=60, kamp=0.1}\n", - "outch oscili:a(kamp, mtof(kpitch)\n", + "outch 1, oscili:a(kamp, mtof(kpitch))\n", "\"\"\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "d4e65796-6b76-4013-808b-041192f2cc76", "metadata": {}, "outputs": [], + "source": [ + "with s.engine.lockedClock():\n", + " for p in range(22, 100):\n", + " s.sched(\"foo\", dur=2, tabargs={'kpitch': p, 'kamp': 0.01})" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ae6a0c58-e634-4488-8e8f-ada8b6c4475f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Instr foo2
\n", + "
\n", + "
kpitch = p5\n",
+       "kamp   = p6\n",
+       "\n",
+       "outch 1, oscili:a(kamp, mtof(kpitch))\n",
+       "
\n", + "
" + ], + "text/plain": [ + "Instr(foo2, kpitch:5=60, kamp:6=0.1)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s.defInstr(\"foo2\", r\"\"\"\n", + "|kpitch=60, kamp=0.1|\n", + "outch 1, oscili:a(kamp, mtof(kpitch))\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "1859e684-a1c2-48f1-9a62-cdcfba4af8e7", + "metadata": {}, + "outputs": [], + "source": [ + "with s.engine.lockedClock():\n", + " for p in range(22, 100):\n", + " s.sched(\"foo2\", dur=2, kpitch=p, kamp=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe6a4190-8dd7-483e-b1dc-af147818a9c2", + "metadata": {}, + "outputs": [], "source": [] } ], diff --git a/setup.py b/setup.py index 624d2f0..c5f4459 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def package_files(directory): "xxhash", "docstring-parser", - "ctcsound7>=0.4.0", + "ctcsound7>=0.4.5", "sndfileio>=1.9.0", "emlib>=1.9.1", "configdict>=2.6.0",