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",
+ "
kpitch = p5\n",
+ "kamp = p6\n",
+ "\n",
+ "outch 1, oscili:a(kamp, mtof(kpitch))\n",
+ "