Merge branch 'main' into werkstatt

This commit is contained in:
André Michelle
2026-03-12 17:31:10 +01:00
76 changed files with 1094 additions and 666 deletions
+174 -174
View File
@@ -3097,9 +3097,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.3.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
"version": "25.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
"integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -8329,9 +8329,9 @@
"license": "MIT"
},
"node_modules/mediabunny": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.38.1.tgz",
"integrity": "sha512-iqZjL21WG32qpdlONAyBBnmhLo4EvF+v4wnfe1rXEKT/7ogiYOZnQGL8TZi7QIFV71DSg7a64egFWUgf3oeSYA==",
"version": "1.39.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.1.tgz",
"integrity": "sha512-gaQKDWtfLhgSN9tRj37lgznHf9MOvey7sbT6GdQA7mWl1kaE5R+P1lfl9CDsgtzongx12IG9X/y2vgZmTHzGDg==",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -10017,9 +10017,9 @@
}
},
"node_modules/preact": {
"version": "10.28.4",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
"version": "10.29.0",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz",
"integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -10811,14 +10811,14 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.97.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
"version": "1.98.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"immutable": "^5.1.5",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
@@ -11790,27 +11790,27 @@
}
},
"node_modules/turbo": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.14.tgz",
"integrity": "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.16.tgz",
"integrity": "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA==",
"dev": true,
"license": "MIT",
"bin": {
"turbo": "bin/turbo"
},
"optionalDependencies": {
"turbo-darwin-64": "2.8.14",
"turbo-darwin-arm64": "2.8.14",
"turbo-linux-64": "2.8.14",
"turbo-linux-arm64": "2.8.14",
"turbo-windows-64": "2.8.14",
"turbo-windows-arm64": "2.8.14"
"turbo-darwin-64": "2.8.16",
"turbo-darwin-arm64": "2.8.16",
"turbo-linux-64": "2.8.16",
"turbo-linux-arm64": "2.8.16",
"turbo-windows-64": "2.8.16",
"turbo-windows-arm64": "2.8.16"
}
},
"node_modules/turbo-darwin-64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.14.tgz",
"integrity": "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.16.tgz",
"integrity": "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg==",
"cpu": [
"x64"
],
@@ -11822,9 +11822,9 @@
]
},
"node_modules/turbo-darwin-arm64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.14.tgz",
"integrity": "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.16.tgz",
"integrity": "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow==",
"cpu": [
"arm64"
],
@@ -11836,9 +11836,9 @@
]
},
"node_modules/turbo-linux-64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.14.tgz",
"integrity": "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.16.tgz",
"integrity": "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w==",
"cpu": [
"x64"
],
@@ -11850,9 +11850,9 @@
]
},
"node_modules/turbo-linux-arm64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.14.tgz",
"integrity": "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.16.tgz",
"integrity": "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ==",
"cpu": [
"arm64"
],
@@ -11864,9 +11864,9 @@
]
},
"node_modules/turbo-windows-64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.14.tgz",
"integrity": "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.16.tgz",
"integrity": "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw==",
"cpu": [
"x64"
],
@@ -11878,9 +11878,9 @@
]
},
"node_modules/turbo-windows-arm64": {
"version": "2.8.14",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.14.tgz",
"integrity": "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how==",
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.16.tgz",
"integrity": "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw==",
"cpu": [
"arm64"
],
@@ -12956,19 +12956,19 @@
},
"packages/app/lab": {
"name": "@opendaw/lab",
"version": "0.0.121",
"version": "0.0.126",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -12999,14 +12999,14 @@
},
"packages/app/nam-test": {
"name": "@opendaw/nam-test",
"version": "0.0.8",
"version": "0.0.9",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-dom": "^0.0.68",
"@opendaw/lib-dsp": "^0.0.66",
"@opendaw/lib-jsx": "^0.0.68",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/nam-wasm": "^1.0.0"
},
"devDependencies": {
@@ -13095,21 +13095,21 @@
},
"packages/app/studio": {
"name": "@opendaw/app-studio",
"version": "0.0.121",
"version": "0.0.126",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-midi": "^0.0.58",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/studio-scripting": "^0.0.50",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-midi": "^0.0.59",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68",
"@opendaw/studio-scripting": "^0.0.52",
"@types/d3-force": "^3.0.10",
"d3-force": "^3.0.0",
"dropbox": "^10.34.0",
@@ -13159,11 +13159,11 @@
},
"packages/lib/box": {
"name": "@opendaw/lib-box",
"version": "0.0.77",
"version": "0.0.78",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -13172,13 +13172,13 @@
},
"packages/lib/box-forge": {
"name": "@opendaw/lib-box-forge",
"version": "0.0.79",
"version": "0.0.80",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"ts-morph": "^25.0.1",
"tsx": "^4.20.3"
},
@@ -13189,12 +13189,12 @@
},
"packages/lib/dawproject": {
"name": "@opendaw/lib-dawproject",
"version": "0.0.62",
"version": "0.0.63",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-xml": "^0.0.57",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-xml": "^0.0.58",
"jszip": "^3.10.1"
},
"devDependencies": {
@@ -13252,11 +13252,11 @@
},
"packages/lib/dom": {
"name": "@opendaw/lib-dom",
"version": "0.0.76",
"version": "0.0.77",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@types/wicg-file-system-access": "^2023.10.6"
},
"devDependencies": {
@@ -13266,10 +13266,10 @@
},
"packages/lib/dsp": {
"name": "@opendaw/lib-dsp",
"version": "0.0.76",
"version": "0.0.77",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -13278,13 +13278,13 @@
},
"packages/lib/fusion": {
"name": "@opendaw/lib-fusion",
"version": "0.0.83",
"version": "0.0.85",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14014,11 +14014,11 @@
},
"packages/lib/jsx": {
"name": "@opendaw/lib-jsx",
"version": "0.0.76",
"version": "0.0.77",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14027,11 +14027,11 @@
},
"packages/lib/midi": {
"name": "@opendaw/lib-midi",
"version": "0.0.58",
"version": "0.0.59",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14040,10 +14040,10 @@
},
"packages/lib/runtime": {
"name": "@opendaw/lib-runtime",
"version": "0.0.72",
"version": "0.0.73",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14052,7 +14052,7 @@
},
"packages/lib/std": {
"name": "@opendaw/lib-std",
"version": "0.0.71",
"version": "0.0.72",
"license": "LGPL-3.0-or-later",
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14062,10 +14062,10 @@
},
"packages/lib/xml": {
"name": "@opendaw/lib-xml",
"version": "0.0.57",
"version": "0.0.58",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -14132,16 +14132,16 @@
},
"packages/studio/adapters": {
"name": "@opendaw/studio-adapters",
"version": "0.0.97",
"version": "0.0.99",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"soundfont2": "^0.5.0"
},
"devDependencies": {
@@ -14151,35 +14151,35 @@
},
"packages/studio/boxes": {
"name": "@opendaw/studio-boxes",
"version": "0.0.82",
"version": "0.0.83",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
"@opendaw/studio-forge-boxes": "^0.0.82",
"@opendaw/studio-forge-boxes": "^0.0.83",
"@opendaw/typescript-config": "^0.0.29"
}
},
"packages/studio/core": {
"name": "@opendaw/studio-core",
"version": "0.0.121",
"version": "0.0.126",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dawproject": "^0.0.62",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dawproject": "^0.0.63",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/nam-wasm": "^1.0.3",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"dropbox": "^10.34.0",
"y-websocket": "^1.4.5",
"yjs": "^13.6.27",
@@ -14189,9 +14189,9 @@
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@opendaw/eslint-config": "^0.0.27",
"@opendaw/studio-core-processors": "^0.0.100",
"@opendaw/studio-core-workers": "^0.0.92",
"@opendaw/studio-forge-boxes": "^0.0.82",
"@opendaw/studio-core-processors": "^0.0.103",
"@opendaw/studio-core-workers": "^0.0.94",
"@opendaw/studio-forge-boxes": "^0.0.83",
"@opendaw/typescript-config": "^0.0.29"
},
"peerDependencies": {
@@ -14201,16 +14201,16 @@
},
"packages/studio/core-processors": {
"name": "@opendaw/studio-core-processors",
"version": "0.0.100",
"version": "0.0.103",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"@types/audioworklet": "^0.0.77"
},
"devDependencies": {
@@ -15169,16 +15169,16 @@
},
"packages/studio/core-workers": {
"name": "@opendaw/studio-core-workers",
"version": "0.0.92",
"version": "0.0.94",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -15187,10 +15187,10 @@
},
"packages/studio/enums": {
"name": "@opendaw/studio-enums",
"version": "0.0.67",
"version": "0.0.68",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -15199,14 +15199,14 @@
},
"packages/studio/forge-boxes": {
"name": "@opendaw/studio-forge-boxes",
"version": "0.0.82",
"version": "0.0.83",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -15215,16 +15215,16 @@
},
"packages/studio/scripting": {
"name": "@opendaw/studio-scripting",
"version": "0.0.50",
"version": "0.0.52",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -15233,23 +15233,23 @@
},
"packages/studio/sdk": {
"name": "@opendaw/studio-sdk",
"version": "0.0.123",
"version": "0.0.128",
"license": "LGPL-3.0-or-later",
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dawproject": "^0.0.62",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-midi": "^0.0.58",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-xml": "^0.0.57",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dawproject": "^0.0.63",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-midi": "^0.0.59",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/lib-xml": "^0.0.58",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/typescript-config": "^0.0.29"
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.126](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/lab
## [0.0.125](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/lab
## [0.0.124](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/lab
## [0.0.123](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/lab
## [0.0.122](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lab
## [0.0.121](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/lab
+11 -11
View File
@@ -1,7 +1,7 @@
{
"name": "@opendaw/lab",
"private": true,
"version": "0.0.121",
"version": "0.0.126",
"type": "module",
"license": "LGPL-3.0-or-later",
"scripts": {
@@ -11,16 +11,16 @@
"lint": "eslint \"src/**/*.ts\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.9](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/nam-test
## [0.0.8](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/nam-test
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@opendaw/nam-test",
"private": true,
"version": "0.0.8",
"version": "0.0.9",
"type": "module",
"license": "LGPL-3.0-or-later",
"scripts": {
@@ -14,8 +14,8 @@
"@opendaw/lib-dom": "^0.0.68",
"@opendaw/lib-dsp": "^0.0.66",
"@opendaw/lib-jsx": "^0.0.68",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/nam-wasm": "^1.0.0"
},
"devDependencies": {
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.126](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/app-studio
## [0.0.125](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/app-studio
## [0.0.124](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/app-studio
## [0.0.123](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/app-studio
## [0.0.122](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/app-studio
## [0.0.121](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/app-studio
+13 -13
View File
@@ -1,7 +1,7 @@
{
"name": "@opendaw/app-studio",
"private": true,
"version": "0.0.121",
"version": "0.0.126",
"type": "module",
"license": "LGPL-3.0-or-later",
"scripts": {
@@ -11,18 +11,18 @@
"lint": "eslint \"src/**/*.ts\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-midi": "^0.0.58",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/studio-scripting": "^0.0.50",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-midi": "^0.0.59",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68",
"@opendaw/studio-scripting": "^0.0.52",
"@types/d3-force": "^3.0.10",
"d3-force": "^3.0.0",
"dropbox": "^10.34.0",
@@ -14,6 +14,7 @@ const IgnoredErrors = [
]
const BrowserInternalPatterns = ["feature named"]
const MonacoPatterns = ["monaco-editor", "vs/base/common/errors"]
const ThirdPartyAppPatterns = ["_callback_receiveMIDIMessage", "_callback_addSource"]
const UrlPattern = /https?:\/\/[^\s)]+/g
export class ErrorHandler {
@@ -58,6 +59,12 @@ export class ErrorHandler {
event.preventDefault()
return true
}
if (event instanceof ErrorEvent
&& ThirdPartyAppPatterns.some(pattern => event.message.includes(pattern))) {
console.warn(`Third-party app error ignored: ${event.message}`)
event.preventDefault()
return true
}
// Handle Monaco editor errors from error events
// Monaco rethrows worker error Event objects through its error pipeline,
// arriving as ErrorEvent where event.error is a raw Event (not an Error).
@@ -126,10 +126,16 @@ export class StudioService implements ProjectEnv {
readonly soundfontManager: GlobalSoundfontLoaderManager,
readonly cloudAuthManager: CloudAuthManager,
readonly buildInfo: BuildInfo) {
this.#sampleService = new SampleService(audioContext,
sample => this.#signals.notify({type: "import-sample", sample}))
this.#soundfontService = new SoundfontService(
soundfont => this.#signals.notify({type: "import-soundfont", soundfont}))
this.#sampleService = new SampleService(audioContext)
this.#sampleService.subscribe(([sample, _]) => this.#signals.notify({
type: "import-sample",
sample
}))
this.#soundfontService = new SoundfontService()
this.#soundfontService.subscribe(([soundfont, _]) => this.#signals.notify({
type: "import-soundfont",
soundfont
}))
this.samplePlayback = new SamplePlayback()
this.#projectProfileService = new ProjectProfileService({
env: this,
@@ -32,7 +32,7 @@ export namespace SlotUtils {
layout.x1 = x1
layout.y0 = rowHeight * channelIndex
layout.y1 = rowHeight * (channelIndex + 1)
PeaksPainter.renderBlocks(context, peaks, channelIndex, layout)
PeaksPainter.renderPixelStrips(context, peaks, channelIndex, layout)
}
if (x0 > 0.0 || forceBounds) {
context.fillRect(Math.round(x0), 0, 1, height * devicePixelRatio)
@@ -49,7 +49,7 @@ export namespace SlotUtils {
layout.x1 = x0
layout.y0 = rowHeight * channelIndex
layout.y1 = rowHeight * (channelIndex + 1)
PeaksPainter.renderBlocks(context, peaks, channelIndex, layout)
PeaksPainter.renderPixelStrips(context, peaks, channelIndex, layout)
}
}
if (u1 < numFrames) {
@@ -61,7 +61,7 @@ export namespace SlotUtils {
layout.x1 = wd
layout.y0 = rowHeight * channelIndex
layout.y1 = rowHeight * (channelIndex + 1)
PeaksPainter.renderBlocks(context, peaks, channelIndex, layout)
PeaksPainter.renderPixelStrips(context, peaks, channelIndex, layout)
}
}
context.globalAlpha = 1.0
@@ -57,7 +57,7 @@ export const AudioEditorCanvas = ({
if (pass.isEmpty()) {return}
const tempoMap = reader.trackBoxAdapter.unwrap().context.tempoMap
AudioRenderer.render(context, range, file, tempoMap, observableOptPlayMode, waveformOffset.getValue(),
gain.getValue(), {top: 0, bottom: actualHeight},
gain.getValue(), {top: 0, bottom: painter.height},
`hsl(${reader.hue}, ${60}%, 45%)`, pass.unwrap(), false)
const cursor = cursorModel.getValue()
@@ -33,11 +33,13 @@ export abstract class TimelineDragAndDrop<T extends (ClipCaptureTarget | RegionC
if (target?.type === "track" && target.track.trackBoxAdapter.type !== TrackType.Audio) {
return Option.None
}
if (target?.type === "clip" && target.clip.trackBoxAdapter.unwrap().type !== TrackType.Audio) {
return Option.None
if (target?.type === "clip") {
const adapter = target.clip.trackBoxAdapter
if (adapter.isEmpty() || adapter.unwrap().type !== TrackType.Audio) {return Option.None}
}
if (target?.type === "region" && target.region.trackBoxAdapter.unwrap().type !== TrackType.Audio) {
return Option.None
if (target?.type === "region") {
const adapter = target.region.trackBoxAdapter
if (adapter.isEmpty() || adapter.unwrap().type !== TrackType.Audio) {return Option.None}
}
if (data.type !== "sample" && data.type !== "instrument" && data.type !== "file") {
return Option.None
@@ -94,9 +96,13 @@ export abstract class TimelineDragAndDrop<T extends (ClipCaptureTarget | RegionC
} else if (drop?.type === "track") {
trackBoxAdapter = drop.track.trackBoxAdapter
} else if (drop?.type === "clip") {
trackBoxAdapter = drop.clip.trackBoxAdapter.unwrap()
const clipTrack = drop.clip.trackBoxAdapter
if (clipTrack.isEmpty()) {return}
trackBoxAdapter = clipTrack.unwrap()
} else if (drop?.type === "region") {
trackBoxAdapter = drop.region.trackBoxAdapter.unwrap()
const regionTrack = drop.region.trackBoxAdapter
if (regionTrack.isEmpty()) {return}
trackBoxAdapter = regionTrack.unwrap()
} else {
return panic("Illegal State")
}
@@ -2,7 +2,7 @@ import css from "./RegionLane.sass?inline"
import {Html} from "@opendaw/lib-dom"
import {Lifecycle} from "@opendaw/lib-std"
import {createElement} from "@opendaw/lib-jsx"
import {renderRegions} from "@/ui/timeline/tracks/audio-unit/regions/RegionRenderer.ts"
import {RegionRenderer} from "@/ui/timeline/tracks/audio-unit/regions/RegionRenderer.ts"
import {TrackBoxAdapter, TrackType} from "@opendaw/studio-adapters"
import {TracksManager} from "@/ui/timeline/tracks/audio-unit/TracksManager.ts"
import {CanvasPainter, TimelineRange} from "@opendaw/studio-core"
@@ -26,7 +26,7 @@ export const RegionLane = ({lifecycle, trackManager, range, adapter}: Construct)
const element: Element = (<div className={className}>{canvas}</div>)
const painter = lifecycle.own(new CanvasPainter(canvas, ({context}) => {
if (visible) {
renderRegions(context, trackManager, range, adapter.listIndex)
RegionRenderer.render(context, trackManager, range, adapter.listIndex)
updated = true
}
}))
@@ -17,158 +17,166 @@ import {Context2d} from "@opendaw/lib-dom"
import {RegionPaintBucket} from "@/ui/timeline/tracks/audio-unit/regions/RegionPaintBucket"
import {RegionLabel} from "@/ui/timeline/RegionLabel"
export const renderRegions = (context: CanvasRenderingContext2D,
tracks: TracksManager,
range: TimelineRange,
index: int): void => {
const canvas = context.canvas
const {width, height} = canvas
const {fontFamily} = getComputedStyle(canvas)
export namespace RegionRenderer {
let audioRenderStrategy: AudioRenderer.Strategy = AudioRenderer.DefaultStrategy
// subtract one pixel to avoid making special cases for a possible outline
const unitMin = range.unitMin - range.unitPadding - range.unitsPerPixel
const unitMax = range.unitMax
export const setAudioRenderStrategy = (strategy: AudioRenderer.Strategy): void => {audioRenderStrategy = strategy}
const dpr = devicePixelRatio
const fontSize = RegionLabel.fontSize() * dpr
const labelHeight = RegionLabel.labelHeight() * dpr
const bound: RegionBound = {top: labelHeight + 1.0, bottom: height - 2.5}
export const render = (context: CanvasRenderingContext2D,
tracks: TracksManager,
range: TimelineRange,
index: int): void => {
const canvas = context.canvas
const {width, height} = canvas
const {fontFamily} = getComputedStyle(canvas)
context.clearRect(0, 0, width, height)
context.textBaseline = "middle"
context.font = `${fontSize}px ${fontFamily}`
// subtract one pixel to avoid making special cases for a possible outline
const unitMin = range.unitMin - range.unitPadding - range.unitsPerPixel
const unitMax = range.unitMax
const grid = true
if (grid) {
const {timelineBoxAdapter: {signatureTrack}} = tracks.service.project
context.fillStyle = "rgba(0, 0, 0, 0.3)"
TimeGrid.fragment(
signatureTrack,
range,
({pulse}) => {
const x0 = Math.floor(range.unitToX(pulse)) * dpr
context.fillRect(x0, 0, dpr, height)
},
{minLength: 32}
)
}
const renderRegions = (strategy: RegionModifyStrategy, filterSelected: boolean, hideSelected: boolean): void => {
const optTrack = tracks.getByIndex(strategy.translateTrackIndex(index))
if (optTrack.isEmpty()) {return}
const trackBoxAdapter = optTrack.unwrap().trackBoxAdapter
const trackDisabled = !trackBoxAdapter.enabled.getValue()
const regions = strategy.iterateRange(trackBoxAdapter.regions.collection, unitMin, unitMax)
for (const [region, next] of Iterables.pairWise(regions)) {
if (region.isSelected ? hideSelected : !filterSelected) {continue}
const actualComplete = strategy.readComplete(region)
const position = strategy.readPosition(region)
const complete = region.isSelected
? actualComplete
: // for no-stretched audio region
Math.min(actualComplete, next?.position ?? Number.POSITIVE_INFINITY)
const x0Int = Math.floor(range.unitToX(Math.max(position, unitMin))) * dpr
const x1Int = Math.max(Math.floor(range.unitToX(Math.min(complete, unitMax)) - 1) * dpr, x0Int + dpr)
const xnInt = x1Int - x0Int
const {labelColor, labelBackground, contentColor, contentBackground, loopStrokeColor} =
RegionPaintBucket.create(region, region.isSelected && !filterSelected, trackDisabled)
context.clearRect(x0Int, 0, xnInt, height)
context.fillStyle = labelBackground
context.fillRect(x0Int, 0, xnInt, labelHeight)
context.fillStyle = contentBackground
context.fillRect(x0Int, labelHeight, xnInt, height - labelHeight)
const maxTextWidth = xnInt - 3 * dpr // subtract text-padding
context.fillStyle = labelColor
if (strategy.readMirror(region)) {
context.font = `italic ${fontSize}px ${fontFamily}`
} else {
context.font = `${fontSize}px ${fontFamily}`
}
const text = region.label.length === 0 ? "◻" : region.label
context.fillText(Context2d.truncateText(context, text, maxTextWidth).text, x0Int + 3 * dpr, 1 + labelHeight / 2)
if (!region.hasCollection) {continue}
context.fillStyle = contentColor
region.accept({
visitNoteRegionBoxAdapter: (region: NoteRegionBoxAdapter): void => {
const optCollection = region.optCollection
if (optCollection.isEmpty()) {return}
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
NotesRenderer.render(context, range, optCollection.unwrap(), bound, contentColor, pass)
}
const dpr = devicePixelRatio
const cssLabelHeight = RegionLabel.labelHeight()
const fontSize = RegionLabel.fontSize() * dpr
const labelHeight = cssLabelHeight * dpr
const bound: RegionBound = {top: cssLabelHeight + 1, bottom: canvas.clientHeight - 2}
context.clearRect(0, 0, width, height)
context.textBaseline = "middle"
context.font = `${fontSize}px ${fontFamily}`
const grid = true
if (grid) {
const {timelineBoxAdapter: {signatureTrack}} = tracks.service.project
context.fillStyle = "rgba(0, 0, 0, 0.3)"
TimeGrid.fragment(
signatureTrack,
range,
({pulse}) => {
const x0 = Math.floor(range.unitToX(pulse)) * dpr
context.fillRect(x0, 0, dpr, height)
},
visitAudioRegionBoxAdapter: (region: AudioRegionBoxAdapter): void => {
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
const tempoMap = region.trackBoxAdapter.unwrap().context.tempoMap
AudioRenderer.render(context, range, region.file, tempoMap,
region.observableOptPlayMode, region.waveformOffset.getValue(),
region.gain.getValue(), bound, contentColor, pass
)
}
AudioFadingRenderer.render(context, range, region.fading, bound, position, complete, labelBackground)
const isRecording = region.file.getOrCreateLoader().state.type === "record"
if (isRecording) {}
},
visitValueRegionBoxAdapter: (region: ValueRegionBoxAdapter) => {
const padding = dpr
const top = labelHeight + padding
const bottom = height - padding * 2
context.save()
context.beginPath()
context.rect(x0Int + padding, top, x1Int - x0Int - padding, bottom - top + padding)
context.clip()
const valueToY = (value: unitValue): number => bottom + value * (top - bottom)
const events = region.events.unwrap()
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
const windowMin = pass.resultStart - pass.rawStart
const windowMax = pass.resultEnd - pass.rawStart
context.strokeStyle = contentColor
context.beginPath()
const adapters = ValueEvent.iterateWindow(events, windowMin, windowMax)
ValueStreamRenderer.render(context, range, adapters, valueToY, contentColor, 0.2, 0.0, pass)
context.stroke()
}
context.restore()
}
})
const isEditing = tracks.service.project.userEditingManager.timeline.isEditing(region.box)
if (isEditing) {
{minLength: 32}
)
}
const renderRegions = (strategy: RegionModifyStrategy, filterSelected: boolean, hideSelected: boolean): void => {
const optTrack = tracks.getByIndex(strategy.translateTrackIndex(index))
if (optTrack.isEmpty()) {return}
const trackBoxAdapter = optTrack.unwrap().trackBoxAdapter
const trackDisabled = !trackBoxAdapter.enabled.getValue()
const regions = strategy.iterateRange(trackBoxAdapter.regions.collection, unitMin, unitMax)
for (const [region, next] of Iterables.pairWise(regions)) {
if (region.isSelected ? hideSelected : !filterSelected) {continue}
const actualComplete = strategy.readComplete(region)
const position = strategy.readPosition(region)
const complete = region.isSelected
? actualComplete
: // for no-stretched audio region
Math.min(actualComplete, next?.position ?? Number.POSITIVE_INFINITY)
const x0Int = Math.floor(range.unitToX(Math.max(position, unitMin))) * dpr
const x1Int = Math.max(Math.floor(range.unitToX(Math.min(complete, unitMax)) - 1) * dpr, x0Int + dpr)
const xnInt = x1Int - x0Int
const {labelColor, labelBackground, contentColor, contentBackground, loopStrokeColor} =
RegionPaintBucket.create(region, region.isSelected && !filterSelected, trackDisabled)
context.clearRect(x0Int, 0, xnInt, height)
context.fillStyle = labelBackground
context.fillRect(x1Int - dpr, labelHeight, dpr, height - labelHeight - dpr)
context.fillRect(x0Int, labelHeight, dpr, height - labelHeight - dpr)
context.fillRect(x0Int, height - dpr, xnInt, height - dpr)
context.fillRect(x0Int, 0, xnInt, labelHeight)
context.fillStyle = contentBackground
context.fillRect(x0Int, labelHeight, xnInt, height - labelHeight)
const maxTextWidth = xnInt - 3 * dpr // subtract text-padding
context.fillStyle = labelColor
if (strategy.readMirror(region)) {
context.font = `italic ${fontSize}px ${fontFamily}`
} else {
context.font = `${fontSize}px ${fontFamily}`
}
const text = region.label.length === 0 ? "◻" : region.label
context.fillText(Context2d.truncateText(context, text, maxTextWidth).text, x0Int + 3 * dpr, 1 + labelHeight / 2)
if (!region.hasCollection) {continue}
context.fillStyle = contentColor
region.accept({
visitNoteRegionBoxAdapter: (region: NoteRegionBoxAdapter): void => {
const optCollection = region.optCollection
if (optCollection.isEmpty()) {return}
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
NotesRenderer.render(context, range, optCollection.unwrap(), bound, contentColor, pass)
}
},
visitAudioRegionBoxAdapter: (region: AudioRegionBoxAdapter): void => {
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
const tempoMap = region.trackBoxAdapter.unwrap().context.tempoMap
AudioRenderer.render(context, range, region.file, tempoMap,
region.observableOptPlayMode, region.waveformOffset.getValue(),
region.gain.getValue(), bound, contentColor, pass,
true, audioRenderStrategy
)
}
AudioFadingRenderer.render(context, range, region.fading, bound, position, complete, labelBackground)
const isRecording = region.file.getOrCreateLoader().state.type === "record"
if (isRecording) {}
},
visitValueRegionBoxAdapter: (region: ValueRegionBoxAdapter) => {
const padding = dpr
const top = labelHeight + padding
const bottom = height - padding * 2
context.save()
context.beginPath()
context.rect(x0Int + padding, top, x1Int - x0Int - padding, bottom - top + padding)
context.clip()
const valueToY = (value: unitValue): number => bottom + value * (top - bottom)
const events = region.events.unwrap()
for (const pass of LoopableRegion.locateLoops({
position, complete,
loopOffset: strategy.readLoopOffset(region),
loopDuration: strategy.readLoopDuration(region)
}, unitMin, unitMax)) {
if (pass.index > 0) {
const x = Math.floor(range.unitToX(pass.resultStart) * dpr)
context.fillStyle = loopStrokeColor
context.fillRect(x, labelHeight, 1, height - labelHeight)
}
const windowMin = pass.resultStart - pass.rawStart
const windowMax = pass.resultEnd - pass.rawStart
context.strokeStyle = contentColor
context.beginPath()
const adapters = ValueEvent.iterateWindow(events, windowMin, windowMax)
ValueStreamRenderer.render(context, range, adapters, valueToY, contentColor, 0.2, 0.0, pass)
context.stroke()
}
context.restore()
}
})
const isEditing = tracks.service.project.userEditingManager.timeline.isEditing(region.box)
if (isEditing) {
context.fillStyle = labelBackground
context.fillRect(x1Int - dpr, labelHeight, dpr, height - labelHeight - dpr)
context.fillRect(x0Int, labelHeight, dpr, height - labelHeight - dpr)
context.fillRect(x0Int, height - dpr, xnInt, height - dpr)
}
}
}
const modifier: Option<RegionModifyStrategies> = tracks.currentRegionModifier
const strategy = modifier.unwrapOrElse(RegionModifyStrategies.Identity)
renderRegions(strategy.unselectedModifyStrategy(), true, !strategy.showOrigin())
renderRegions(strategy.selectedModifyStrategy(), false, false)
}
const modifier: Option<RegionModifyStrategies> = tracks.currentRegionModifier
const strategy = modifier.unwrapOrElse(RegionModifyStrategies.Identity)
renderRegions(strategy.unselectedModifyStrategy(), true, !strategy.showOrigin())
renderRegions(strategy.selectedModifyStrategy(), false, false)
}
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.80](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-box-forge
## [0.0.79](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-box-forge
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-box-forge",
"version": "0.0.79",
"version": "0.0.80",
"private": true,
"sideEffects": false,
"license": "LGPL-3.0-or-later",
@@ -16,10 +16,10 @@
"test": "npm run gen && vitest run"
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"ts-morph": "^25.0.1",
"tsx": "^4.20.3"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.78](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-box
## [0.0.77](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-box
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-box",
"version": "0.0.77",
"version": "0.0.78",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,8 +23,8 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.63](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-dawproject
## [0.0.62](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-dawproject
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-dawproject",
"version": "0.0.62",
"version": "0.0.63",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,9 +23,9 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-xml": "^0.0.57",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-xml": "^0.0.58",
"jszip": "^3.10.1"
},
"devDependencies": {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.77](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-dom
## [0.0.76](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-dom
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-dom",
"version": "0.0.76",
"version": "0.0.77",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,8 +23,8 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@types/wicg-file-system-access": "^2023.10.6"
},
"devDependencies": {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.77](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-dsp
## [0.0.76](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-dsp
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-dsp",
"version": "0.0.76",
"version": "0.0.77",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,7 +27,7 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.85](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/lib-fusion
## [0.0.84](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-fusion
## [0.0.83](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-fusion
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-fusion",
"version": "0.0.83",
"version": "0.0.85",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "LGPL-3.0-or-later",
@@ -23,10 +23,10 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
@@ -13,10 +13,10 @@ export namespace PeaksPainter {
v1: number // value max (+1)
}
export const renderBlocks = (path: CanvasRenderingContext2D,
peaks: Peaks,
channelIndex: int,
{u0, u1, v0, v1, x0, x1, y0, y1}: Layout): void => {
export const renderPixelStrips = (path: CanvasRenderingContext2D,
peaks: Peaks,
channelIndex: int,
{u0, u1, v0, v1, x0, x1, y0, y1}: Layout): void => {
const unitsEachPixel = (u1 - u0) / (x1 - x0)
const stage = peaks.nearest(unitsEachPixel)
if (stage === null) {return}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.77](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-jsx
## [0.0.76](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-jsx
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-jsx",
"version": "0.0.76",
"version": "0.0.77",
"license": "LGPL-3.0-or-later",
"sideEffects": false,
"main": "./dist/index.js",
@@ -23,8 +23,8 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.59](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-midi
## [0.0.58](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-midi
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-midi",
"version": "0.0.58",
"version": "0.0.59",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,8 +23,8 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.73](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-runtime
## [0.0.72](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-runtime
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-runtime",
"version": "0.0.72",
"version": "0.0.73",
"license": "LGPL-3.0-or-later",
"sideEffects": false,
"publishConfig": {
@@ -23,7 +23,7 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.72](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-std
## [0.0.71](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-std
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-std",
"version": "0.0.71",
"version": "0.0.72",
"license": "LGPL-3.0-or-later",
"sideEffects": false,
"publishConfig": {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.58](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/lib-xml
## [0.0.57](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/lib-xml
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/lib-xml",
"version": "0.0.57",
"version": "0.0.58",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.99](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-adapters
## [0.0.98](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-adapters
## [0.0.97](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-adapters
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-adapters",
"version": "0.0.97",
"version": "0.0.99",
"license": "LGPL-3.0-or-later",
"publishConfig": {
"access": "public"
@@ -23,13 +23,13 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"soundfont2": "^0.5.0"
},
"devDependencies": {
@@ -118,6 +118,16 @@ export class NoteEventCollectionBoxAdapter implements BoxAdapter, SelectableLoca
return result
}
overlapping(from: ppqn, to: ppqn, pitch: int): ReadonlyArray<NoteEventBoxAdapter> {
const result: Array<NoteEventBoxAdapter> = []
for (const event of this.#events.asArray()) {
if (event.pitch === pitch && event.complete > from && event.position < to) {
result.push(event)
}
}
return result
}
requestSorting(): void {
this.#events.onIndexingChanged()
this.onEventPropertyChanged()
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.83](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-boxes
## [0.0.82](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-06)
**Note:** Version bump only for package @opendaw/studio-boxes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-boxes",
"version": "0.0.82",
"version": "0.0.83",
"license": "LGPL-3.0-or-later",
"publishConfig": {
"access": "public"
@@ -22,13 +22,13 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
"@opendaw/studio-forge-boxes": "^0.0.82",
"@opendaw/studio-forge-boxes": "^0.0.83",
"@opendaw/typescript-config": "^0.0.29"
},
"gitHead": "e4f70170c5947610932306da94e7f0bb817e43b3"
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.103](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-core-processors
## [0.0.102](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-core-processors
## [0.0.101](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-core-processors
## [0.0.100](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-core-processors
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-core-processors",
"version": "0.0.100",
"version": "0.0.103",
"license": "LGPL-3.0-or-later",
"private": true,
"exports": {
@@ -13,13 +13,13 @@
"test": "vitest run"
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"@types/audioworklet": "^0.0.77"
},
"devDependencies": {
@@ -9,7 +9,6 @@ import {
Observer,
Option,
panic,
quantizeFloor,
SortedSet,
Subscription,
SyncStream,
@@ -120,7 +119,15 @@ export class EngineProcessor extends AudioWorkletProcessor implements EngineCont
#playbackTimestamp: ppqn = 0.0 // this is where we start playing again (after paused)
#perfWriteIndex: number = 0
constructor({processorOptions: {syncStreamBuffer, controlFlagsBuffer, hrClockBuffer, project, exportConfiguration}}: {
constructor({
processorOptions: {
syncStreamBuffer,
controlFlagsBuffer,
hrClockBuffer,
project,
exportConfiguration
}
}: {
processorOptions: EngineProcessorAttachment
} & AudioNodeOptions) {
super()
@@ -500,7 +507,7 @@ export class EngineProcessor extends AudioWorkletProcessor implements EngineCont
const position = this.#timeInfo.position
const [nominator, denominator] = this.#timelineBoxAdapter.signature
const countInOffset = PPQN.fromSignature(this.#preferences.settings.recording.countInBars * nominator, denominator)
this.#recordingStartTime = quantizeFloor(position, PPQN.fromSignature(nominator, denominator))
this.#recordingStartTime = position
this.#timeInfo.isCountingIn = true
this.#timeInfo.metronomeEnabled = true
this.#timeInfo.transporting = true
@@ -1,15 +1,9 @@
import {int} from "@opendaw/lib-std"
import {int, UUID} from "@opendaw/lib-std"
import {AudioBuffer, AudioData} from "@opendaw/lib-dsp"
import {VoiceState} from "./VoiceState"
/**
* PitchVoice is used for pitch-stretch mode (warp marker based).
* It does NOT implement the Voice interface as it's managed differently
* by the TapeDeviceProcessor (not via TimeStretchSequencer).
*
* Uses fadeDirection to track fading in (+1) vs fading out (-1).
*/
export class PitchVoice {
readonly sourceUuid: UUID.Bytes
readonly #output: AudioBuffer
readonly #data: AudioData
readonly #fadeLength: number
@@ -22,8 +16,9 @@ export class PitchVoice {
#blockOffset: int
#fadeOutBlockOffset: int = 0
constructor(output: AudioBuffer, data: AudioData, fadeLength: number, playbackRate: number,
constructor(sourceUuid: UUID.Bytes, output: AudioBuffer, data: AudioData, fadeLength: number, playbackRate: number,
offset: number = 0.0, blockOffset: int = 0) {
this.sourceUuid = sourceUuid
this.#output = output
this.#data = data
this.#fadeLength = fadeLength
@@ -20,14 +20,12 @@ import {DeviceProcessor} from "../../DeviceProcessor"
import {NoteEventTarget} from "../../NoteEventSource"
import {VOICE_FADE_DURATION} from "./Tape/constants"
import {PitchVoice} from "./Tape/PitchVoice"
import {Voice} from "./Tape/Voice"
import {TimeStretchSequencer} from "./Tape/TimeStretchSequencer"
type AnyVoice = Voice | PitchVoice
type Lane = {
adapter: TrackBoxAdapter
voices: Array<AnyVoice>
pitchVoices: SortedSet<UUID.Bytes, PitchVoice>
fadingVoices: Array<PitchVoice>
sequencer: TimeStretchSequencer
}
@@ -37,6 +35,11 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
readonly #peaks: PeakBroadcaster
readonly #lanes: SortedSet<UUID.Bytes, Lane>
readonly #fadingGainBuffer: Float32Array = new Float32Array(RenderQuantum)
readonly #unitGainBuffer: Float32Array = (() => {
const buffer = new Float32Array(RenderQuantum)
buffer.fill(1.0)
return buffer
})()
#enabled: boolean = true
@@ -55,7 +58,8 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
this.#adapter.deviceHost().audioUnitBoxAdapter().tracks.catchupAndSubscribe({
onAdd: (adapter: TrackBoxAdapter) => this.#lanes.add({
adapter,
voices: [],
pitchVoices: UUID.newSet<PitchVoice>(voice => voice.sourceUuid),
fadingVoices: [],
sequencer: new TimeStretchSequencer()
}),
onRemove: (adapter: TrackBoxAdapter) => this.#lanes.removeByKey(adapter.uuid),
@@ -79,7 +83,8 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
this.#audioOutput.clear()
this.eventInput.clear()
this.#lanes.forEach(lane => {
lane.voices = []
lane.pitchVoices.clear()
lane.fadingVoices = []
lane.sequencer.reset()
})
}
@@ -98,12 +103,17 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
#processBlock(lane: Lane, block: Block): void {
const {adapter} = lane
if (adapter.type !== TrackType.Audio || !adapter.enabled.getValue()) {
lane.voices.forEach(voice => voice.startFadeOut(0))
this.#fadeOutAllPitchVoices(lane)
lane.sequencer.reset()
return
}
const {p0, p1, flags} = block
const {p0, p1, s0, s1, flags} = block
if (!Bits.every(flags, BlockFlag.transporting | BlockFlag.playing)) {return}
if (Bits.some(flags, BlockFlag.discontinuous)) {
this.#fadeOutAllPitchVoices(lane)
lane.sequencer.reset()
}
const visitedUuids: Set<string> = new Set()
const intervals = this.context.clipSequencing.iterate(adapter.uuid, p0, p1)
for (const {optClip, sectionFrom, sectionTo} of intervals) {
optClip.match({
@@ -113,7 +123,6 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
const file = region.file
const optData = file.getOrCreateLoader().data
if (optData.isEmpty()) {return}
const waveformOffset = region.waveformOffset.getValue()
const timeStretch = region.asPlayModeTimeStretch
if (timeStretch.nonEmpty()) {
const transients: EventCollection<TransientMarkerBoxAdapter> = file.transients
@@ -121,13 +130,15 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
for (const cycle of LoopableRegion.locateLoops(region, p0, p1)) {
const timeStretchBoxAdapter = timeStretch.unwrap()
this.#processPassTimestretch(lane, block, cycle,
optData.unwrap(), timeStretchBoxAdapter, transients, waveformOffset, region.fading,
optData.unwrap(), timeStretchBoxAdapter, transients,
region.waveformOffset.getValue(), region.fading,
region.position, region.duration)
}
} else {
visitedUuids.add(UUID.toString(region.uuid))
for (const cycle of LoopableRegion.locateLoops(region, p0, p1)) {
this.#processPassPitch(
lane, block, cycle, region, optData.unwrap())
lane, block, cycle, region, optData.unwrap(), region.uuid)
}
}
}
@@ -152,26 +163,39 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
timeStretch, transients, clip.waveformOffset.getValue(), null, 0, clip.duration)
}
} else {
visitedUuids.add(UUID.toString(clip.uuid))
for (const cycle of LoopableRegion.locateLoops({
position: 0.0,
loopDuration: clip.duration,
loopOffset: 0.0,
complete: Number.POSITIVE_INFINITY
}, sectionFrom, sectionTo)) {
this.#processPassPitch(lane, block, cycle, clip, optData.unwrap())
this.#processPassPitch(lane, block, cycle, clip, optData.unwrap(), clip.uuid)
}
}
}
})
}
lane.pitchVoices.removeByPredicate(voice => {
if (visitedUuids.has(UUID.toString(voice.sourceUuid))) {return false}
voice.startFadeOut(0)
lane.fadingVoices.push(voice)
return true
})
const sn = s1 - s0
for (const voice of lane.fadingVoices) {
voice.process(s0, sn, this.#unitGainBuffer)
}
lane.fadingVoices = lane.fadingVoices.filter(voice => !voice.done())
}
#processPassPitch(lane: Lane,
block: Block,
cycle: LoopableRegion.LoopCycle,
adapter: AudioContentBoxAdapter,
data: AudioData): void {
const {p0, p1, s0, s1, flags} = block
data: AudioData,
sourceUuid: UUID.Bytes): void {
const {p0, p1, s0, s1} = block
const sn = s1 - s0
const pn = p1 - p0
const r0 = (cycle.resultStart - p0) / pn
@@ -181,15 +205,11 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
const bpn = (bp1 - bp0) | 0
const waveformOffset: number = adapter.waveformOffset.getValue()
assert(s0 <= bp0 && bp1 <= s1, () => `Out of bounds ${bp0}, ${bp1}`)
if (Bits.some(flags, BlockFlag.discontinuous)) {
lane.voices.forEach(voice => voice.startFadeOut(0))
lane.sequencer.reset()
}
const asPlayModePitch = adapter.asPlayModePitchStretch
if (adapter.isPlayModeNoStretch) {
const elapsedSeconds = this.context.tempoMap.intervalToSeconds(cycle.rawStart, cycle.resultStart)
const offset = (elapsedSeconds + waveformOffset) * data.sampleRate
this.#updateOrCreatePitchVoice(lane, data, data.sampleRate / sampleRate, offset, 0)
this.#updateOrCreatePitchVoice(lane, sourceUuid, data, data.sampleRate / sampleRate, offset, 0)
} else if (asPlayModePitch.isEmpty()) {
const audioDurationSamples = data.numberOfFrames
const audioDurationNormalized = cycle.resultEndValue - cycle.resultStartValue
@@ -197,25 +217,25 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
const timelineSamplesInCycle = (cycle.resultEnd - cycle.resultStart) / pn * sn
const playbackRate = audioSamplesInCycle / timelineSamplesInCycle
const offset = cycle.resultStartValue * data.numberOfFrames + waveformOffset * data.sampleRate
this.#updateOrCreatePitchVoice(lane, data, playbackRate, offset, 0)
this.#updateOrCreatePitchVoice(lane, sourceUuid, data, playbackRate, offset, 0)
} else {
const pitchBoxAdapter = asPlayModePitch.unwrap()
const warpMarkers = pitchBoxAdapter.warpMarkers
const firstWarp = warpMarkers.first()
const lastWarp = warpMarkers.last()
if (firstWarp === null || lastWarp === null) {
lane.voices.forEach(voice => voice.startFadeOut(0))
this.#evictPitchVoice(lane, sourceUuid)
return
}
const contentPpqn = cycle.resultStart - cycle.rawStart
if (contentPpqn < firstWarp.position || contentPpqn >= lastWarp.position) {
lane.voices.forEach(voice => voice.startFadeOut(0))
this.#evictPitchVoice(lane, sourceUuid)
return
}
const currentSeconds = this.#ppqnToSeconds(contentPpqn, cycle.resultStartValue, warpMarkers)
const playbackRate = this.#getPlaybackRateFromWarp(contentPpqn, warpMarkers, data.sampleRate, pn, sn)
const offset = (currentSeconds + waveformOffset) * data.sampleRate
this.#updateOrCreatePitchVoice(lane, data, playbackRate, offset, 0)
this.#updateOrCreatePitchVoice(lane, sourceUuid, data, playbackRate, offset, 0)
}
if (isInstanceOf(adapter, AudioRegionBoxAdapter) && adapter.fading.hasFading) {
const regionPosition = adapter.position
@@ -226,40 +246,55 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
} else {
this.#fadingGainBuffer.fill(1.0, 0, bpn)
}
for (const voice of lane.voices) {
const voice = lane.pitchVoices.getOrNull(sourceUuid)
if (voice !== null) {
voice.process(bp0 | 0, bpn, this.#fadingGainBuffer)
if (voice.done()) {
lane.pitchVoices.removeByKey(sourceUuid)
}
}
lane.voices = lane.voices.filter(voice => !voice.done())
}
#updateOrCreatePitchVoice(lane: Lane, data: AudioData, playbackRate: number, offset: number, blockOffset: int): void {
#updateOrCreatePitchVoice(lane: Lane, sourceUuid: UUID.Bytes, data: AudioData,
playbackRate: number, offset: number, blockOffset: int): void {
const fadeLengthSamples = Math.round(VOICE_FADE_DURATION * sampleRate)
if (lane.voices.length === 0) {
lane.voices.push(new PitchVoice(this.#audioOutput, data, fadeLengthSamples, playbackRate, offset, blockOffset))
const existing = lane.pitchVoices.getOrNull(sourceUuid)
if (existing === null) {
lane.pitchVoices.add(
new PitchVoice(sourceUuid, this.#audioOutput, data, fadeLengthSamples, playbackRate, offset, blockOffset), true)
} else if (existing.isFadingOut()) {
lane.fadingVoices.push(existing)
lane.pitchVoices.add(
new PitchVoice(sourceUuid, this.#audioOutput, data, fadeLengthSamples, playbackRate, offset, blockOffset), true)
} else {
let hasActiveVoice = false
for (const voice of lane.voices) {
if (voice instanceof PitchVoice) {
if (voice.isFadingOut()) {
continue
}
const drift = Math.abs(voice.readPosition - offset)
if (drift > fadeLengthSamples) {
voice.startFadeOut(blockOffset)
} else {
voice.setPlaybackRate(playbackRate)
hasActiveVoice = true
}
} else {
voice.startFadeOut(blockOffset)
}
}
if (!hasActiveVoice) {
lane.voices.push(new PitchVoice(this.#audioOutput, data, fadeLengthSamples, playbackRate, offset, blockOffset))
const drift = Math.abs(existing.readPosition - offset)
if (drift > fadeLengthSamples) {
existing.startFadeOut(blockOffset)
lane.fadingVoices.push(existing)
lane.pitchVoices.add(
new PitchVoice(sourceUuid, this.#audioOutput, data, fadeLengthSamples, playbackRate, offset, blockOffset), true)
} else {
existing.setPlaybackRate(playbackRate)
}
}
}
#evictPitchVoice(lane: Lane, sourceUuid: UUID.Bytes): void {
const voice = lane.pitchVoices.removeByKeyIfExist(sourceUuid)
if (voice !== null) {
voice.startFadeOut(0)
lane.fadingVoices.push(voice)
}
}
#fadeOutAllPitchVoices(lane: Lane): void {
for (const voice of lane.pitchVoices) {
voice.startFadeOut(0)
lane.fadingVoices.push(voice)
}
lane.pitchVoices.clear()
}
#processPassTimestretch(lane: Lane,
block: Block,
cycle: LoopableRegion.LoopCycle,
@@ -270,11 +305,7 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
fadingConfig: FadingEnvelope.Config | null,
regionPosition: number,
regionDuration: number): void {
for (const voice of lane.voices) {
if (voice instanceof PitchVoice) {
voice.startFadeOut(0)
}
}
this.#fadeOutAllPitchVoices(lane)
const {p0, p1, s0, s1} = block
const sn = s1 - s0
const pn = p1 - p0
@@ -300,8 +331,6 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
cycle,
this.#fadingGainBuffer
)
lane.voices = lane.voices.filter(voice =>
voice instanceof PitchVoice && !voice.done())
}
#getPlaybackRateFromWarp(ppqn: number,
@@ -329,4 +358,4 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
const alpha = (ppqn - left.position) / (right.position - left.position)
return left.seconds + alpha * (right.seconds - left.seconds)
}
}
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.94](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-core-workers
## [0.0.93](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-core-workers
## [0.0.92](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-core-workers
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-core-workers",
"version": "0.0.92",
"version": "0.0.94",
"license": "LGPL-3.0-or-later",
"private": true,
"exports": {
@@ -12,13 +12,13 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.126](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/studio-core
## [0.0.125](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/studio-core
## [0.0.124](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-core
## [0.0.123](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-core
## [0.0.122](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-core
## [0.0.121](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-core
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-core",
"version": "0.0.121",
"version": "0.0.126",
"license": "LGPL-3.0-or-later",
"publishConfig": {
"access": "public"
@@ -33,17 +33,17 @@
"test": "vitest run --config vitest.config.ts"
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dawproject": "^0.0.62",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dawproject": "^0.0.63",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/nam-wasm": "^1.0.3",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68",
"dropbox": "^10.34.0",
"y-websocket": "^1.4.5",
"yjs": "^13.6.27",
@@ -57,9 +57,9 @@
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@opendaw/eslint-config": "^0.0.27",
"@opendaw/studio-core-processors": "^0.0.100",
"@opendaw/studio-core-workers": "^0.0.92",
"@opendaw/studio-forge-boxes": "^0.0.82",
"@opendaw/studio-core-processors": "^0.0.103",
"@opendaw/studio-core-workers": "^0.0.94",
"@opendaw/studio-forge-boxes": "^0.0.83",
"@opendaw/typescript-config": "^0.0.29"
},
"gitHead": "e4f70170c5947610932306da94e7f0bb817e43b3"
+10 -4
View File
@@ -4,10 +4,12 @@ import {
Errors,
isInstanceOf,
isNotUndefined,
Notifier,
Observer,
panic,
Procedure,
Progress,
RuntimeNotifier,
Subscription,
UUID
} from "@opendaw/lib-std"
import {Files} from "@opendaw/lib-dom"
@@ -20,18 +22,22 @@ export namespace AssetService {
export type ImportArgs = {
uuid?: UUID.Bytes
name?: string,
bpm?: number,
arrayBuffer: ArrayBuffer,
progressHandler?: Progress.Handler
progressHandler?: Progress.Handler,
origin?: "import" | "recording"
}
}
export abstract class AssetService<T extends Sample | Soundfont> {
export abstract class AssetService<T extends Sample | Soundfont, RAW = void> {
protected abstract readonly nameSingular: string
protected abstract readonly namePlural: string
protected abstract readonly boxType: Class<AudioFileBox | SoundfontFileBox>
protected abstract readonly filePickerOptions: FilePickerOptions
protected constructor(protected readonly onUpdate: Procedure<T>) {}
protected readonly notifier: Notifier<[T, RAW]> = new Notifier<[T, RAW]>()
subscribe(observer: Observer<[T, RAW]>): Subscription {return this.notifier.subscribe(observer)}
async browse(multiple: boolean): Promise<ReadonlyArray<T>> {
return this.browseFiles(multiple, this.filePickerOptions)
+15 -34
View File
@@ -1,30 +1,21 @@
import {
ByteArrayInput,
int,
Notifier,
Observer,
Option,
panic,
Procedure,
Progress,
Subscription,
Terminable,
Terminator,
UUID
} from "@opendaw/lib-std"
import {AudioData, BPMTools} from "@opendaw/lib-dsp"
import {Peaks, SamplePeaks} from "@opendaw/lib-fusion"
import {
mergeChunkPlanes,
RingBuffer,
SampleLoader,
SampleLoaderState,
SampleMetaData
} from "@opendaw/studio-adapters"
import {SampleStorage} from "./samples"
import {AudioData} from "@opendaw/lib-dsp"
import {Peaks} from "@opendaw/lib-fusion"
import {mergeChunkPlanes, RingBuffer, SampleLoader, SampleLoaderState} from "@opendaw/studio-adapters"
import {RenderQuantum} from "./RenderQuantum"
import {Workers} from "./Workers"
import {PeaksWriter} from "./PeaksWriter"
import {SampleService} from "./samples"
export class RecordingWorklet extends AudioWorkletNode implements Terminable, SampleLoader {
readonly #terminator: Terminator = new Terminator()
@@ -42,6 +33,8 @@ export class RecordingWorklet extends AudioWorkletNode implements Terminable, Sa
#limitSamples: int = Number.POSITIVE_INFINITY
#state: SampleLoaderState = {type: "record"}
#onSaved: Option<Procedure<UUID.Bytes>> = Option.None
#sampleService: Option<SampleService> = Option.None
#bpm: Option<number> = Option.None
constructor(context: BaseAudioContext, config: RingBuffer.Config) {
super(context, "recording-processor", {
@@ -76,6 +69,8 @@ export class RecordingWorklet extends AudioWorkletNode implements Terminable, Sa
}
set onSaved(callback: Procedure<UUID.Bytes>) {this.#onSaved = Option.wrap(callback)}
set bpm(value: number) {this.#bpm = Option.wrap(value)}
set sampleService(service: SampleService) {this.#sampleService = Option.wrap(service)}
setFillLength(value: int): void {this.#peakWriter.numFrames = value}
@@ -102,36 +97,22 @@ export class RecordingWorklet extends AudioWorkletNode implements Terminable, Sa
toString(): string {return `{RecordingWorklet}`}
async #finalize(): Promise<SampleStorage.NewSample> {
async #finalize(): Promise<void> {
this.#isRecording = false
this.#reader.stop()
if (this.#output.length === 0) {return panic("No recording data available")}
const totalSamples: int = this.#limitSamples
const sample_rate = this.context.sampleRate
const numberOfChannels = this.channelCount
const mergedFrames = mergeChunkPlanes(this.#output, RenderQuantum, this.#output.length * RenderQuantum)
.map(frame => frame.slice(-totalSamples))
const audioData = AudioData.create(sample_rate, totalSamples, numberOfChannels)
mergedFrames.forEach((frame, i) => audioData.frames[i].set(frame))
const audioData = AudioData.create(this.context.sampleRate, totalSamples, this.channelCount)
mergedFrames.forEach((frame, index) => audioData.frames[index].set(frame))
this.#data = Option.wrap(audioData)
const shifts = SamplePeaks.findBestFit(totalSamples)
const peaks = await Workers
.Peak.generateAsync(Progress.Empty, shifts, audioData.frames, totalSamples, numberOfChannels)
this.#peaks = Option.wrap(SamplePeaks.from(new ByteArrayInput(peaks)))
const bpm = BPMTools.detect(audioData.frames[0], sample_rate)
const duration = totalSamples / sample_rate
const meta: SampleMetaData = {name: "Recording", bpm, sample_rate, duration, origin: "recording"}
const sample: SampleStorage.NewSample = {
uuid: this.uuid,
audio: audioData,
peaks: peaks as ArrayBuffer,
meta
}
await SampleStorage.get().save(sample)
this.#onSaved.ifSome(callback => callback(this.uuid))
const sample = await this.#sampleService
.unwrap("SampleService not set")
.importRecording(audioData, this.#bpm.unwrapOrElse(120))
this.#onSaved.ifSome(callback => callback(UUID.parse(sample.uuid)))
this.#setState({type: "loaded"})
this.terminate()
return sample
}
#setState(value: SampleLoaderState): void {
@@ -103,7 +103,7 @@ export class CaptureAudio extends Capture<CaptureAudioBox> {
async prepareRecording(): Promise<void> {
const {project} = this.manager
const {env: {audioContext, audioWorklets, sampleManager}} = project
const {env: {audioContext, audioWorklets, sampleManager, sampleService}} = project
if (isUndefined(audioContext.outputLatency)) {
const approved = RuntimeNotifier.approve({
headline: "Warning",
@@ -122,6 +122,8 @@ export class CaptureAudio extends Capture<CaptureAudioBox> {
}
const {gainNode, channelCount} = audioChain
const recordingWorklet = audioWorklets.createRecording(channelCount, RenderQuantum)
recordingWorklet.bpm = project.timelineBox.bpm.getValue()
recordingWorklet.sampleService = sampleService
sampleManager.record(recordingWorklet)
gainNode.connect(recordingWorklet)
this.#preparedWorklet = recordingWorklet
@@ -124,7 +124,27 @@ export namespace RecordAudio {
currentTake = Option.wrap(createTakeRegion(position, currentWaveformOffset, previousTrack))
}
recordingWorklet.onSaved = uuid => project.trackUserCreatedSample(uuid)
recordingWorklet.onSaved = uuid => {
project.trackUserCreatedSample(uuid)
editing.modify(() => {
fileBox.ifSome(oldFileBox => {
editing.modify(() => {
const newFileBox = AudioFileBox.create(boxGraph, uuid, box => {
box.fileName.setValue(oldFileBox.fileName.getValue())
box.startInSeconds.setValue(oldFileBox.startInSeconds.getValue())
box.endInSeconds.setValue(oldFileBox.endInSeconds.getValue())
})
for (const pointer of [...oldFileBox.pointerHub.incoming()]) {
pointer.refer(newFileBox)
}
for (const pointer of [...oldFileBox.transientMarkers.pointerHub.incoming()]) {
pointer.refer(newFileBox.transientMarkers)
}
oldFileBox.delete()
})
})
})
}
terminator.ownAll(
Terminable.create(() => {
tryCatch(() => sourceNode.disconnect(recordingWorklet))
+27 -3
View File
@@ -46,6 +46,7 @@ export namespace RecordMidi {
const {loopArea} = timelineBox
const terminator = new Terminator()
const activeNotes = new Map<byte, ActiveNote>()
const pendingNotes = new Map<byte, byte>()
const latency = PPQN.secondsToPulses(audioContext.outputLatency ?? 10.0, timelineBox.bpm.getValue())
let currentTake: Option<TakeData> = Option.None
let lastPosition: ppqn = 0
@@ -108,6 +109,20 @@ export namespace RecordMidi {
}
}
const flushPendingNotes = (take: TakeData) => {
for (const [pitch, velocity] of pendingNotes) {
const event = NoteEventBox.create(boxGraph, UUID.generate(), box => {
box.position.setValue(0)
box.duration.setValue(MIN_NOTE_DURATION)
box.pitch.setValue(pitch)
box.velocity.setValue(velocity)
box.events.refer(take.collection.events)
})
activeNotes.set(pitch, {event, take, creationOffset: positionOffset})
}
pendingNotes.clear()
}
const startNewTake = (position: ppqn) => {
const previousTrack = currentTake.mapOr(take => take.trackBox, null)
currentTake = Option.wrap(createTakeRegion(position, previousTrack))
@@ -135,7 +150,9 @@ export namespace RecordMidi {
if (currentTake.isEmpty()) {
editing.modify(() => {
const pos = quantizeFloor(currentPosition, beats)
currentTake = Option.wrap(createTakeRegion(pos, null))
const take = createTakeRegion(pos, null)
currentTake = Option.wrap(take)
flushPendingNotes(take)
}, false)
}
currentTake.ifSome(({regionBox, collection}) => {
@@ -166,7 +183,10 @@ export namespace RecordMidi {
const writePosition = position.getValue() + latency
if (NoteSignal.isOn(signal)) {
const {pitch, velocity} = signal
if (currentTake.isEmpty()) {return}
if (currentTake.isEmpty()) {
pendingNotes.set(pitch, velocity)
return
}
const take = currentTake.unwrap()
const {regionBox, collection} = take
editing.modify(() => {
@@ -182,7 +202,11 @@ export namespace RecordMidi {
activeNotes.set(pitch, {event, take, creationOffset: positionOffset})
}, false)
} else if (NoteSignal.isOff(signal)) {
activeNotes.delete(signal.pitch)
if (pendingNotes.has(signal.pitch)) {
pendingNotes.delete(signal.pitch)
} else {
activeNotes.delete(signal.pitch)
}
}
}))
return terminator
@@ -2,11 +2,15 @@ import {SampleLoaderManager, SoundfontLoaderManager} from "@opendaw/studio-adapt
import {Editing, Func} from "@opendaw/lib-std"
import {BoxGraph} from "@opendaw/lib-box"
import {AudioWorklets} from "../AudioWorklets"
import {SampleService} from "../samples"
import {SoundfontService} from "../soundfont"
export interface ProjectEnv {
audioContext: AudioContext
audioWorklets: AudioWorklets
sampleManager: SampleLoaderManager
soundfontManager: SoundfontLoaderManager
sampleService: SampleService
soundfontService: SoundfontService
createEditing?: Func<BoxGraph, Editing>
}
@@ -1,6 +1,6 @@
import {ppqn, PPQN, TimeBase} from "@opendaw/lib-dsp"
import {ColorCodes, Sample, TrackType} from "@opendaw/studio-adapters"
import {int, isDefined, panic, quantizeCeil, UUID} from "@opendaw/lib-std"
import {int, isDefined, panic, quantizeRound, UUID} from "@opendaw/lib-std"
import {
AudioClipBox,
AudioFileBox,
@@ -25,6 +25,7 @@ export namespace AudioContentFactory {
warpMarkers?: ReadonlyArray<WarpMarkerTemplate>
waveformOffset?: number
gainInDb?: number
disableQuantize?: boolean
}
export type Clip = Props & { index: int }
@@ -43,14 +44,13 @@ export namespace AudioContentFactory {
* Calculates the duration of an audio region based on sample properties.
* Returns duration in PPQN for stretched regions, or in seconds for non-stretched.
*/
export const calculateDuration = (sample: Sample): ppqn => {
export const calculateDuration = (sample: Sample, disableQuantize: boolean = false): ppqn => {
const {duration: durationInSeconds, bpm} = sample
if (bpm === 0) {
// Non-stretched: duration is in seconds
return durationInSeconds
}
// Stretched: duration is in PPQN
return quantizeCeil(PPQN.secondsToPulses(durationInSeconds, bpm), PPQN.SemiQuaver)
const pulses = PPQN.secondsToPulses(durationInSeconds, bpm)
return disableQuantize ? pulses : quantizeRound(pulses, PPQN.SemiQuaver)
}
// --- Region Creation --- //
@@ -125,7 +125,8 @@ export namespace AudioContentFactory {
return panic("Cannot create audio-region on non-audio track")
}
const {name, duration: durationInSeconds, bpm} = sample
const durationInPPQN = props.duration ?? quantizeCeil(PPQN.secondsToPulses(durationInSeconds, bpm), PPQN.SemiQuaver)
const pulses = PPQN.secondsToPulses(durationInSeconds, bpm)
const durationInPPQN = props.duration ?? (!props.disableQuantize ? quantizeRound(pulses, PPQN.SemiQuaver) : pulses)
if (isDefined(props.warpMarkers)) {
AudioContentHelpers.addWarpMarkers(boxGraph, playMode, props.warpMarkers)
} else {
@@ -155,7 +156,8 @@ export namespace AudioContentFactory {
return panic("Cannot create audio-region on non-audio track")
}
const {name, duration: durationInSeconds, bpm} = sample
const durationInPPQN = props.duration ?? quantizeCeil(PPQN.secondsToPulses(durationInSeconds, bpm), PPQN.SemiQuaver)
const pulses = PPQN.secondsToPulses(durationInSeconds, bpm)
const durationInPPQN = props.duration ?? (!props.disableQuantize ? quantizeRound(pulses, PPQN.SemiQuaver) : pulses)
if (isDefined(props.warpMarkers)) {
AudioContentHelpers.addWarpMarkers(boxGraph, playMode, props.warpMarkers)
} else {
@@ -9,6 +9,13 @@ const isIntEncodedAsFloat = (v: number) =>
v > 0 && v < 1e-6 && Number.isFinite(v) && (v / 1.401298464324817e-45) % 1 === 0
export const migrateAudioClipBox = (boxGraph: BoxGraph<BoxIO.TypeMap>, box: AudioClipBox): void => {
if (box.file.targetVertex.isEmpty()) {
console.debug("Migrate 'AudioClipBox' remove clip without AudioFileBox")
boxGraph.beginTransaction()
box.delete()
boxGraph.endTransaction()
return
}
if (box.events.isEmpty()) {
console.debug("Migrate 'AudioClipBox' to have a ValueEventCollectionBox")
boxGraph.beginTransaction()
@@ -12,6 +12,13 @@ const toSeconds = (property: ValueOwner<ppqn>, bpm: number): seconds =>
PPQN.pulsesToSeconds(property.getValue(), bpm)
export const migrateAudioRegionBox = (boxGraph: BoxGraph<BoxIO.TypeMap>, box: AudioRegionBox, bpm: number): void => {
if (box.file.targetVertex.isEmpty()) {
console.debug("Migrate 'AudioRegionBox' remove region without AudioFileBox")
boxGraph.beginTransaction()
box.delete()
boxGraph.endTransaction()
return
}
const {duration, loopOffset, loopDuration, playback} = box
if (isIntEncodedAsFloat(duration.getValue())
|| isIntEncodedAsFloat(loopOffset.getValue())
@@ -1,4 +1,4 @@
import {Arrays, Class, isUndefined, Procedure, Progress, tryCatch, UUID} from "@opendaw/lib-std"
import {Arrays, Class, isDefined, Progress, tryCatch, UUID} from "@opendaw/lib-std"
import {Box} from "@opendaw/lib-box"
import {AudioData, estimateBpm} from "@opendaw/lib-dsp"
import {Promises} from "@opendaw/lib-runtime"
@@ -12,18 +12,27 @@ import {Workers} from "../Workers"
import {SampleStorage} from "./SampleStorage"
import {OpenSampleAPI} from "./OpenSampleAPI"
export class SampleService extends AssetService<Sample> {
export class SampleService extends AssetService<Sample, AudioData> {
protected readonly namePlural: string = "Samples"
protected readonly nameSingular: string = "Sample"
protected readonly boxType: Class<Box> = AudioFileBox
protected readonly filePickerOptions: FilePickerOptions = FilePickerAcceptTypes.WavFiles
constructor(readonly audioContext: AudioContext, onUpdate: Procedure<Sample>) {
super(onUpdate)
constructor(readonly audioContext: AudioContext) {super()}
async importRecording(audioData: AudioData, bpm: number, name: string = "Recording"): Promise<Sample> {
const arrayBuffer = WavFile.encodeFloats({
frames: audioData.frames.slice(),
numberOfFrames: audioData.numberOfFrames,
numberOfChannels: audioData.numberOfChannels,
sampleRate: audioData.sampleRate
})
return this.importFile({name, bpm, arrayBuffer, origin: "recording"})
}
async importFile({uuid, name, arrayBuffer, progressHandler = Progress.Empty}
: AssetService.ImportArgs): Promise<Sample> {
async importFile({uuid, name, bpm, arrayBuffer, progressHandler = Progress.Empty, origin = "import"}
: AssetService.ImportArgs,
transformMeta?: (meta: SampleMetaData, audioData: Readonly<AudioData>) => Promise<void>): Promise<Sample> {
console.debug(`importSample '${name}' (${arrayBuffer.byteLength >> 10}kb)`)
uuid ??= await UUID.sha256(arrayBuffer)
const audioData = await this.#decodeAudio(arrayBuffer)
@@ -36,15 +45,18 @@ export class SampleService extends AssetService<Sample> {
audioData.numberOfFrames,
audioData.numberOfChannels) as ArrayBuffer
const meta: SampleMetaData = {
bpm: estimateBpm(duration),
name: isUndefined(name) ? "Unnnamed" : name,
bpm: bpm ?? estimateBpm(duration),
name: name ?? "Unnnamed",
duration,
sample_rate: audioData.sampleRate,
origin: "import"
origin
}
if (isDefined(transformMeta)) {
await transformMeta(meta, audioData)
}
await SampleStorage.get().save({uuid, audio: audioData, peaks, meta})
const sample = {uuid: UUID.toString(uuid), ...meta}
this.onUpdate(sample)
await SampleStorage.get().save({uuid, audio: audioData, peaks, meta})
this.notifier.notify([sample, audioData])
return sample
}
@@ -1,4 +1,4 @@
import {Arrays, Class, Option, panic, Procedure, RuntimeNotifier, UUID} from "@opendaw/lib-std"
import {Arrays, Class, Option, panic, RuntimeNotifier, UUID} from "@opendaw/lib-std"
import {Box} from "@opendaw/lib-box"
import {Wait} from "@opendaw/lib-runtime"
import {SoundfontFileBox} from "@opendaw/studio-boxes"
@@ -9,7 +9,7 @@ import {OpenSoundfontAPI} from "./OpenSoundfontAPI"
import {AssetService} from "../AssetService"
import {ExternalLib} from "../ExternalLib"
export class SoundfontService extends AssetService<Soundfont> {
export class SoundfontService extends AssetService<Soundfont, void> {
protected readonly namePlural: string = "Soundfonts"
protected readonly nameSingular: string = "Soundfont"
protected readonly boxType: Class<Box> = SoundfontFileBox
@@ -18,9 +18,8 @@ export class SoundfontService extends AssetService<Soundfont> {
#local: Option<Array<Soundfont>> = Option.None
#remote: Option<ReadonlyArray<Soundfont>> = Option.None
constructor(onUpdate: Procedure<Soundfont>) {
super(onUpdate)
constructor() {
super()
Promise.all([
SoundfontStorage.get().list(),
OpenSoundfontAPI.get().all()
@@ -72,7 +71,7 @@ export class SoundfontService extends AssetService<Soundfont> {
if (!list.some(other => other.uuid === soundfont.uuid)) {
list.push(soundfont)
}
this.onUpdate(soundfont)
this.notifier.notify([soundfont, undefined])
updater.terminate()
return soundfont
}
+49 -32
View File
@@ -6,7 +6,7 @@ import {AudioFileBoxAdapter, AudioPlayMode} from "@opendaw/studio-adapters"
import {TimelineRange} from "../timeline/TimelineRange"
export namespace AudioRenderer {
type Segment = {
export type Segment = {
x0: number
x1: number
u0: number
@@ -14,27 +14,58 @@ export namespace AudioRenderer {
outside: boolean
}
export const render = (
context: CanvasRenderingContext2D,
range: TimelineRange,
file: AudioFileBoxAdapter,
tempoMap: TempoMap,
playMode: Option<AudioPlayMode>,
waveformOffset: number,
gain: number,
{top, bottom}: RegionBound,
contentColor: string,
{rawStart, resultStart, resultEnd}: LoopableRegion.LoopCycle,
clip: boolean = true
export interface Strategy {
render(context: CanvasRenderingContext2D,
segments: ReadonlyArray<Segment>,
peaks: Peaks,
bound: RegionBound,
gain: number): void
}
export const DefaultStrategy: Strategy = {
render(context, segments, peaks, {top, bottom}, gain): void {
const dpr = devicePixelRatio
const actualTop = top * dpr
const actualBottom = bottom * dpr
const height = actualBottom - actualTop
const numberOfChannels = peaks.numChannels
const peaksHeight = Math.floor((height - 4) / numberOfChannels)
const scale = dbToGain(-gain)
for (const {x0, x1, u0, u1, outside} of segments) {
context.globalAlpha = outside ? 0.25 : 1.0
for (let channel = 0; channel < numberOfChannels; channel++) {
PeaksPainter.renderPixelStrips(context, peaks, channel, {
u0,
u1,
v0: -scale,
v1: +scale,
x0,
x1,
y0: 3 + actualTop + channel * peaksHeight,
y1: 3 + actualTop + (channel + 1) * peaksHeight
})
}
}
}
}
export const render = (context: CanvasRenderingContext2D,
range: TimelineRange,
file: AudioFileBoxAdapter,
tempoMap: TempoMap,
playMode: Option<AudioPlayMode>,
waveformOffset: number,
gain: number,
bound: RegionBound,
contentColor: string,
{rawStart, resultStart, resultEnd}: LoopableRegion.LoopCycle,
clip: boolean = true,
strategy: Strategy = DefaultStrategy
) => {
if (file.peaks.isEmpty()) {return}
const peaks: Peaks = file.peaks.unwrap()
const durationInSeconds = file.endInSeconds - file.startInSeconds
const numFrames = peaks.numFrames
const numberOfChannels = peaks.numChannels
const ht = bottom - top
const peaksHeight = Math.floor((ht - 4) / numberOfChannels)
const scale = dbToGain(-gain)
const segments: Array<Segment> = []
if (playMode.nonEmpty()) {
const {warpMarkers} = playMode.unwrap()
@@ -357,20 +388,6 @@ export namespace AudioRenderer {
}
context.fillStyle = contentColor
for (const {x0, x1, u0, u1, outside} of segments) {
context.globalAlpha = outside && !clip ? 0.25 : 1.0
for (let channel = 0; channel < numberOfChannels; channel++) {
PeaksPainter.renderBlocks(context, peaks, channel, {
u0,
u1,
v0: -scale,
v1: +scale,
x0,
x1,
y0: 3 + top + channel * peaksHeight,
y1: 3 + top + (channel + 1) * peaksHeight
})
}
}
strategy.render(context, segments, peaks, bound, gain)
}
}
+23 -20
View File
@@ -11,6 +11,9 @@ export namespace AudioFadingRenderer {
startPPQN: number,
endPPQN: number,
color: string): void => {
const dpr = devicePixelRatio
const actualTop = top * dpr
const actualBottom = bottom * dpr
const {inSlope: fadeInSlope, outSlope: fadeOutSlope} = fading
const duration = endPPQN - startPPQN
const totalFading = fading.in + fading.out
@@ -19,46 +22,46 @@ export namespace AudioFadingRenderer {
const fadeOut = fading.out * scale
context.strokeStyle = color
context.fillStyle = "rgba(0,0,0,0.25)"
context.lineWidth = devicePixelRatio
context.lineWidth = dpr
if (fadeIn > 0) {
const fadeInEndPPQN = startPPQN + fadeIn
const x0 = range.unitToX(startPPQN) * devicePixelRatio
const x1 = range.unitToX(fadeInEndPPQN) * devicePixelRatio
const x0 = range.unitToX(startPPQN) * dpr
const x1 = range.unitToX(fadeInEndPPQN) * dpr
const xn = x1 - x0
const path = new Path2D()
path.moveTo(x0, bottom)
path.moveTo(x0, actualBottom)
let x = x0
Curve.run(fadeInSlope, xn, bottom, top, y => path.lineTo(++x, y))
path.lineTo(x1, top)
Curve.run(fadeInSlope, xn, actualBottom, actualTop, y => path.lineTo(++x, y))
path.lineTo(x1, actualTop)
context.stroke(path)
path.lineTo(x0, top)
path.lineTo(x0, bottom)
path.lineTo(x0, actualTop)
path.lineTo(x0, actualBottom)
context.fill(path)
}
if (fadeOut > 0) {
const x0 = range.unitToX(endPPQN - fadeOut) * devicePixelRatio
const x1 = range.unitToX(endPPQN) * devicePixelRatio
const x0 = range.unitToX(endPPQN - fadeOut) * dpr
const x1 = range.unitToX(endPPQN) * dpr
const xn = x1 - x0
const path = new Path2D()
path.moveTo(x0, top)
path.moveTo(x0, actualTop)
let x = x0
Curve.run(fadeOutSlope, xn, top, bottom, y => path.lineTo(++x, y))
path.lineTo(x1, bottom)
Curve.run(fadeOutSlope, xn, actualTop, actualBottom, y => path.lineTo(++x, y))
path.lineTo(x1, actualBottom)
context.strokeStyle = color
context.stroke(path)
path.lineTo(x1, top)
path.lineTo(x0, top)
path.lineTo(x1, actualTop)
path.lineTo(x0, actualTop)
context.fill(path)
}
const handleRadius = 1.5 * devicePixelRatio
const x0 = Math.max(range.unitToX(startPPQN + fadeIn), range.unitToX(startPPQN)) * devicePixelRatio
const x1 = Math.min(range.unitToX(endPPQN - fadeOut), range.unitToX(endPPQN)) * devicePixelRatio
const handleRadius = 1.5 * dpr
const x0 = Math.max(range.unitToX(startPPQN + fadeIn), range.unitToX(startPPQN)) * dpr
const x1 = Math.min(range.unitToX(endPPQN - fadeOut), range.unitToX(endPPQN)) * dpr
context.fillStyle = color
context.beginPath()
context.arc(x0, top, handleRadius, 0, TAU)
context.arc(x0, actualTop, handleRadius, 0, TAU)
context.fill()
context.beginPath()
context.arc(x1, top, handleRadius, 0, TAU)
context.arc(x1, actualTop, handleRadius, 0, TAU)
context.fill()
}
}
@@ -2,4 +2,5 @@ export * from "./audio"
export * from "./env"
export * from "./fading"
export * from "./notes"
export * from "./value"
export * from "./riffle"
export * from "./value"
@@ -9,7 +9,10 @@ export namespace NotesRenderer {
{top, bottom}: RegionBound,
contentColor: string,
{rawStart, regionStart, resultStart, resultEnd}: LoopableRegion.LoopCycle) => {
const height = bottom - top
const dpr = devicePixelRatio
const actualTop = top * dpr
const actualBottom = bottom * dpr
const height = actualBottom - actualTop
context.fillStyle = contentColor
const padding = 8
const noteHeight = 5
@@ -19,9 +22,9 @@ export namespace NotesRenderer {
const position = rawStart + note.position
if (position < regionStart) {continue}
const complete = Math.min(rawStart + note.complete, resultEnd)
const x0 = Math.floor(range.unitToX(position) * devicePixelRatio)
const x1 = Math.floor(range.unitToX(complete) * devicePixelRatio)
const y = top + padding + Math.floor(note.normalizedPitch() * (height - (padding * 2 + noteHeight)))
const x0 = Math.floor(range.unitToX(position) * dpr)
const x1 = Math.floor(range.unitToX(complete) * dpr)
const y = actualTop + padding + Math.floor(note.normalizedPitch() * (height - (padding * 2 + noteHeight)))
context.fillRect(x0, y, Math.max(1, x1 - x0), noteHeight)
}
}
@@ -0,0 +1,92 @@
import {dbToGain} from "@opendaw/lib-dsp"
import {Peaks} from "@opendaw/lib-fusion"
import {AudioRenderer} from "./audio"
export const RiffleStrategy: AudioRenderer.Strategy = {
render(context, segments, peaks, {top, bottom}, gain): void {
if (segments.length === 0) {return}
const dpr = devicePixelRatio
const actualTop = top * dpr
const actualBottom = bottom * dpr
const height = actualBottom - actualTop
const numberOfChannels = peaks.numChannels
const peaksHeight = Math.floor(height / numberOfChannels)
const gainScale = dbToGain(-gain)
const blockWidth = 3 * dpr
// noinspection PointlessArithmeticExpressionJS
const gap = dpr * 1
const pixelsPerBlock = blockWidth + gap
let globalX0 = Infinity
let globalX1 = -Infinity
for (const {x0, x1} of segments) {
globalX0 = Math.min(globalX0, Math.floor(x0))
globalX1 = Math.max(globalX1, Math.floor(x1))
}
if (globalX0 >= globalX1) {return}
const seg0 = segments[0]
const pxPerUnit = (seg0.x1 - seg0.x0) / (seg0.u1 - seg0.u0)
const anchorX = seg0.x0 - seg0.u0 * pxPerUnit
const gridPhase = ((globalX0 - anchorX) % pixelsPerBlock + pixelsPerBlock) % pixelsPerBlock
const firstBlockX = globalX0 - gridPhase
for (let channel = 0; channel < numberOfChannels; channel++) {
const data = peaks.data[channel]
const channelY0 = actualTop + channel * peaksHeight
const channelY1 = actualTop + (channel + 1) * peaksHeight
const centerY = (channelY0 + channelY1) / 2
const yScale = (channelY1 - channelY0 - 1.0) / (gainScale * 2)
for (let bx = firstBlockX; bx < globalX1; bx += pixelsPerBlock) {
const bxEnd = bx + pixelsPerBlock
let blockMin = 0.0
let blockMax = 0.0
let blockAlpha = 0.0
let hasData = false
for (const seg of segments) {
const segX0 = Math.floor(seg.x0)
const segX1 = Math.floor(seg.x1)
if (segX0 >= bxEnd || segX1 <= bx) {continue}
const pixelSpan = seg.x1 - seg.x0
if (pixelSpan <= 0) {continue}
const uPerPx = (seg.u1 - seg.u0) / pixelSpan
const stage = peaks.nearest(uPerPx)
if (stage === null) {continue}
const uPerPeak = stage.unitsEachPeak()
const peaksPerPx = uPerPx / uPerPeak
const overflow = seg.x0 - segX0
const fromAtX0 = (seg.u0 - overflow * uPerPx) / uPerPx * peaksPerPx
const overlapStart = Math.max(bx, segX0)
const overlapEnd = Math.min(bxEnd, segX1)
const fromPeak = fromAtX0 + (overlapStart - segX0) * peaksPerPx
const toPeak = fromAtX0 + (overlapEnd - segX0) * peaksPerPx
const idxFrom = Math.max(0, Math.floor(fromPeak))
const idxTo = Math.floor(toPeak)
for (let idx = idxFrom; idx < idxTo; idx++) {
const bits = data[stage.dataOffset + idx]
blockMin = Math.min(Peaks.unpack(bits, 0), blockMin)
blockMax = Math.max(Peaks.unpack(bits, 1), blockMax)
}
if (idxFrom < idxTo) {hasData = true}
blockAlpha = Math.max(blockAlpha, seg.outside ? 0.25 : 1.0)
}
if (!hasData) {continue}
context.globalAlpha = blockAlpha
const x = Math.max(bx, globalX0)
const w = Math.min(blockWidth, Math.min(bxEnd, globalX1) - x)
if (w <= 0) {continue}
const yMin = channelY0 + Math.floor((blockMin + gainScale) * yScale)
const yMax = channelY0 + Math.floor((blockMax + gainScale) * yScale)
const ry0 = Math.max(channelY0, Math.min(yMin, yMax))
const ry1 = Math.min(channelY1, Math.max(yMin, yMax))
const finalY1 = ry0 === ry1 ? ry0 + 1 : ry1
const maxDist = Math.max(centerY - ry0, finalY1 - centerY)
const symY0 = centerY - maxDist
const symY1 = centerY + maxDist
const h = symY1 - symY0
const r = Math.min(dpr, w / 2, h / 2)
context.beginPath()
context.roundRect(x, symY0, w, h, r)
context.fill()
}
}
context.globalAlpha = 1.0
}
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.68](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-enums
## [0.0.67](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-04)
**Note:** Version bump only for package @opendaw/studio-enums
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-enums",
"version": "0.0.67",
"version": "0.0.68",
"license": "LGPL-3.0-or-later",
"publishConfig": {
"access": "public"
@@ -22,7 +22,7 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-std": "^0.0.71"
"@opendaw/lib-std": "^0.0.72"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.83](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-forge-boxes
## [0.0.82](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-06)
**Note:** Version bump only for package @opendaw/studio-forge-boxes
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-forge-boxes",
"version": "0.0.82",
"version": "0.0.83",
"license": "LGPL-3.0-or-later",
"private": true,
"exports": {
@@ -14,11 +14,11 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.52](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-scripting
## [0.0.51](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-scripting
## [0.0.50](https://github.com/andremichelle/openDAW/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-scripting
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-scripting",
"version": "0.0.50",
"version": "0.0.52",
"license": "LGPL-3.0-or-later",
"type": "module",
"publishConfig": {
@@ -30,13 +30,13 @@
"test": "echo \"No tests to run\""
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-enums": "^0.0.68"
},
"devDependencies": {
"@opendaw/eslint-config": "^0.0.27",
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.128](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/studio-sdk
## [0.0.127](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-12)
**Note:** Version bump only for package @opendaw/studio-sdk
## [0.0.126](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-sdk
## [0.0.125](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-11)
**Note:** Version bump only for package @opendaw/studio-sdk
## [0.0.124](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-10)
**Note:** Version bump only for package @opendaw/studio-sdk
## [0.0.123](https://github.com/andremichelle/opendaw/compare/@opendaw/[email protected]...@opendaw/[email protected]) (2026-03-07)
**Note:** Version bump only for package @opendaw/studio-sdk
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@opendaw/studio-sdk",
"version": "0.0.123",
"version": "0.0.128",
"license": "LGPL-3.0-or-later",
"type": "module",
"publishConfig": {
@@ -42,20 +42,20 @@
"@opendaw/typescript-config": "^0.0.29"
},
"dependencies": {
"@opendaw/lib-box": "^0.0.77",
"@opendaw/lib-dawproject": "^0.0.62",
"@opendaw/lib-dom": "^0.0.76",
"@opendaw/lib-dsp": "^0.0.76",
"@opendaw/lib-fusion": "^0.0.83",
"@opendaw/lib-jsx": "^0.0.76",
"@opendaw/lib-midi": "^0.0.58",
"@opendaw/lib-runtime": "^0.0.72",
"@opendaw/lib-std": "^0.0.71",
"@opendaw/lib-xml": "^0.0.57",
"@opendaw/studio-adapters": "^0.0.97",
"@opendaw/studio-boxes": "^0.0.82",
"@opendaw/studio-core": "^0.0.121",
"@opendaw/studio-enums": "^0.0.67"
"@opendaw/lib-box": "^0.0.78",
"@opendaw/lib-dawproject": "^0.0.63",
"@opendaw/lib-dom": "^0.0.77",
"@opendaw/lib-dsp": "^0.0.77",
"@opendaw/lib-fusion": "^0.0.85",
"@opendaw/lib-jsx": "^0.0.77",
"@opendaw/lib-midi": "^0.0.59",
"@opendaw/lib-runtime": "^0.0.73",
"@opendaw/lib-std": "^0.0.72",
"@opendaw/lib-xml": "^0.0.58",
"@opendaw/studio-adapters": "^0.0.99",
"@opendaw/studio-boxes": "^0.0.83",
"@opendaw/studio-core": "^0.0.126",
"@opendaw/studio-enums": "^0.0.68"
},
"gitHead": "e4f70170c5947610932306da94e7f0bb817e43b3"
}
+1 -1
View File
@@ -2,4 +2,4 @@
* The current version of the OpenDAW SDK.
* This value is automatically synchronized with package.json during publish.
*/
export const OPENDAW_SDK_VERSION = "0.0.123"
export const OPENDAW_SDK_VERSION = "0.0.128"