├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── index.d.ts
├── index.js
├── index.js.map
├── index.min.js
├── index.min.js.LICENSE.txt
├── index.min.js.map
├── libfaust-wasm.data
└── libfaust-wasm.wasm
├── package-lock.json
├── package.json
├── src
├── Faust.ts
├── FaustAudioWorkletNode.ts
├── FaustAudioWorkletProcessor.ts
├── FaustOfflineProcessor.ts
├── FaustWasmToScriptProcessor.ts
├── LibFaustLoader.d.ts
├── LibFaustLoader.js
├── index.ts
├── libfaust-wasm.js
├── types.d.ts
├── utils.ts
├── wasm.d.ts
└── wasm
│ └── mixer32.wasm
├── test
├── mono.html
├── plot.html
├── poly-key.html
├── poly.html
├── test.html
├── wmono.html
└── wpoly.html
├── tsconfig.json
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | local
4 | src/libfaust-wasm.js
5 | webpack.config.js
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "settings": {
7 | "import/resolver": {
8 | "node": {
9 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".d.ts"]
10 | }
11 | }
12 | },
13 | "extends": [
14 | "airbnb-base",
15 | "plugin:@typescript-eslint/recommended"
16 | ],
17 | "globals": {
18 | "Atomics": "readonly",
19 | "SharedArrayBuffer": "readonly"
20 | },
21 | "parserOptions": {
22 | "ecmaVersion": 2018,
23 | "sourceType": "module",
24 | "jsx": true,
25 | "useJSXTextNode": true
26 | },
27 | "ignorePatterns": ["lib.*.d.ts"],
28 | "rules": {
29 | "array-callback-return": "warn",
30 | "arrow-body-style": "off",
31 | "arrow-parens": ["warn", "as-needed", { "requireForBlockBody": true }],
32 | "class-methods-use-this": "off",
33 | "comma-dangle": ["error", "never"],
34 | "curly": ["warn", "multi-line"],
35 | "default-case": ["warn", { "commentPattern": "^no default$" }],
36 | "dot-location": ["warn", "property"],
37 | "eqeqeq": ["warn", "smart"],
38 | "getter-return": "warn",
39 | "guard-for-in": "off",
40 | "import/extensions": ["error", "never"],
41 | "import/first": "error",
42 | "import/newline-after-import": "warn",
43 | "import/no-amd": "error",
44 | "import/no-webpack-loader-syntax": "error",
45 | "import/no-extraneous-dependencies": "off",
46 | "import/prefer-default-export": "off",
47 | "indent": ["error", 4, { "SwitchCase": 1 }],
48 | "jsx-quotes": ["error", "prefer-double"],
49 | "lines-between-class-members": "off",
50 | "max-classes-per-file": "off",
51 | "max-len": "off",
52 | "new-parens": "warn",
53 | "no-array-constructor": "off",
54 | "no-await-in-loop": "off",
55 | "no-bitwise": "off",
56 | "no-caller": "warn",
57 | "no-cond-assign": ["warn", "except-parens"],
58 | "no-const-assign": "warn",
59 | "no-control-regex": "warn",
60 | "no-continue": "off",
61 | "no-delete-var": "warn",
62 | "no-dupe-args": "warn",
63 | "no-dupe-class-members": "off",
64 | "no-dupe-keys": "warn",
65 | "no-duplicate-case": "warn",
66 | "no-empty": ["warn", { "allowEmptyCatch": true }],
67 | "no-empty-character-class": "warn",
68 | "no-empty-pattern": "warn",
69 | "no-eval": "warn",
70 | "no-ex-assign": "warn",
71 | "no-extend-native": "warn",
72 | "no-extra-bind": "warn",
73 | "no-extra-label": "warn",
74 | "no-fallthrough": "warn",
75 | "no-func-assign": "warn",
76 | "no-implied-eval": "warn",
77 | "no-invalid-regexp": "warn",
78 | "no-iterator": "warn",
79 | "no-lonely-if": "off",
80 | "no-label-var": "warn",
81 | "no-labels": ["warn", { "allowLoop": true, "allowSwitch": false }],
82 | "no-lone-blocks": "warn",
83 | "no-loop-func": "off",
84 | "no-mixed-operators": "off",
85 | "no-multi-str": "warn",
86 | "no-native-reassign": "warn",
87 | "no-negated-in-lhs": "warn",
88 | "no-nested-ternary": "off",
89 | "no-new-func": "warn",
90 | "no-new-object": "warn",
91 | "no-new-symbol": "warn",
92 | "no-new-wrappers": "warn",
93 | "no-obj-calls": "warn",
94 | "no-octal": "warn",
95 | "no-octal-escape": "warn",
96 | "no-param-reassign": ["error", { "props": false }],
97 | "no-plusplus": "off",
98 | "no-prototype-builtins": "off",
99 | "no-redeclare": ["warn", { "builtinGlobals": false }],
100 | "no-regex-spaces": "warn",
101 | "no-restricted-globals": "off",
102 | "no-restricted-properties": [
103 | "error",
104 | {
105 | "object": "require",
106 | "property": "ensure",
107 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
108 | },
109 | {
110 | "object": "System",
111 | "property": "import",
112 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
113 | }
114 | ],
115 | "no-restricted-syntax": [
116 | "error",
117 | {
118 | "selector": "LabeledStatement",
119 | "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
120 | },
121 | {
122 | "selector": "WithStatement",
123 | "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
124 | }
125 | ],
126 | "no-return-assign": "off",
127 | "no-shadow": "off",
128 | "no-script-url": "warn",
129 | "no-self-assign": "warn",
130 | "no-self-compare": "warn",
131 | "no-sequences": "warn",
132 | "no-shadow-restricted-names": "warn",
133 | "no-sparse-arrays": "off",
134 | "no-template-curly-in-string": "warn",
135 | "no-this-before-super": "warn",
136 | "no-throw-literal": "warn",
137 | "no-trailing-spaces": "warn",
138 | "no-undef": "off",
139 | "no-underscore-dangle": "off",
140 | "no-use-before-define": "off",
141 | "no-unreachable": "warn",
142 | "no-unused-labels": "warn",
143 | "no-unused-vars": "off",
144 | "no-useless-computed-key": "warn",
145 | "no-useless-concat": "warn",
146 | "no-useless-constructor": "off",
147 | "no-useless-escape": "warn",
148 | "no-useless-rename": [
149 | "warn",
150 | {
151 | "ignoreDestructuring": false,
152 | "ignoreImport": false,
153 | "ignoreExport": false
154 | }
155 | ],
156 | "no-with": "warn",
157 | "no-whitespace-before-property": "warn",
158 | "object-curly-newline": ["error", {
159 | "ObjectExpression": { "multiline": true, "consistent": true },
160 | "ObjectPattern": { "multiline": true, "consistent": true },
161 | "ImportDeclaration": { "multiline": true, "consistent": true },
162 | "ExportDeclaration": { "multiline": true, "consistent": true }
163 | }],
164 | "prefer-destructuring": "off",
165 | "prefer-template":"off",
166 | "quotes": ["error", "double", { "avoidEscape": true }],
167 | "semi": "off",
168 | "spaced-comment": [2, "always", { "markers": ["/"] }],
169 | "radix": "off",
170 | "require-yield": "warn",
171 | "rest-spread-spacing": ["warn", "never"],
172 | "strict": ["warn", "never"],
173 | "unicode-bom": ["warn", "never"],
174 | "use-isnan": "warn",
175 | "valid-typeof": "warn",
176 | "@typescript-eslint/ban-types": "off",
177 | "@typescript-eslint/class-name-casing": "off",
178 | "@typescript-eslint/consistent-type-assertions": "warn",
179 | "@typescript-eslint/explicit-function-return-type": "off",
180 | "@typescript-eslint/explicit-member-accessibility": "off",
181 | "@typescript-eslint/explicit-module-boundary-types": "off",
182 | "@typescript-eslint/no-empty-function": "off",
183 | "@typescript-eslint/no-empty-interface": "off",
184 | "@typescript-eslint/no-explicit-any": "off",
185 | "@typescript-eslint/prefer-interface": "off",
186 | "@typescript-eslint/no-array-constructor": "warn",
187 | "@typescript-eslint/no-namespace": "error",
188 | "@typescript-eslint/no-use-before-define": [
189 | "warn",
190 | {
191 | "functions": false,
192 | "classes": false,
193 | "variables": false,
194 | "typedefs": false
195 | }
196 | ],
197 | "@typescript-eslint/no-unused-expressions": [
198 | "error",
199 | {
200 | "allowShortCircuit": true,
201 | "allowTernary": true,
202 | "allowTaggedTemplates": true
203 | }
204 | ],
205 | "@typescript-eslint/no-unused-vars": [
206 | "warn",
207 | {
208 | "args": "none",
209 | "ignoreRestSiblings": true
210 | }
211 | ],
212 | "@typescript-eslint/no-useless-constructor": "warn",
213 | "@typescript-eslint/semi": ["error", "always"]
214 | },
215 | "parser": "@typescript-eslint/parser",
216 | "plugins": ["@typescript-eslint"]
217 | }
218 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directories
27 | node_modules
28 | jspm_packages
29 |
30 | # Optional npm cache directory
31 | .npm
32 |
33 | # Optional REPL history
34 | .node_repl_history
35 |
36 | # =========================
37 | # Operating System Files
38 | # =========================
39 |
40 | # OSX
41 | # =========================
42 |
43 | .DS_Store
44 | .AppleDouble
45 | .LSOverride
46 |
47 | # Thumbnails
48 | ._*
49 |
50 | # Files that might appear in the root of a volume
51 | .DocumentRevisions-V100
52 | .fseventsd
53 | .Spotlight-V100
54 | .TemporaryItems
55 | .Trashes
56 | .VolumeIcon.icns
57 |
58 | # Directories potentially created on remote AFP share
59 | .AppleDB
60 | .AppleDesktop
61 | Network Trash Folder
62 | Temporary Items
63 | .apdisk
64 |
65 | # Windows
66 | # =========================
67 |
68 | # Windows image file caches
69 | Thumbs.db
70 | ehthumbs.db
71 |
72 | # Folder config file
73 | Desktop.ini
74 |
75 | # Recycle Bin used on file shares
76 | $RECYCLE.BIN/
77 |
78 | # Windows Installer files
79 | *.cab
80 | *.msi
81 | *.msm
82 | *.msp
83 |
84 | # Windows shortcuts
85 | *.lnk
86 |
87 | # Local files
88 | local/
89 |
90 | .vscode/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Faust2WebAudio
2 |
3 | [**WARNING**] 01/19/2024: this project using the Faust [2.70.3 release](https://github.com/grame-cncm/faust/releases/tag/2.70.3) is now obsolete. Please use the [faustwasm](https://github.com/grame-cncm/faustwasm) project for new developments [**WARNING**].
4 |
5 | The repository is now maintained by [Grame-CNCM](https://github.com/grame-cncm/faust2webaudio). Please check that fork for the latest version.
6 |
7 | Produces a ScriptProcessorNode or an AudioWorkletNode with Faust .dsp code using the libfaust WebAssembly compiler.
8 |
9 | Supported Platforms: Chrome >= 49, Firefox >= 45, Edge >= 13, Safari >= 10, iOS >= 10, Android >= 68
10 |
11 | ## Testing
12 |
13 | Clone a copy of the repo:
14 |
15 | ```bash
16 | git clone https://github.com/grame-cncm/faust2webaudio
17 | ```
18 | Put the directory in a local server, then open following pages:
19 |
20 | For Mono ScriptProcessor: `./test/mono.html`
21 |
22 | For Poly ScriptProcessor: `./test/poly.html`
23 |
24 | For Mono AudioWorklet: `./test/wmono.html`
25 |
26 | For Poly AudioWorklet: `./test/wpoly.html`
27 |
28 | ## Installing as npm package
29 |
30 | ```bash
31 | npm install -D grame-cncm/faust2webaudio
32 | ```
33 |
34 | Example:
35 |
36 | ```JavaScript
37 | import { Faust } from "faust2webaudio";
38 |
39 | // Initialise Web Audio context
40 | const audioContext = new window.AudioContext();
41 |
42 | // Define Faust programs to run
43 | const monoCode = `
44 | import("stdfaust.lib");
45 | process = ba.pulsen(1, 10000) : pm.djembe(60, 0.3, 0.4, 1) <: dm.freeverb_demo;`;
46 |
47 | const polyCode = `
48 | import("stdfaust.lib");
49 | process = ba.pulsen(1, 10000) : pm.djembe(ba.hz2midikey(freq), 0.3, 0.4, 1) * gate * gain with {
50 | freq = hslider("freq", 440, 40, 8000, 1);
51 | gain = hslider("gain", 0.5, 0, 1, 0.01);
52 | gate = button("gate");
53 | };
54 | effect = dm.freeverb_demo;`;
55 |
56 | // Set up Faust compiler
57 | const faust = new Faust({
58 | // Update the below paths with the locations of the necessary files!
59 | // They can be found inside the Node module, under the 'dist' directory.
60 | wasmLocation: "path/to/libfaust-wasm.wasm",
61 | dataLocation: "path/to/libfaust-wasm.data"
62 | });
63 |
64 | // Ensure that the compiler is ready before continuing
65 | await faust.ready;
66 |
67 | // Compile monophonic code and connect the generated Web Audio node to the output.
68 | const monoNode = await faust.getNode(polyCode, {
69 | audioCtx: audioContext,
70 | useWorklet: window.AudioWorklet ? true : false,
71 | args: { "-I": "libraries/" }
72 | });
73 | monoNode.connect(audioContext.destination);
74 |
75 | // Compile polyphonic code and connect the generated Web Audio node to the output.
76 | const polyNode = await faust.getNode(polyCode, {
77 | audioCtx: audioContext,
78 | useWorklet: window.AudioWorklet ? true : false,
79 | voices: 4,
80 | args: { "-I": "libraries/" }
81 | });
82 | polyNode.connect(audioContext.destination);
83 | ```
84 |
85 | Windows users: Ensure that your copy of `libfaust-wasm.data` is terminated with LF line endings and not CRLF! If you have problems, try replacing it with a copy downloaded directly from [this repository](dist/libfaust-wasm.data).
86 |
87 | ## Building
88 |
89 | Firstly ensure that you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed.
90 |
91 | Clone a copy of the repo then change to the directory:
92 |
93 | ```bash
94 | git clone https://github.com/grame-cncm/faust2webaudio
95 | cd faust2webaudio
96 | ```
97 | Install dev dependencies:
98 |
99 | ```bash
100 | npm install
101 | ```
102 |
103 | Possibly:
104 |
105 | ```bash
106 | npm update
107 | ```
108 |
109 | To upgrade Libfaust version: replace `src/libfaust-wasm.js`, `dist/libfaust-wasm.wasm` and `dist/libfaust-wasm.data` with the new files.
110 |
111 | To build everything (using Webpack 4, Babel 7, TypeScript), this will produce `dist/index.js` and `dist/index.min.js` (this command must be done before commit + push on GitHub):
112 | ```bash
113 | npm run dist
114 | ```
115 |
116 | If you don't want to build the minified js for testing purpose:
117 | ```bash
118 | npm run build
119 | ```
120 | To test, put the directory in a local server, then open the following pages:
121 |
122 | For Mono ScriptProcessor: `./test/mono.html`
123 | For Poly ScriptProcessor: `./test/poly.html`
124 | For Mono AudioWorklet: `./test/wmono.html`
125 | For Poly AudioWorklet: `./test/wpoly.html`
126 |
127 | ## Versioning
128 |
129 | You'll have to raise the package version number in `package.json` for `npm run update` to properly work.
130 |
131 |
132 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | export { Faust } from "../src/Faust";
4 | export { FaustAudioWorkletNode } from "../src/FaustAudioWorkletNode";
5 | export { FaustScriptProcessorNode } from "../src/types";
6 |
--------------------------------------------------------------------------------
/dist/index.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /**
2 | * [js-sha256]{@link https://github.com/emn178/js-sha256}
3 | *
4 | * @version 0.9.0
5 | * @author Chen, Yi-Cyuan [emn178@gmail.com]
6 | * @copyright Chen, Yi-Cyuan 2014-2017
7 | * @license MIT
8 | */
9 |
--------------------------------------------------------------------------------
/dist/libfaust-wasm.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grame-cncm/faust2webaudio/bf7da8fc18f3b53f28e2401d48969c5118bb517b/dist/libfaust-wasm.wasm
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "faust2webaudio",
3 | "version": "0.6.81",
4 | "description": "Generate WebAudio Node from Faust code.",
5 | "main": "./dist/index.min.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "build": "webpack --mode development",
9 | "dist": "npm run build && webpack --mode production"
10 | },
11 | "author": "Grame-CNCM",
12 | "repository": "http://github.com/grame-cncm/faust2webaudio",
13 | "license": "GPL-2.0-or-later",
14 | "devDependencies": {
15 | "@types/emscripten": "^1.39.5",
16 | "@types/node": "^16.11.13",
17 | "@typescript-eslint/eslint-plugin": "^5.7.0",
18 | "@typescript-eslint/parser": "^5.7.0",
19 | "esbuild-loader": "^2.17.0",
20 | "eslint": "^8.4.1",
21 | "eslint-config-airbnb-base": "^15.0.0",
22 | "eslint-plugin-import": "^2.25.2",
23 | "js-sha256": "^0.9.0",
24 | "typescript": "^4.5.4",
25 | "url-loader": "^4.1.1",
26 | "webpack": "^5.65.0",
27 | "webpack-cli": "^4.9.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Faust.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { sha256 } from "js-sha256";
3 | import { LibFaustLoader, LibFaust } from "./LibFaustLoader";
4 | import { FaustWasmToScriptProcessor } from "./FaustWasmToScriptProcessor";
5 | import { FaustAudioWorkletProcessorWrapper } from "./FaustAudioWorkletProcessor";
6 | import { FaustAudioWorkletNode } from "./FaustAudioWorkletNode";
7 |
8 | import * as utils from "./utils";
9 | import { FaustOfflineProcessor } from "./FaustOfflineProcessor";
10 | import { TCompiledDsp, TFaustCompileOptions, FaustScriptProcessorNode, TFaustCompileArgs, TCompiledCode, TCompiledCodes, TAudioNodeOptions, TCompiledStrCodes } from "./types";
11 |
12 | // import * as Binaryen from "binaryen";
13 |
14 | /**
15 | * Main Faust class,
16 | * usage: `new Faust().ready.then(faust => any);`
17 | */
18 | export class Faust {
19 | /**
20 | * The libfaust Wasm Emscripten Module
21 | */
22 | private libFaust: LibFaust;
23 | private createWasmCDSPFactoryFromString: ($name: number, $code: number, argc: number, $argv: number, $errorMsg: number, internalMemory: boolean) => number;
24 | private deleteAllWasmCDSPFactories: () => void;
25 | private expandCDSPFromString: ($name: number, $code: number, argc: number, $argv: number, $shaKey: number, $errorMsg: number) => number;
26 | private getCLibFaustVersion: () => number;
27 | private getWasmCModule: ($moduleCode: number) => number;
28 | private getWasmCModuleSize: ($moduleCode: number) => number;
29 | private getWasmCHelpers: ($moduleCode: number) => number;
30 | private freeWasmCModule: ($moduleCode: number) => void;
31 | private freeCMemory: ($: number) => number;
32 | private cleanupAfterException: () => void;
33 | private getErrorAfterException: () => number;
34 | private getLibFaustVersion: () => string;
35 | private generateCAuxFilesFromString: ($name: number, $code: number, argc: number, $argv: number, $errorMsg: number) => number;
36 | /**
37 | * Debug mode, set to true to print out each message
38 | */
39 | debug = false;
40 | /**
41 | * An object to storage compiled dsp with it's sha1.
42 | */
43 | private dspTable: { [shaKey: string]: TCompiledDsp } = {};
44 | /**
45 | * Registered WorkletProcessor names
46 | */
47 | private workletProcessors: string[] = [];
48 | private _log: string[] = [];
49 | /**
50 | * Offline processor used to plot.
51 | */
52 | private offlineProcessor: FaustOfflineProcessor = new FaustOfflineProcessor();
53 | /**
54 | * Location of `libfaust-wasm.wasm`
55 | */
56 | private wasmLocation: string;
57 | /**
58 | * Location of `libfaust-wasm.data`
59 | */
60 | private dataLocation: string;
61 |
62 | /**
63 | * Creates an instance of Faust.
64 | * usage: `new Faust().ready.then(faust => any);`
65 | */
66 | constructor(options?: { debug?: boolean; wasmLocation?: string; dataLocation?: string }) {
67 | this.debug = !!(options && options.debug);
68 | this.wasmLocation = options.wasmLocation || "http://fr0stbyter.github.io/faust2webaudio/dist/libfaust-wasm.wasm";
69 | this.dataLocation = options.dataLocation || "http://fr0stbyter.github.io/faust2webaudio/dist/libfaust-wasm.data";
70 | }
71 | /**
72 | * Load a libfaust module.
73 | *
74 | * @returns {Promise}
75 | * @memberof Faust
76 | */
77 | async loadLibFaust(): Promise {
78 | if (this.libFaust) return this;
79 | this.libFaust = await LibFaustLoader.load(this.wasmLocation, this.dataLocation);
80 | this.importLibFaustFunctions();
81 | return this;
82 | }
83 | /**
84 | * A promise to resolve when libfaust is ready.
85 | */
86 | get ready(): Promise {
87 | return this.loadLibFaust();
88 | }
89 | private importLibFaustFunctions(): void {
90 | if (!this.libFaust) return;
91 | // Low-level API
92 | this.createWasmCDSPFactoryFromString = this.libFaust.cwrap("createWasmCDSPFactoryFromString", "number", ["number", "number", "number", "number", "number", "number"]);
93 | this.deleteAllWasmCDSPFactories = this.libFaust.cwrap("deleteAllWasmCDSPFactories", null, []);
94 | this.expandCDSPFromString = this.libFaust.cwrap("expandCDSPFromString", "number", ["number", "number", "number", "number", "number", "number"]);
95 | this.getCLibFaustVersion = this.libFaust.cwrap("getCLibFaustVersion", "number", []);
96 | this.getWasmCModule = this.libFaust.cwrap("getWasmCModule", "number", ["number"]);
97 | this.getWasmCModuleSize = this.libFaust.cwrap("getWasmCModuleSize", "number", ["number"]);
98 | this.getWasmCHelpers = this.libFaust.cwrap("getWasmCHelpers", "number", ["number"]);
99 | this.freeWasmCModule = this.libFaust.cwrap("freeWasmCModule", null, ["number"]);
100 | this.freeCMemory = this.libFaust.cwrap("freeCMemory", null, ["number"]);
101 | this.cleanupAfterException = this.libFaust.cwrap("cleanupAfterException", null, []);
102 | this.getErrorAfterException = this.libFaust.cwrap("getErrorAfterException", "number", []);
103 | this.getLibFaustVersion = () => this.libFaust.UTF8ToString(this.getCLibFaustVersion());
104 | this.generateCAuxFilesFromString = this.libFaust.cwrap("generateCAuxFilesFromString", "number", ["number", "number", "number", "number", "number"]);
105 | }
106 | /**
107 | * Create a AudioNode from dsp source code with options.
108 | *
109 | * @param {string} code - the source code
110 | * @param {TFaustCompileOptions} optionsIn - options with audioCtx, bufferSize, voices, useWorklet, args, plot and plotHandler
111 | */
112 | async getNode(code: string, optionsIn: TFaustCompileOptions): Promise {
113 | const { audioCtx, voices, useWorklet, bufferSize, plotHandler, args } = optionsIn;
114 | const argv = utils.toArgv(args);
115 | const compiledDsp = await this.compileCodes(code, argv, !voices);
116 | if (!compiledDsp) return null;
117 | const options = { compiledDsp, audioCtx, voices, plotHandler, bufferSize: useWorklet ? 128 : bufferSize };
118 | return useWorklet ? this.getAudioWorkletNode(options) : this.getScriptProcessorNode(options);
119 | }
120 | /**
121 | * Get DSP information.
122 | */
123 | async inspect(code: string, optionsIn: { voices?: number; args?: TFaustCompileArgs }): Promise {
124 | const { voices, args } = optionsIn;
125 | const argv = utils.toArgv(args);
126 | return this.compileCodes(code, argv, !voices);
127 | }
128 | /**
129 | * Plot a dsp offline.
130 | */
131 | async plot(optionsIn?: { code?: string; size?: number; sampleRate?: number; args?: TFaustCompileArgs }): Promise {
132 | let compiledDsp;
133 | const argv = utils.toArgv(optionsIn.args);
134 | if (optionsIn.code) {
135 | compiledDsp = await this.compileCodes(optionsIn.code, argv, true);
136 | if (!compiledDsp) return null;
137 | }
138 | return this.offlineProcessor.plot({ compiledDsp, ...optionsIn });
139 | }
140 | /**
141 | * compileCode
142 | * Generate Uint8Array and helpersCode from a dsp source code
143 | *
144 | * @param {string} factoryName - Class name of the source code
145 | * @param {string} code - dsp source code
146 | * @param {string[]} argvIn - Array of paramaters to be given to the Faust compiler
147 | * @param {boolean} internalMemory - Use internal Memory flag, false for poly, true for mono
148 | * @returns {TCompiledCode} - An object with ui8Code, code, helpersCode
149 | */
150 | private compileCode(factoryName: string, code: string, argvIn: string[], internalMemory: boolean): TCompiledCode {
151 | const codeSize = this.libFaust.lengthBytesUTF8(code) + 1;
152 | const $code = this.libFaust._malloc(codeSize);
153 | const name = "FaustDSP";
154 | const nameSize = this.libFaust.lengthBytesUTF8(name) + 1;
155 | const $name = this.libFaust._malloc(nameSize);
156 | const $errorMsg = this.libFaust._malloc(4096);
157 |
158 | this.libFaust.stringToUTF8(name, $name, nameSize);
159 | this.libFaust.stringToUTF8(code, $code, codeSize);
160 |
161 | // Add 'cn' option with the factory name
162 | const argv = argvIn || [];
163 | argv.push("-cn", factoryName);
164 |
165 | // Prepare 'argv_aux' array for C side
166 | const ptrSize = 4;
167 | const $argv = this.libFaust._malloc(argv.length * ptrSize); // Get buffer from emscripten.
168 | let argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length); // Get a integer view on the newly allocated buffer.
169 | for (let i = 0; i < argv.length; i++) {
170 | const size$arg = this.libFaust.lengthBytesUTF8(argv[i]) + 1;
171 | const $arg = this.libFaust._malloc(size$arg);
172 | this.libFaust.stringToUTF8(argv[i], $arg, size$arg);
173 | argvBuffer$[i] = $arg;
174 | }
175 | try {
176 | const time1 = performance.now();
177 | const $moduleCode = this.createWasmCDSPFactoryFromString($name, $code, argv.length, $argv, $errorMsg, internalMemory);
178 | const time2 = performance.now();
179 | this.log("Faust compilation duration : " + (time2 - time1));
180 | const errorMsg = this.libFaust.UTF8ToString($errorMsg);
181 | if (errorMsg) throw new Error(errorMsg);
182 |
183 | if ($moduleCode === 0) return null;
184 | const $compiledCode = this.getWasmCModule($moduleCode);
185 | const compiledCodeSize = this.getWasmCModuleSize($moduleCode);
186 | // Copy native 'binary' string in JavaScript Uint8Array
187 | const ui8Code = new Uint8Array(compiledCodeSize);
188 | for (let i = 0; i < compiledCodeSize; i++) {
189 | // faster than 'getValue' which gets the type of access for each read...
190 | ui8Code[i] = this.libFaust.HEAP8[$compiledCode + i];
191 | }
192 | const $helpersCode = this.getWasmCHelpers($moduleCode);
193 | const helpersCode = this.libFaust.UTF8ToString($helpersCode);
194 | // Free strings
195 | this.libFaust._free($code);
196 | this.libFaust._free($name);
197 | this.libFaust._free($errorMsg);
198 | // Free C allocated wasm module
199 | this.freeWasmCModule($moduleCode);
200 | // Get an updated integer view on the newly allocated buffer after possible emscripten memory grow
201 | argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length);
202 | // Free 'argv' C side array
203 | for (let i = 0; i < argv.length; i++) {
204 | this.libFaust._free(argvBuffer$[i]);
205 | }
206 | this.libFaust._free($argv);
207 | return { ui8Code, code, helpersCode };
208 | } catch (e) {
209 | // libfaust is compiled without C++ exception activated, so a JS exception is throwed and catched here
210 | const errorMsg = this.libFaust.UTF8ToString(this.getErrorAfterException());
211 | this.cleanupAfterException();
212 | // Report the Emscripten error
213 | throw errorMsg ? new Error(errorMsg) : e;
214 | }
215 | }
216 | /**
217 | * compileCodes
218 | * Generate shaKey, effects, dsp, their Wasm Modules and helpers from a dsp source code
219 | *
220 | * @param {string} code - dsp source code
221 | * @param {string[]} argv - Array of paramaters to be given to the Faust compiler
222 | * @param {boolean} internalMemory - Use internal Memory flag, false for poly, true for mono
223 | * @returns {Promise} - An object contains shaKey, empty polyphony map, original codes, modules and helpers
224 | */
225 | private async compileCodes(code: string, argv: string[], internalMemory: boolean): Promise {
226 | // Code memory type and argv in the SHAKey to differentiate compilation flags and Monophonic and Polyphonic factories
227 | const strArgv = argv.join("");
228 | // const shaKey = sha256(this.expandCode(code, argv) + (internalMemory ? "internal_memory" : "external_memory") + strArgv);
229 | // Use the original code to compute the shaKey
230 | const shaKey = sha256(code + (internalMemory ? "internal_memory" : "external_memory") + strArgv);
231 | const compiledDsp = this.dspTable[shaKey];
232 | if (compiledDsp) {
233 | this.log("Existing library : " + shaKey);
234 | // Existing factory, do not create it...
235 | return compiledDsp;
236 | }
237 | this.log("libfaust.js version : " + this.getLibFaustVersion());
238 | // Create 'effect' expression
239 | const effectCode = `adapt(1,1) = _; adapt(2,2) = _,_; adapt(1,2) = _ <: _,_; adapt(2,1) = _,_ :> _;
240 | adaptor(F,G) = adapt(outputs(F),inputs(G));
241 | dsp_code = environment{${code}};
242 | process = adaptor(dsp_code.process, dsp_code.effect) : dsp_code.effect;`;
243 | const dspCompiledCode = this.compileCode(shaKey, code, argv, internalMemory);
244 | let effectCompiledCode: TCompiledCode;
245 | try {
246 | effectCompiledCode = this.compileCode(shaKey + "_", effectCode, argv, internalMemory);
247 | } catch (e) { } // eslint-disable-line no-empty
248 | const compiledCodes = { dsp: dspCompiledCode, effect: effectCompiledCode };
249 | return this.compileDsp(compiledCodes, shaKey);
250 | }
251 | /**
252 | * expandCode
253 | * From a DSP source file, creates a "self-contained" DSP source string where all needed librairies have been included.
254 | * All compilations options are 'normalized' and included as a comment in the expanded string.
255 | *
256 | * @param {string} code - dsp source code
257 | * @param {TFaustCompileArgs | string[]} args - Paramaters to be given to the Faust compiler
258 | * @returns {string} "self-contained" DSP source string where all needed librairies
259 | */
260 | expandCode(code: string, args?: TFaustCompileArgs | string[]): string {
261 | this.log("libfaust.js version : " + this.getLibFaustVersion());
262 | // Allocate strings on the HEAP
263 | const codeSize = this.libFaust.lengthBytesUTF8(code) + 1;
264 | const $code = this.libFaust._malloc(codeSize);
265 |
266 | const name = "FaustDSP";
267 | const nameSize = this.libFaust.lengthBytesUTF8(name) + 1;
268 | const $name = this.libFaust._malloc(nameSize);
269 |
270 | const $shaKey = this.libFaust._malloc(64);
271 | const $errorMsg = this.libFaust._malloc(4096);
272 |
273 | this.libFaust.stringToUTF8(name, $name, nameSize);
274 | this.libFaust.stringToUTF8(code, $code, codeSize);
275 |
276 | const argvIn = args ? Array.isArray(args) ? args : utils.toArgv(args) : [];
277 | // Force "wasm" compilation
278 | const argv = [...argvIn, "-lang", "wasm"];
279 |
280 | // Prepare 'argv' array for C side
281 | const ptrSize = 4;
282 | const $argv = this.libFaust._malloc(argv.length * ptrSize); // Get buffer from emscripten.
283 | let argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length); // Get a integer view on the newly allocated buffer.
284 | for (let i = 0; i < argv.length; i++) {
285 | const size$arg = this.libFaust.lengthBytesUTF8(argv[i]) + 1;
286 | const $arg = this.libFaust._malloc(size$arg);
287 | this.libFaust.stringToUTF8(argv[i], $arg, size$arg);
288 | argvBuffer$[i] = $arg;
289 | }
290 | try {
291 | const $expandedCode = this.expandCDSPFromString($name, $code, argv.length, $argv, $shaKey, $errorMsg);
292 | const expandedCode = this.libFaust.UTF8ToString($expandedCode);
293 | const errorMsg = this.libFaust.UTF8ToString($errorMsg);
294 | if (errorMsg) this.error(errorMsg);
295 | // Free strings
296 | this.libFaust._free($code);
297 | this.libFaust._free($name);
298 | this.libFaust._free($shaKey);
299 | this.libFaust._free($errorMsg);
300 | // Free C allocated expanded string
301 | this.freeCMemory($expandedCode);
302 | // Get an updated integer view on the newly allocated buffer after possible emscripten memory grow
303 | argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length);
304 | // Free 'argv' C side array
305 | for (let i = 0; i < argv.length; i++) {
306 | this.libFaust._free(argvBuffer$[i]);
307 | }
308 | this.libFaust._free($argv);
309 | return expandedCode;
310 | } catch (e) {
311 | // libfaust is compiled without C++ exception activated, so a JS exception is throwed and catched here
312 | const errorMsg = this.libFaust.UTF8ToString(this.getErrorAfterException());
313 | this.cleanupAfterException();
314 | // Report the Emscripten error
315 | throw errorMsg ? new Error(errorMsg) : e;
316 | }
317 | }
318 | /**
319 | * compileDsp
320 | * Compile wasm modules from dsp and effect Uint8Arrays.
321 | *
322 | * @param {TCompiledCodes} codes
323 | * @param {string} shaKey
324 | * @returns {Promise}
325 | */
326 | private async compileDsp(codes: TCompiledCodes, shaKey: string): Promise {
327 | const time1 = performance.now();
328 | /*
329 | if (typeof Binaryen !== "undefined") {
330 | try {
331 | const binaryenModule = Binaryen.readBinary(codes.dsp.ui8Code);
332 | this.log("Binaryen based optimisation");
333 | binaryenModule.optimize();
334 | // console.log(binaryen_module.emitText());
335 | codes.dsp.ui8Code = binaryenModule.emitBinary();
336 | binaryenModule.dispose();
337 | } catch (e) {
338 | this.log("Binaryen not available, no optimisation...");
339 | }
340 | }
341 | */
342 | const dspModule = await WebAssembly.compile(codes.dsp.ui8Code);
343 | if (!dspModule) {
344 | this.error("Faust DSP factory cannot be compiled");
345 | throw new Error("Faust DSP factory cannot be compiled");
346 | }
347 | const time2 = performance.now();
348 | this.log("WASM compilation duration : " + (time2 - time1));
349 | const compiledDsp: TCompiledDsp = { shaKey, codes, dspModule, dspMeta: undefined }; // Default mode
350 | // 'libfaust.js' wasm backend generates UI methods, then we compile the code
351 | // eval(helpers_code1);
352 | // factory.getJSON = eval("getJSON" + dspName);
353 | // factory.getBase64Code = eval("getBase64Code" + dspName);
354 | try {
355 | const json = codes.dsp.helpersCode.match(/getJSON\w+?\(\)[\s\n]*{[\s\n]*return[\s\n]*'(\{.+?)';}/)[1].replace(/\\'/g, "'");
356 | // const base64Code = codes.dsp.helpersCode.match(/getBase64Code\w+?\(\)[\s\n]*{[\s\n]*return[\s\n]*"([A-Za-z0-9+/=]+?)";[\s\n]+}/)[1];
357 | const meta = JSON.parse(json);
358 | compiledDsp.dspMeta = meta;
359 | } catch (e) {
360 | this.error("Error in JSON.parse: " + e.message);
361 | throw e;
362 | }
363 | this.dspTable[shaKey] = compiledDsp;
364 | // Possibly compile effect
365 | if (!codes.effect) return compiledDsp;
366 | try {
367 | const effectModule = await WebAssembly.compile(codes.effect.ui8Code);
368 | compiledDsp.effectModule = effectModule;
369 | // 'libfaust.js' wasm backend generates UI methods, then we compile the code
370 | // eval(helpers_code2);
371 | // factory.getJSONeffect = eval("getJSON" + factory_name2);
372 | // factory.getBase64Codeeffect = eval("getBase64Code" + factory_name2);
373 | try {
374 | const json = codes.effect.helpersCode.match(/getJSON\w+?\(\)[\s\n]*{[\s\n]*return[\s\n]*'(\{.+?)';}/)[1].replace(/\\'/g, "'");
375 | // const base64Code = codes.effect.helpersCode.match(/getBase64Code\w+?\(\)[\s\n]*{[\s\n]*return[\s\n]*"([A-Za-z0-9+/=]+?)";[\s\n]+}/)[1];
376 | const meta = JSON.parse(json);
377 | compiledDsp.effectMeta = meta;
378 | } catch (e) {
379 | this.error("Error in JSON.parse: " + e.message);
380 | throw e;
381 | }
382 | } catch (e) {
383 | return compiledDsp;
384 | }
385 | return compiledDsp;
386 | }
387 | /**
388 | * Get a ScriptProcessorNode from compiled dsp
389 | *
390 | * @param {TCompiledDsp} compiledDsp - DSP compiled by libfaust
391 | * @param {TAudioNodeOptions} optionsIn
392 | */
393 | private async getScriptProcessorNode(optionsIn: TAudioNodeOptions): Promise {
394 | return new FaustWasmToScriptProcessor(this).getNode(optionsIn);
395 | }
396 |
397 | // deleteDSPInstance() {}
398 |
399 | /**
400 | * Get a AudioWorkletNode from compiled dsp
401 | */
402 | private async getAudioWorkletNode(optionsIn: TAudioNodeOptions): Promise {
403 | const { compiledDsp: compiledDspWithCodes, audioCtx, voices, plotHandler } = optionsIn;
404 | const compiledDsp = { ...compiledDspWithCodes };
405 | delete compiledDsp.codes;
406 | const id = compiledDsp.shaKey + "_" + voices;
407 | if (this.workletProcessors.indexOf(id) === -1) {
408 | const strProcessor = `
409 | const remap = ${utils.remap.toString()};
410 | const midiToFreq = ${utils.midiToFreq.toString()};
411 | const findPath = (${utils.findPathClosure.toString()})();
412 | const createWasmImport = ${utils.createWasmImport.toString()};
413 | const createWasmMemory = ${utils.createWasmMemory.toString()};
414 | const faustData = ${JSON.stringify({
415 | id,
416 | voices,
417 | dspMeta: compiledDsp.dspMeta,
418 | effectMeta: compiledDsp.effectMeta
419 | })};
420 | (${FaustAudioWorkletProcessorWrapper.toString()})();
421 | `;
422 | const url = window.URL.createObjectURL(new Blob([strProcessor], { type: "text/javascript" }));
423 | await audioCtx.audioWorklet.addModule(url);
424 | this.workletProcessors.push(id);
425 | }
426 | return new FaustAudioWorkletNode({ audioCtx, id, voices, compiledDsp, plotHandler, mixer32Module: utils.mixer32Module });
427 | }
428 | /**
429 | * Remove a DSP from registry
430 | */
431 | private deleteDsp(compiledDsp: TCompiledDsp): void {
432 | // The JS side is cleared
433 | delete this.dspTable[compiledDsp.shaKey];
434 | // The native C++ is cleared each time (freeWasmCModule has been already called in faust.compile)
435 | this.deleteAllWasmCDSPFactories();
436 | }
437 | /**
438 | * Stringify current storaged DSP Table.
439 | */
440 | stringifyDspTable(): string {
441 | const strTable: { [shaKey: string]: TCompiledStrCodes } = {};
442 | for (const key in this.dspTable) {
443 | const { codes } = this.dspTable[key];
444 | strTable[key] = {
445 | dsp: {
446 | strCode: btoa(utils.ab2str(codes.dsp.ui8Code)),
447 | code: codes.dsp.code,
448 | helpersCode: codes.dsp.helpersCode
449 | },
450 | effect: codes.effect ? {
451 | strCode: btoa(utils.ab2str(codes.effect.ui8Code)),
452 | code: codes.effect.code,
453 | helpersCode: codes.effect.helpersCode
454 | } : undefined
455 | };
456 | }
457 | return JSON.stringify(strTable);
458 | }
459 | /**
460 | * parse and store a stringified DSP Table.
461 | */
462 | parseDspTable(str: string) {
463 | const strTable = JSON.parse(str) as { [shaKey: string]: TCompiledStrCodes };
464 | for (const shaKey in strTable) {
465 | if (this.dspTable[shaKey]) continue;
466 | const strCodes = strTable[shaKey];
467 | const compiledCodes: TCompiledCodes = {
468 | dsp: {
469 | ui8Code: utils.str2ab(atob(strCodes.dsp.strCode)),
470 | code: strCodes.dsp.code,
471 | helpersCode: strCodes.dsp.helpersCode
472 | },
473 | effect: strCodes.effect ? {
474 | ui8Code: utils.str2ab(atob(strCodes.effect.strCode)),
475 | code: strCodes.effect.code,
476 | helpersCode: strCodes.effect.helpersCode
477 | } : undefined
478 | };
479 | this.compileDsp(compiledCodes, shaKey).then(dsp => this.dspTable[shaKey] = dsp);
480 | }
481 | }
482 | // deleteDSPWorkletInstance() {}
483 | /**
484 | * Get an SVG Diagram XML File as string
485 | *
486 | * @param {string} code faust source code
487 | * @param {TFaustCompileArgs} args - Paramaters to be given to the Faust compiler
488 | * @returns {string} svg file as string
489 | */
490 | getDiagram(code: string, args?: TFaustCompileArgs): string {
491 | const codeSize = this.libFaust.lengthBytesUTF8(code) + 1;
492 | const $code = this.libFaust._malloc(codeSize);
493 | const name = "FaustDSP";
494 | const nameSize = this.libFaust.lengthBytesUTF8(name) + 1;
495 | const $name = this.libFaust._malloc(nameSize);
496 | const $errorMsg = this.libFaust._malloc(4096);
497 |
498 | this.libFaust.stringToUTF8(name, $name, nameSize);
499 | this.libFaust.stringToUTF8(code, $code, codeSize);
500 | const argvIn = args ? utils.toArgv(args) : [];
501 | const argv = [...argvIn, "-lang", "wast", "-o", "/dev/null", "-svg"];
502 |
503 | // Prepare 'argv' array for C side
504 | const ptrSize = 4;
505 | const $argv = this.libFaust._malloc(argv.length * ptrSize); // Get buffer from emscripten.
506 | let argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length); // Get a integer view on the newly allocated buffer.
507 | for (let i = 0; i < argv.length; i++) {
508 | const size$arg = this.libFaust.lengthBytesUTF8(argv[i]) + 1;
509 | const $arg = this.libFaust._malloc(size$arg);
510 | this.libFaust.stringToUTF8(argv[i], $arg, size$arg);
511 | argvBuffer$[i] = $arg;
512 | }
513 | try {
514 | this.generateCAuxFilesFromString($name, $code, argv.length, $argv, $errorMsg);
515 | // Free strings
516 | this.libFaust._free($code);
517 | this.libFaust._free($name);
518 | this.libFaust._free($errorMsg);
519 | // Get an updated integer view on the newly allocated buffer after possible emscripten memory grow
520 | argvBuffer$ = new Int32Array(this.libFaust.HEAP32.buffer, $argv, argv.length);
521 | // Free 'argv' C side array
522 | for (let i = 0; i < argv.length; i++) {
523 | this.libFaust._free(argvBuffer$[i]);
524 | }
525 | this.libFaust._free($argv);
526 | } catch (e) {
527 | // libfaust is compiled without C++ exception activated, so a JS exception is throwed and catched here
528 | const errorMsg = this.libFaust.UTF8ToString(this.getErrorAfterException());
529 | this.cleanupAfterException();
530 | // Report the Emscripten error
531 | throw errorMsg ? new Error(errorMsg) : e;
532 | }
533 | return this.libFaust.FS.readFile("FaustDSP-svg/process.svg", { encoding: "utf8" }) as string;
534 | }
535 | /**
536 | * Expose LibFaust Emscripten Module File System
537 | *
538 | * @param {string} path path string
539 | * @returns Emscripten Module File System
540 | */
541 | get fs() {
542 | return this.libFaust.FS;
543 | }
544 | log(...args: any[]) {
545 | if (this.debug) console.log(...args);
546 | const msg = args.length === 1 && typeof args[0] === "string" ? args[0] : JSON.stringify(args.length !== 1 ? args : args[0]);
547 | this._log.push(msg);
548 | if (typeof this.logHandler === "function") this.logHandler(msg, 0);
549 | }
550 | error(...args: any[]) {
551 | console.error(...args);
552 | const msg = args.length === 1 && typeof args[0] === "string" ? args[0] : JSON.stringify(args.length !== 1 ? args : args[0]);
553 | this._log.push(msg);
554 | if (typeof this.logHandler === "function") this.logHandler(msg, 1);
555 | }
556 | logHandler: (msg: string, errorLevel: 1 | 0) => any;
557 | }
558 |
--------------------------------------------------------------------------------
/src/FaustAudioWorkletNode.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable object-curly-newline */
2 | /* eslint-disable object-property-newline */
3 | import { TDspMeta, TCompiledDsp, TFaustUI, TFaustUIGroup, TFaustUIItem } from "./types";
4 | import { remap } from "./utils";
5 |
6 | export class FaustAudioWorkletNode extends (window.AudioWorkletNode ? AudioWorkletNode : null) {
7 | onprocessorerror = (e: ErrorEvent) => {
8 | console.error("Error from " + this.dspMeta.name + " AudioWorkletNode: "); // eslint-disable-line no-console
9 | throw e.error;
10 | };
11 | /* WAP ??
12 | getMetadata = this.getJSON;
13 | setParam = this.setParamValue;
14 | getParam = this.getParamValue;
15 | inputChannelCount = this.getNumInputs;
16 | outputChannelCount = this.getNumOutputs;
17 | getParams = () => this.inputsItems;
18 | getDescriptor = this.getParams;
19 | onMidi = this.midiMessage;
20 | getState = async () => {
21 | const params = {} as { [key: string]: string };
22 | this.getDescriptor().forEach(key => params[key] = JSON.stringify(this.getParam(key)));
23 | return params;
24 | }
25 | setState = async (state: { [key: string]: number; }) => {
26 | for (const key in state) {
27 | this.setParam(key, state[key]);
28 | }
29 | try {
30 | this.gui.setAttribute("state", JSON.stringify(state));
31 | } catch (error) {
32 | console.warn("Plugin without gui or GUI not defined: ", error);
33 | }
34 | return state;
35 | }
36 | setPatch = (patch: any) => this.presets ? this.setState(this.presets[patch]) : undefined; // ??
37 | metadata = (handler: (...args: any[]) => any) => handler(null);
38 | gui: any;
39 | presets: any;
40 | */
41 |
42 | voices?: number;
43 | dspMeta: TDspMeta;
44 | effectMeta: TDspMeta;
45 | outputHandler: (address: string, value: number) => any;
46 | inputsItems: string[];
47 | outputsItems: string[];
48 |
49 | fPitchwheelLabel: { path: string; min: number; max: number }[];
50 | fCtrlLabel: { path: string; min: number; max: number }[][];
51 |
52 | plotHandler: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any;
53 |
54 | constructor(options: { audioCtx: AudioContext; id: string; compiledDsp: TCompiledDsp; voices?: number; plotHandler?: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any; mixer32Module: WebAssembly.Module }) {
55 | super(options.audioCtx, options.id, {
56 | numberOfInputs: options.compiledDsp.dspMeta.inputs > 0 ? 1 : 0,
57 | numberOfOutputs: options.compiledDsp.dspMeta.outputs > 0 ? 1 : 0,
58 | channelCount: Math.max(1, options.compiledDsp.dspMeta.inputs),
59 | outputChannelCount: [options.compiledDsp.dspMeta.outputs],
60 | channelCountMode: "explicit",
61 | channelInterpretation: "speakers",
62 | processorOptions: { id: options.id, voices: options.voices, compiledDsp: options.compiledDsp, mixer32Module: options.mixer32Module }
63 | });
64 | // Patch it with additional functions
65 | this.port.onmessage = (e: MessageEvent) => {
66 | if (e.data.type === "param" && this.outputHandler) {
67 | this.outputHandler(e.data.path, e.data.value);
68 | } else if (e.data.type === "plot") {
69 | if (this.plotHandler) this.plotHandler(e.data.value, e.data.index, e.data.events);
70 | }
71 | };
72 | this.voices = options.voices;
73 | this.dspMeta = options.compiledDsp.dspMeta;
74 | this.effectMeta = options.compiledDsp.effectMeta;
75 | this.outputHandler = null;
76 | this.inputsItems = [];
77 | this.outputsItems = [];
78 | this.fPitchwheelLabel = [];
79 | this.fCtrlLabel = new Array(128).fill(null).map(() => []);
80 | this.plotHandler = options.plotHandler;
81 | this.parseUI(this.dspMeta.ui);
82 | if (this.effectMeta) this.parseUI(this.effectMeta.ui);
83 | try {
84 | if (this.parameters) this.parameters.forEach(p => p.automationRate = "k-rate");
85 | } catch (e) {} // eslint-disable-line no-empty
86 | }
87 | parseUI(ui: TFaustUI) {
88 | ui.forEach(group => this.parseGroup(group));
89 | }
90 | parseGroup(group: TFaustUIGroup) {
91 | if (group.items) this.parseItems(group.items);
92 | }
93 | parseItems(items: TFaustUIItem[]) {
94 | items.forEach(item => this.parseItem(item));
95 | }
96 | parseItem(item: TFaustUIItem) {
97 | if (item.type === "vgroup" || item.type === "hgroup" || item.type === "tgroup") {
98 | this.parseItems(item.items);
99 | } else if (item.type === "hbargraph" || item.type === "vbargraph") {
100 | // Keep bargraph adresses
101 | this.outputsItems.push(item.address);
102 | } else if (item.type === "vslider" || item.type === "hslider" || item.type === "button" || item.type === "checkbox" || item.type === "nentry") {
103 | // Keep inputs adresses
104 | this.inputsItems.push(item.address);
105 | if (!item.meta) return;
106 | item.meta.forEach((meta) => {
107 | const { midi } = meta;
108 | if (!midi) return;
109 | const strMidi = midi.trim();
110 | if (strMidi === "pitchwheel") {
111 | this.fPitchwheelLabel.push({ path: item.address, min: item.min, max: item.max });
112 | } else {
113 | const matched = strMidi.match(/^ctrl\s(\d+)/);
114 | if (!matched) return;
115 | this.fCtrlLabel[parseInt(matched[1])].push({ path: item.address, min: item.min, max: item.max });
116 | }
117 | });
118 | }
119 | }
120 |
121 | /**
122 | * Instantiates a new polyphonic voice.
123 | *
124 | * @param {number} channel - the MIDI channel (0..15, not used for now)
125 | * @param {number} pitch - the MIDI pitch (0..127)
126 | * @param {number} velocity - the MIDI velocity (0..127)
127 | * @memberof FaustAudioWorkletNode
128 | */
129 | keyOn(channel: number, pitch: number, velocity: number) {
130 | const e = { type: "keyOn", data: [channel, pitch, velocity] };
131 | this.port.postMessage(e);
132 | }
133 | /**
134 | * De-instantiates a polyphonic voice.
135 | *
136 | * @param {number} channel - the MIDI channel (0..15, not used for now)
137 | * @param {number} pitch - the MIDI pitch (0..127)
138 | * @param {number} velocity - the MIDI velocity (0..127)
139 | * @memberof FaustAudioWorkletNode
140 | */
141 | keyOff(channel: number, pitch: number, velocity: number) {
142 | const e = { type: "keyOff", data: [channel, pitch, velocity] };
143 | this.port.postMessage(e);
144 | }
145 | /**
146 | * Gently terminates all the active voices.
147 | *
148 | * @memberof FaustAudioWorkletNode
149 | */
150 | allNotesOff() {
151 | const e = { type: "ctrlChange", data: [0, 123, 0] };
152 | this.port.postMessage(e);
153 | }
154 | ctrlChange(channel: number, ctrlIn: number, valueIn: any) {
155 | const e = { type: "ctrlChange", data: [channel, ctrlIn, valueIn] };
156 | this.port.postMessage(e);
157 | if (!this.fCtrlLabel[ctrlIn].length) return;
158 | this.fCtrlLabel[ctrlIn].forEach((ctrl) => {
159 | const { path } = ctrl;
160 | const value = remap(valueIn, 0, 127, ctrl.min, ctrl.max);
161 | const param = this.parameters.get(path);
162 | if (param) param.setValueAtTime(value, this.context.currentTime);
163 | });
164 | }
165 | pitchWheel(channel: number, wheel: number) {
166 | const e = { type: "pitchWheel", data: [channel, wheel] };
167 | this.port.postMessage(e);
168 | this.fPitchwheelLabel.forEach((pw) => {
169 | const { path } = pw;
170 | const value = remap(wheel, 0, 16383, pw.min, pw.max);
171 | const param = this.parameters.get(path);
172 | if (param) param.setValueAtTime(value, this.context.currentTime);
173 | });
174 | }
175 | midiMessage(data: number[] | Uint8Array) {
176 | const cmd = data[0] >> 4;
177 | const channel = data[0] & 0xf;
178 | const data1 = data[1];
179 | const data2 = data[2];
180 | if (channel === 9) return;
181 | if (cmd === 8 || (cmd === 9 && data2 === 0)) this.keyOff(channel, data1, data2);
182 | else if (cmd === 9) this.keyOn(channel, data1, data2);
183 | else if (cmd === 11) this.ctrlChange(channel, data1, data2);
184 | else if (cmd === 14) this.pitchWheel(channel, data2 * 128.0 + data1);
185 | else this.port.postMessage({ data, type: "midi" });
186 | }
187 | metadata() {}
188 | setParamValue(path: string, value: number) {
189 | const e = { type: "param", data: { path, value } };
190 | this.port.postMessage(e);
191 | const param = this.parameters.get(path);
192 | if (param) param.setValueAtTime(value, this.context.currentTime);
193 | }
194 | getParamValue(path: string) {
195 | const param = this.parameters.get(path);
196 | if (param) return param.value;
197 | return null;
198 | }
199 | setOutputParamHandler(handler: (address: string, value: number) => any) {
200 | this.outputHandler = handler;
201 | }
202 | getOutputParamHandler() {
203 | return this.outputHandler;
204 | }
205 | getNumInputs() {
206 | return this.dspMeta.inputs;
207 | }
208 | getNumOutputs() {
209 | return this.dspMeta.outputs;
210 | }
211 | getParams() {
212 | return this.inputsItems;
213 | }
214 | getJSON() {
215 | if (this.voices) {
216 | const o = this.dspMeta;
217 | const e = this.effectMeta;
218 | const r = { ...o };
219 | if (e) {
220 | r.ui = [{ type: "tgroup", label: "Sequencer", items: [
221 | { type: "vgroup", label: "Instrument", items: o.ui },
222 | { type: "vgroup", label: "Effect", items: e.ui }
223 | ] }];
224 | } else {
225 | r.ui = [{ type: "tgroup", label: "Polyphonic", items: [
226 | { type: "vgroup", label: "Voices", items: o.ui }
227 | ] }];
228 | }
229 | return JSON.stringify(r);
230 | }
231 | return JSON.stringify(this.dspMeta);
232 | }
233 | getUI() {
234 | if (this.voices) {
235 | const o = this.dspMeta;
236 | const e = this.effectMeta;
237 | if (e) {
238 | return [{ type: "tgroup", label: "Sequencer", items: [
239 | { type: "vgroup", label: "Instrument", items: o.ui },
240 | { type: "vgroup", label: "Effect", items: e.ui }
241 | ] }];
242 | }
243 | return [{ type: "tgroup", label: "Polyphonic", items: [
244 | { type: "vgroup", label: "Voices", items: o.ui }
245 | ] }];
246 | }
247 | return this.dspMeta.ui;
248 | }
249 | destroy() {
250 | this.port.postMessage({ type: "destroy" });
251 | this.port.close();
252 | delete this.plotHandler;
253 | delete this.outputHandler;
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/FaustAudioWorkletProcessor.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable no-console */
3 | /* eslint-disable no-restricted-properties */
4 | /* eslint-disable object-property-newline */
5 | /* eslint-env worker */
6 | import { TDspMeta, FaustDspNode, TFaustUI, TFaustUIGroup, TFaustUIItem, FaustWebAssemblyExports, FaustWebAssemblyMixerExports, TCompiledDsp } from "./types";
7 |
8 | // AudioWorklet Globals
9 | declare class AudioWorkletProcessor {
10 | public port: MessagePort;
11 | public process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: { [key: string]: Float32Array }): boolean;
12 | constructor(options: AudioWorkletNodeOptions);
13 | }
14 | type AudioWorkletProcessorConstructor = {
15 | new (options: AudioWorkletNodeOptions): T;
16 | };
17 | declare function registerProcessor(name: string, constructor: AudioWorkletProcessorConstructor): void;
18 | declare const currentFrame: number;
19 | declare const currentTime: number;
20 | declare const sampleRate: number;
21 | interface AudioParamDescriptor {
22 | automationRate?: AutomationRate;
23 | defaultValue?: number;
24 | maxValue?: number;
25 | minValue?: number;
26 | name: string;
27 | }
28 |
29 | // Injected by Faust
30 | type FaustData = {
31 | id: string;
32 | voices: number;
33 | dspMeta: TDspMeta;
34 | effectMeta?: TDspMeta;
35 | };
36 | declare const faustData: FaustData;
37 | declare const remap: (v: number, mn0: number, mx0: number, mn1: number, mx1: number) => number;
38 | declare const midiToFreq: (v: number) => number;
39 | declare const findPath: (o: any, p: string) => boolean;
40 | declare const createWasmImport: (voices: number, memory: WebAssembly.Memory) => { [key: string]: any };
41 | declare const createWasmMemory: (voicesIn: number, dspMeta: TDspMeta, effectMeta: TDspMeta, bufferSize: number) => WebAssembly.Memory;
42 |
43 | export const FaustAudioWorkletProcessorWrapper = () => {
44 | class FaustConst {
45 | static id = faustData.id;
46 | static dspMeta = faustData.dspMeta;
47 | static effectMeta = faustData.effectMeta;
48 | }
49 | class FaustAudioWorkletProcessor extends AudioWorkletProcessor implements FaustDspNode {
50 | static bufferSize = 128;
51 | // JSON parsing functions
52 | static parseUI(ui: TFaustUI, obj: AudioParamDescriptor[] | FaustAudioWorkletProcessor, callback: (...args: any[]) => any) {
53 | for (let i = 0; i < ui.length; i++) {
54 | this.parseGroup(ui[i], obj, callback);
55 | }
56 | }
57 | static parseGroup(group: TFaustUIGroup, obj: AudioParamDescriptor[] | FaustAudioWorkletProcessor, callback: (...args: any[]) => any) {
58 | if (group.items) {
59 | this.parseItems(group.items, obj, callback);
60 | }
61 | }
62 | static parseItems(items: TFaustUIItem[], obj: AudioParamDescriptor[] | FaustAudioWorkletProcessor, callback: (...args: any[]) => any) {
63 | for (let i = 0; i < items.length; i++) {
64 | callback(items[i], obj, callback);
65 | }
66 | }
67 | static parseItem(item: TFaustUIItem, obj: AudioParamDescriptor[], callback: (...args: any[]) => any) {
68 | if (item.type === "vgroup" || item.type === "hgroup" || item.type === "tgroup") {
69 | FaustAudioWorkletProcessor.parseItems(item.items, obj, callback); // callback may not binded to this
70 | } else if (item.type === "hbargraph" || item.type === "vbargraph") {
71 | // Nothing
72 | } else if (item.type === "vslider" || item.type === "hslider" || item.type === "nentry") {
73 | if (!faustData.voices ||
74 | (!item.address.endsWith("/gate") &&
75 | !item.address.endsWith("/freq") &&
76 | !item.address.endsWith("/key") &&
77 | !item.address.endsWith("/vel") &&
78 | !item.address.endsWith("/velocity") &&
79 | !item.address.endsWith("/gain"))) {
80 | obj.push({ name: item.address, defaultValue: item.init || 0, minValue: item.min || 0, maxValue: item.max || 0 });
81 | }
82 | } else if (item.type === "button" || item.type === "checkbox") {
83 | if (!faustData.voices ||
84 | (!item.address.endsWith("/gate") &&
85 | !item.address.endsWith("/freq") &&
86 | !item.address.endsWith("/key") &&
87 | !item.address.endsWith("/vel") &&
88 | !item.address.endsWith("/velocity") &&
89 | !item.address.endsWith("/gain"))) {
90 | obj.push({ name: item.address, defaultValue: item.init || 0, minValue: 0, maxValue: 1 });
91 | }
92 | }
93 | }
94 | static parseItem2(item: TFaustUIItem, obj: FaustAudioWorkletProcessor, callback: (...args: any[]) => any) {
95 | if (item.type === "vgroup" || item.type === "hgroup" || item.type === "tgroup") {
96 | FaustAudioWorkletProcessor.parseItems(item.items, obj, callback); // callback may not binded to this
97 | } else if (item.type === "hbargraph" || item.type === "vbargraph") {
98 | // Keep bargraph adresses
99 | obj.outputsItems.push(item.address);
100 | obj.pathTable$[item.address] = item.index;
101 | } else if (item.type === "vslider" || item.type === "hslider" || item.type === "button" || item.type === "checkbox" || item.type === "nentry") {
102 | // Keep inputs adresses
103 | obj.inputsItems.push(item.address);
104 | obj.pathTable$[item.address] = item.index;
105 | if (!item.meta) return;
106 | item.meta.forEach((meta) => {
107 | const { midi } = meta;
108 | if (!midi) return;
109 | const strMidi = midi.trim();
110 | if (strMidi === "pitchwheel") {
111 | obj.fPitchwheelLabel.push({ path: item.address, min: item.min, max: item.max });
112 | } else {
113 | const matched = strMidi.match(/^ctrl\s(\d+)/);
114 | if (!matched) return;
115 | obj.fCtrlLabel[parseInt(matched[1])].push({ path: item.address, min: item.min, max: item.max });
116 | }
117 | });
118 | }
119 | }
120 | static get parameterDescriptors() {
121 | // Analyse JSON to generate AudioParam parameters
122 | const params = [] as AudioParamDescriptor[];
123 | this.parseUI(FaustConst.dspMeta.ui, params, this.parseItem);
124 | if (FaustConst.effectMeta) this.parseUI(FaustConst.effectMeta.ui, params, this.parseItem);
125 | return params;
126 | }
127 | destroyed: boolean;
128 | dspInstance: WebAssembly.Instance;
129 | effectInstance?: WebAssembly.Instance;
130 | mixerInstance?: WebAssembly.Instance;
131 | memory?: WebAssembly.Memory;
132 |
133 | bufferSize: number;
134 | voices: number;
135 | dspMeta: TDspMeta;
136 | $ins: number;
137 | $outs: number;
138 | dspInChannnels: Float32Array[];
139 | dspOutChannnels: Float32Array[];
140 | fPitchwheelLabel: { path: string; min: number; max: number }[];
141 | fCtrlLabel: { path: string; min: number; max: number }[][];
142 | numIn: number;
143 | numOut: number;
144 | ptrSize: number;
145 | sampleSize: number;
146 | outputsTimer: number;
147 | inputsItems: string[];
148 | outputsItems: string[];
149 | pathTable$: { [address: string]: number };
150 | $audioHeap: number;
151 | $$audioHeapInputs: number;
152 | $$audioHeapOutputs: number;
153 | $audioHeapInputs: number;
154 | $audioHeapOutputs: number;
155 | $dsp: number;
156 | factory: FaustWebAssemblyExports;
157 | HEAP: ArrayBuffer;
158 | HEAP32: Int32Array;
159 | HEAPF32: Float32Array;
160 |
161 | effectMeta?: TDspMeta;
162 | $effect?: number;
163 | $mixing?: number;
164 | fFreqLabel$?: number[];
165 | fKeyLabel$?: number[];
166 | fGateLabel$?: number[];
167 | fGainLabel$?: number[];
168 | fVelLabel$?: number[];
169 | fDate?: number;
170 | $$audioHeapMixing?: number;
171 | $audioHeapMixing?: number;
172 | mixer?: FaustWebAssemblyMixerExports;
173 | effect?: FaustWebAssemblyExports;
174 | dspVoices$?: number[];
175 | dspVoicesState?: number[];
176 | dspVoicesLevel?: number[];
177 | dspVoicesDate?: number[];
178 | kActiveVoice?: number;
179 | kFreeVoice?: number;
180 | kReleaseVoice?: number;
181 | kNoVoice?: number;
182 |
183 | $buffer: number;
184 | cachedEvents: { type: string; data: any }[];
185 |
186 | outputHandler: (address: string, value: number) => any;
187 | computeHandler: (bufferSize: number) => any;
188 |
189 | handleMessage = (e: MessageEvent) => { // use arrow function for binding
190 | const msg = e.data;
191 | this.cachedEvents.push({ type: e.data.type, data: e.data.data });
192 | switch (msg.type) {
193 | // Generic MIDI message
194 | case "midi": this.midiMessage(msg.data); break;
195 | // Typed MIDI message
196 | case "keyOn": this.keyOn(msg.data[0], msg.data[1], msg.data[2]); break;
197 | case "keyOff": this.keyOff(msg.data[0], msg.data[1], msg.data[2]); break;
198 | case "ctrlChange": this.ctrlChange(msg.data[0], msg.data[1], msg.data[2]); break;
199 | case "pitchWheel": this.pitchWheel(msg.data[0], msg.data[1]); break;
200 | // Generic data message
201 | case "param": this.setParamValue(msg.data.path, msg.data.value); break;
202 | // case "patch": this.onpatch(msg.data); break;
203 | case "destroy": {
204 | this.port.close();
205 | this.destroyed = true;
206 | delete this.outputHandler;
207 | delete this.computeHandler;
208 | break;
209 | }
210 | default:
211 | }
212 | };
213 | constructor(options: AudioWorkletNodeOptions) {
214 | super(options);
215 | const processorOptions: { id: string; voices: number; compiledDsp: TCompiledDsp; mixer32Module: WebAssembly.Module } = options.processorOptions;
216 | this.instantiateWasm(processorOptions);
217 | this.port.onmessage = this.handleMessage; // Naturally binded with arrow function property
218 | this.destroyed = false;
219 |
220 | this.bufferSize = 128;
221 | this.voices = processorOptions.voices;
222 | this.dspMeta = processorOptions.compiledDsp.dspMeta;
223 |
224 | this.outputHandler = (path, value) => this.port.postMessage({ path, value, type: "param" });
225 | this.computeHandler = null;
226 |
227 | this.$ins = null;
228 | this.$outs = null;
229 |
230 | this.dspInChannnels = [];
231 | this.dspOutChannnels = [];
232 |
233 | this.fPitchwheelLabel = [];
234 | this.fCtrlLabel = new Array(128).fill(null).map(() => []);
235 |
236 | this.numIn = this.dspMeta.inputs;
237 | this.numOut = this.dspMeta.outputs;
238 |
239 | // Memory allocator
240 | this.ptrSize = 4;
241 | this.sampleSize = 4;
242 |
243 | // Create the WASM instance
244 | this.factory = this.dspInstance.exports as FaustWebAssemblyExports;
245 | this.HEAP = this.voices ? this.memory.buffer : this.factory.memory.buffer;
246 | this.HEAP32 = new Int32Array(this.HEAP);
247 | this.HEAPF32 = new Float32Array(this.HEAP);
248 |
249 | // console.log(this.HEAP);
250 | // console.log(this.HEAP32);
251 | // console.log(this.HEAPF32);
252 |
253 | // bargraph
254 | this.outputsTimer = 5;
255 | this.outputsItems = [];
256 |
257 | // input items
258 | this.inputsItems = [];
259 |
260 | // Start of HEAP index
261 |
262 | // DSP is placed first with index 0. Audio buffer start at the end of DSP.
263 | this.$audioHeap = this.voices ? 0 : this.dspMeta.size;
264 |
265 | // Setup pointers offset
266 | this.$$audioHeapInputs = this.$audioHeap;
267 | this.$$audioHeapOutputs = this.$$audioHeapInputs + this.numIn * this.ptrSize;
268 |
269 | // Setup buffer offset
270 | this.$audioHeapInputs = this.$$audioHeapOutputs + (this.numOut * this.ptrSize);
271 | this.$audioHeapOutputs = this.$audioHeapInputs + (this.numIn * this.bufferSize * this.sampleSize);
272 | if (this.voices) {
273 | this.$$audioHeapMixing = this.$$audioHeapOutputs + this.numOut * this.ptrSize;
274 | // Setup buffer offset
275 | this.$audioHeapInputs = this.$$audioHeapMixing + this.numOut * this.ptrSize;
276 | this.$audioHeapOutputs = this.$audioHeapInputs + this.numIn * this.bufferSize * this.sampleSize;
277 | this.$audioHeapMixing = this.$audioHeapOutputs + this.numOut * this.bufferSize * this.sampleSize;
278 | this.$dsp = this.$audioHeapMixing + this.numOut * this.bufferSize * this.sampleSize;
279 | } else {
280 | this.$audioHeapInputs = this.$$audioHeapOutputs + this.numOut * this.ptrSize;
281 | this.$audioHeapOutputs = this.$audioHeapInputs + this.numIn * this.bufferSize * this.sampleSize;
282 | // Start of DSP memory : Mono DSP is placed first with index 0
283 | this.$dsp = 0;
284 | }
285 |
286 | if (this.voices) {
287 | this.effectMeta = FaustConst.effectMeta;
288 | this.$mixing = null;
289 | this.fFreqLabel$ = [];
290 | this.fKeyLabel$ = [];
291 | this.fGateLabel$ = [];
292 | this.fGainLabel$ = [];
293 | this.fVelLabel$ = [];
294 | this.fDate = 0;
295 |
296 | this.mixer = this.mixerInstance.exports as FaustWebAssemblyMixerExports;
297 | this.effect = this.effectInstance ? this.effectInstance.exports as FaustWebAssemblyExports : null;
298 |
299 | // Start of DSP memory ('polyphony' DSP voices)
300 | this.dspVoices$ = [];
301 | this.dspVoicesState = [];
302 | this.dspVoicesLevel = [];
303 | this.dspVoicesDate = [];
304 |
305 | this.kActiveVoice = 0;
306 | this.kFreeVoice = -1;
307 | this.kReleaseVoice = -2;
308 | this.kNoVoice = -3;
309 |
310 | for (let i = 0; i < this.voices; i++) {
311 | this.dspVoices$[i] = this.$dsp + i * this.dspMeta.size;
312 | this.dspVoicesState[i] = this.kFreeVoice;
313 | this.dspVoicesLevel[i] = 0;
314 | this.dspVoicesDate[i] = 0;
315 | }
316 | // Effect memory starts after last voice
317 | this.$effect = this.dspVoices$[this.voices - 1] + this.dspMeta.size;
318 | }
319 |
320 | this.pathTable$ = {};
321 |
322 | this.$buffer = 0;
323 | this.cachedEvents = [];
324 |
325 | // Init resulting DSP
326 | this.setup();
327 | }
328 | instantiateWasm(options: { id: string; voices: number; compiledDsp: TCompiledDsp; mixer32Module: WebAssembly.Module }) {
329 | const memory = createWasmMemory(options.voices, options.compiledDsp.dspMeta, options.compiledDsp.effectMeta, 128);
330 | this.memory = memory;
331 | const imports = createWasmImport(options.voices, memory);
332 | this.dspInstance = new WebAssembly.Instance(options.compiledDsp.dspModule, imports);
333 | if (options.compiledDsp.effectModule) {
334 | this.effectInstance = new WebAssembly.Instance(options.compiledDsp.effectModule, imports);
335 | }
336 | if (options.voices) {
337 | const mixerImports = { imports: { print: console.log }, memory: { memory } };
338 | this.mixerInstance = new WebAssembly.Instance(options.mixer32Module, mixerImports);
339 | }
340 | }
341 | updateOutputs() {
342 | if (this.outputsItems.length > 0 && this.outputHandler && this.outputsTimer-- === 0) {
343 | this.outputsTimer = 5;
344 | this.outputsItems.forEach(item => this.outputHandler(item, this.factory.getParamValue(this.$dsp, this.pathTable$[item])));
345 | }
346 | }
347 |
348 | parseUI(ui: TFaustUI) {
349 | return FaustAudioWorkletProcessor.parseUI(ui, this, FaustAudioWorkletProcessor.parseItem2);
350 | }
351 | parseGroup(group: TFaustUIGroup) {
352 | return FaustAudioWorkletProcessor.parseGroup(group, this, FaustAudioWorkletProcessor.parseItem2);
353 | }
354 | parseItems(items: TFaustUIItem[]) {
355 | return FaustAudioWorkletProcessor.parseItems(items, this, FaustAudioWorkletProcessor.parseItem2);
356 | }
357 | parseItem(item: TFaustUIItem) {
358 | return FaustAudioWorkletProcessor.parseItem2(item, this, FaustAudioWorkletProcessor.parseItem2);
359 | }
360 |
361 | setParamValue(path: string, val: number) {
362 | if (this.voices) {
363 | if (this.effect && findPath(this.effectMeta.ui, path)) this.effect.setParamValue(this.$effect, this.pathTable$[path], val);
364 | else this.dspVoices$.forEach($voice => this.factory.setParamValue($voice, this.pathTable$[path], val));
365 | } else {
366 | this.factory.setParamValue(this.$dsp, this.pathTable$[path], val);
367 | }
368 | }
369 | getParamValue(path: string) {
370 | if (this.voices) {
371 | if (this.effect && findPath(this.effectMeta.ui, path)) return this.effect.getParamValue(this.$effect, this.pathTable$[path]);
372 | return this.factory.getParamValue(this.dspVoices$[0], this.pathTable$[path]);
373 | }
374 | return this.factory.getParamValue(this.$dsp, this.pathTable$[path]);
375 | }
376 | setup() {
377 | if (this.numIn > 0) {
378 | this.$ins = this.$$audioHeapInputs;
379 | for (let i = 0; i < this.numIn; i++) {
380 | this.HEAP32[(this.$ins >> 2) + i] = this.$audioHeapInputs + this.bufferSize * this.sampleSize * i;
381 | }
382 | // Prepare Ins buffer tables
383 | const dspInChans = this.HEAP32.subarray(this.$ins >> 2, (this.$ins + this.numIn * this.ptrSize) >> 2);
384 | for (let i = 0; i < this.numIn; i++) {
385 | this.dspInChannnels[i] = this.HEAPF32.subarray(dspInChans[i] >> 2, (dspInChans[i] + this.bufferSize * this.sampleSize) >> 2);
386 | }
387 | }
388 | if (this.numOut > 0) {
389 | this.$outs = this.$$audioHeapOutputs;
390 | if (this.voices) this.$mixing = this.$$audioHeapMixing;
391 | for (let i = 0; i < this.numOut; i++) {
392 | this.HEAP32[(this.$outs >> 2) + i] = this.$audioHeapOutputs + this.bufferSize * this.sampleSize * i;
393 | if (this.voices) this.HEAP32[(this.$mixing >> 2) + i] = this.$audioHeapMixing + this.bufferSize * this.sampleSize * i;
394 | }
395 | // Prepare Out buffer tables
396 | const dspOutChans = this.HEAP32.subarray(this.$outs >> 2, (this.$outs + this.numOut * this.ptrSize) >> 2);
397 | for (let i = 0; i < this.numOut; i++) {
398 | this.dspOutChannnels[i] = this.HEAPF32.subarray(dspOutChans[i] >> 2, (dspOutChans[i] + this.bufferSize * this.sampleSize) >> 2);
399 | }
400 | }
401 | // Parse UI
402 | this.parseUI(this.dspMeta.ui);
403 | if (this.effect) this.parseUI(this.effectMeta.ui);
404 |
405 | // keep 'keyOn/keyOff' labels
406 | if (this.voices) {
407 | this.inputsItems.forEach((item) => {
408 | if (item.endsWith("/gate")) this.fGateLabel$.push(this.pathTable$[item]);
409 | else if (item.endsWith("/freq")) this.fFreqLabel$.push(this.pathTable$[item]);
410 | else if (item.endsWith("/key")) this.fKeyLabel$.push(this.pathTable$[item]);
411 | else if (item.endsWith("/gain")) this.fGainLabel$.push(this.pathTable$[item]);
412 | else if (item.endsWith("/vel") || item.endsWith("/velocity")) this.fVelLabel$.push(this.pathTable$[item]);
413 | });
414 | // Init DSP voices
415 | this.dspVoices$.forEach($voice => this.factory.init($voice, sampleRate));
416 | // Init effect
417 | if (this.effect) this.effect.init(this.$effect, sampleRate);
418 | } else {
419 | // Init DSP
420 | this.factory.init(this.$dsp, sampleRate); // 'sampleRate' is defined in AudioWorkletGlobalScope
421 | }
422 | }
423 | // Poly only methods
424 | getPlayingVoice(pitch: number) {
425 | if (!this.voices) return null;
426 | let voice = this.kNoVoice;
427 | let oldestDatePlaying = Number.MAX_VALUE;
428 | for (let i = 0; i < this.voices; i++) {
429 | if (this.dspVoicesState[i] === pitch) {
430 | // Keeps oldest playing voice
431 | if (this.dspVoicesDate[i] < oldestDatePlaying) {
432 | oldestDatePlaying = this.dspVoicesDate[i];
433 | voice = i;
434 | }
435 | }
436 | }
437 | return voice;
438 | }
439 | allocVoice(voice: number) {
440 | if (!this.voices) return null;
441 | // so that envelop is always re-initialized
442 | this.factory.instanceClear(this.dspVoices$[voice]);
443 | this.dspVoicesDate[voice] = this.fDate++;
444 | this.dspVoicesState[voice] = this.kActiveVoice;
445 | return voice;
446 | }
447 | getFreeVoice() {
448 | if (!this.voices) return null;
449 | for (let i = 0; i < this.voices; i++) {
450 | if (this.dspVoicesState[i] === this.kFreeVoice) return this.allocVoice(i);
451 | }
452 | let voiceRelease = this.kNoVoice;
453 | let voicePlaying = this.kNoVoice;
454 | let oldestDateRelease = Number.MAX_VALUE;
455 | let oldestDatePlaying = Number.MAX_VALUE;
456 | for (let i = 0; i < this.voices; i++) { // Scan all voices
457 | // Try to steal a voice in kReleaseVoice mode...
458 | if (this.dspVoicesState[i] === this.kReleaseVoice) {
459 | // Keeps oldest release voice
460 | if (this.dspVoicesDate[i] < oldestDateRelease) {
461 | oldestDateRelease = this.dspVoicesDate[i];
462 | voiceRelease = i;
463 | }
464 | } else if (this.dspVoicesDate[i] < oldestDatePlaying) {
465 | oldestDatePlaying = this.dspVoicesDate[i];
466 | voicePlaying = i;
467 | }
468 | }
469 | // Then decide which one to steal
470 | if (oldestDateRelease !== Number.MAX_VALUE) {
471 | // console.log(`Steal release voice : voice_date = ${this.dspVoicesDate[voiceRelease]} cur_date = ${this.fDate} voice = ${voiceRelease}`);
472 | return this.allocVoice(voiceRelease);
473 | }
474 | if (oldestDatePlaying !== Number.MAX_VALUE) {
475 | // console.log(`Steal playing voice : voice_date = ${this.dspVoicesDate[voicePlaying]} cur_date = ${this.fDate} voice = ${voicePlaying}`);
476 | return this.allocVoice(voicePlaying);
477 | }
478 | return this.kNoVoice;
479 | }
480 | keyOn(channel: number, pitch: number, velocity: number) {
481 | if (!this.voices) return;
482 | const voice = this.getFreeVoice();
483 | // console.log("keyOn voice " + voice);
484 | this.fFreqLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, midiToFreq(pitch)));
485 | this.fKeyLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, pitch));
486 | this.fGateLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, 1));
487 | this.fGainLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, velocity / 127));
488 | this.fVelLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, velocity));
489 | this.dspVoicesState[voice] = pitch;
490 | }
491 | keyOff(channel: number, pitch: number, velocity: number) {
492 | if (!this.voices) return;
493 | const voice = this.getPlayingVoice(pitch);
494 | if (voice === this.kNoVoice) return; // console.log("Playing voice not found...");
495 | // console.log("keyOff voice " + voice);
496 | this.fGateLabel$.forEach($ => this.factory.setParamValue(this.dspVoices$[voice], $, 0)); // No use of velocity for now...
497 | this.dspVoicesState[voice] = this.kReleaseVoice; // Release voice
498 | }
499 | allNotesOff() {
500 | if (!this.voices) return;
501 | for (let i = 0; i < this.voices; i++) {
502 | this.fGateLabel$.forEach($gate => this.factory.setParamValue(this.dspVoices$[i], $gate, 0));
503 | this.dspVoicesState[i] = this.kReleaseVoice;
504 | }
505 | }
506 |
507 | midiMessage(data: number[] | Uint8Array) {
508 | const cmd = data[0] >> 4;
509 | const channel = data[0] & 0xf;
510 | const data1 = data[1];
511 | const data2 = data[2];
512 | if (channel === 9) return;
513 | if (cmd === 8 || (cmd === 9 && data2 === 0)) this.keyOff(channel, data1, data2);
514 | else if (cmd === 9) this.keyOn(channel, data1, data2);
515 | else if (cmd === 11) this.ctrlChange(channel, data1, data2);
516 | else if (cmd === 14) this.pitchWheel(channel, data2 * 128.0 + data1);
517 | }
518 | ctrlChange(channel: number, ctrl: number, value: number) {
519 | if (ctrl === 123 || ctrl === 120) {
520 | this.allNotesOff();
521 | }
522 | if (!this.fCtrlLabel[ctrl].length) return;
523 | this.fCtrlLabel[ctrl].forEach((ctrl) => {
524 | const { path } = ctrl;
525 | this.setParamValue(path, remap(value, 0, 127, ctrl.min, ctrl.max));
526 | if (this.outputHandler) this.outputHandler(path, this.getParamValue(path));
527 | });
528 | }
529 | pitchWheel(channel: number, wheel: number) {
530 | this.fPitchwheelLabel.forEach((pw) => {
531 | this.setParamValue(pw.path, remap(wheel, 0, 16383, pw.min, pw.max));
532 | if (this.outputHandler) this.outputHandler(pw.path, this.getParamValue(pw.path));
533 | });
534 | }
535 | process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: { [key: string]: Float32Array }) {
536 | if (this.destroyed) return false;
537 | const input = inputs[0];
538 | const output = outputs[0];
539 | // Check inputs
540 | if (this.numIn > 0 && (!input || !input[0] || input[0].length === 0)) {
541 | // console.log("Process input error");
542 | return true;
543 | }
544 | // Check outputs
545 | if (this.numOut > 0 && (!output || !output[0] || output[0].length === 0)) {
546 | // console.log("Process output error");
547 | return true;
548 | }
549 | // Copy inputs
550 | if (input !== undefined) {
551 | for (let chan = 0; chan < Math.min(this.numIn, input.length); ++chan) {
552 | const dspInput = this.dspInChannnels[chan];
553 | dspInput.set(input[chan]);
554 | }
555 | }
556 | // Update controls (possibly needed for sample accurate control)
557 | for (const path in parameters) {
558 | const paramArray = parameters[path];
559 | this.setParamValue(path, paramArray[0]);
560 | }
561 | // Possibly call an externally given callback (for instance to synchronize playing a MIDIFile...)
562 | if (this.computeHandler) this.computeHandler(this.bufferSize);
563 | if (this.voices) {
564 | this.mixer.clearOutput(this.bufferSize, this.numOut, this.$outs); // First clear the outputs
565 | for (let i = 0; i < this.voices; i++) { // Compute all running voices
566 | this.factory.compute(this.dspVoices$[i], this.bufferSize, this.$ins, this.$mixing); // Compute voice
567 | this.mixer.mixVoice(this.bufferSize, this.numOut, this.$mixing, this.$outs); // Mix it in result
568 | }
569 | if (this.effect) this.effect.compute(this.$effect, this.bufferSize, this.$outs, this.$outs); // Apply effect. Not a typo, effect is applied on the outs.
570 | } else {
571 | this.factory.compute(this.$dsp, this.bufferSize, this.$ins, this.$outs); // Compute
572 | }
573 | // Update bargraph
574 | this.updateOutputs();
575 | // Copy outputs
576 | if (output !== undefined) {
577 | for (let i = 0; i < Math.min(this.numOut, output.length); i++) {
578 | const dspOutput = this.dspOutChannnels[i];
579 | output[i].set(dspOutput);
580 | }
581 | this.port.postMessage({ type: "plot", value: output, index: this.$buffer++, events: this.cachedEvents });
582 | this.cachedEvents = [];
583 | }
584 | return true;
585 | }
586 | printMemory() {
587 | console.log("============== Memory layout ==============");
588 | console.log("dspMeta.size: " + this.dspMeta.size);
589 | console.log("$audioHeap: " + this.$audioHeap);
590 | console.log("$$audioHeapInputs: " + this.$$audioHeapInputs);
591 | console.log("$$audioHeapOutputs: " + this.$$audioHeapOutputs);
592 | console.log("$$audioHeapMixing: " + this.$$audioHeapMixing);
593 | console.log("$audioHeapInputs: " + this.$audioHeapInputs);
594 | console.log("$audioHeapOutputs: " + this.$audioHeapOutputs);
595 | console.log("$audioHeapMixing: " + this.$audioHeapMixing);
596 | console.log("$dsp: " + this.$dsp);
597 | if (this.dspVoices$) this.dspVoices$.forEach(($voice, i) => console.log("dspVoices$[" + i + "]: " + $voice));
598 | console.log("$effect: " + this.$effect);
599 | console.log("$mixing: " + this.$mixing);
600 | }
601 | }
602 |
603 | // Globals
604 | // Synchronously compile and instantiate the WASM module
605 | registerProcessor(FaustConst.id || "mydsp", FaustAudioWorkletProcessor);
606 | };
607 |
--------------------------------------------------------------------------------
/src/FaustOfflineProcessor.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-properties */
2 | /* eslint-disable object-property-newline */
3 | import { TDspMeta, FaustWebAssemblyExports, TCompiledDsp } from "./types";
4 |
5 | export class FaustOfflineProcessor {
6 | private bufferSize = 1024;
7 | private sampleRate: number;
8 | private dspMeta: TDspMeta;
9 | private $ins: number;
10 | private $outs: number;
11 | private dspInChannnels: Float32Array[];
12 | private dspOutChannnels: Float32Array[];
13 | private numIn: number;
14 | private numOut: number;
15 | private ptrSize: number;
16 | private sampleSize: number;
17 | private $audioHeap: number;
18 | private $$audioHeapInputs: number;
19 | private $$audioHeapOutputs: number;
20 | private $audioHeapInputs: number;
21 | private $audioHeapOutputs: number;
22 | private $dsp: number;
23 | private factory: FaustWebAssemblyExports;
24 | private HEAP: ArrayBuffer;
25 | private HEAP32: Int32Array;
26 | private HEAPF32: Float32Array;
27 | private output: Float32Array[];
28 |
29 | static get importObject() {
30 | return {
31 | env: {
32 | memory: undefined as WebAssembly.Memory, memoryBase: 0, tableBase: 0,
33 | _abs: Math.abs,
34 | // Float version
35 | _acosf: Math.acos, _asinf: Math.asin, _atanf: Math.atan, _atan2f: Math.atan2,
36 | _ceilf: Math.ceil, _cosf: Math.cos, _expf: Math.exp, _floorf: Math.floor,
37 | _fmodf: (x: number, y: number) => x % y,
38 | _logf: Math.log, _log10f: Math.log10, _max_f: Math.max, _min_f: Math.min,
39 | _remainderf: (x: number, y: number) => x - Math.round(x / y) * y,
40 | _powf: Math.pow, _roundf: Math.fround, _sinf: Math.sin, _sqrtf: Math.sqrt, _tanf: Math.tan,
41 | _acoshf: Math.acosh, _asinhf: Math.asinh, _atanhf: Math.atanh,
42 | _coshf: Math.cosh, _sinhf: Math.sinh, _tanhf: Math.tanh,
43 | _isnanf: Number.isNaN, _isinff: (x: number) => !isFinite(x),
44 | _copysignf: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x),
45 |
46 | // Double version
47 | _acos: Math.acos, _asin: Math.asin, _atan: Math.atan, _atan2: Math.atan2,
48 | _ceil: Math.ceil, _cos: Math.cos, _exp: Math.exp, _floor: Math.floor,
49 | _fmod: (x: number, y: number) => x % y,
50 | _log: Math.log, _log10: Math.log10, _max_: Math.max, _min_: Math.min,
51 | _remainder: (x: number, y: number) => x - Math.round(x / y) * y,
52 | _pow: Math.pow, _round: Math.fround, _sin: Math.sin, _sqrt: Math.sqrt, _tan: Math.tan,
53 | _acosh: Math.acosh, _asinh: Math.asinh, _atanh: Math.atanh,
54 | _cosh: Math.cosh, _sinh: Math.sinh, _tanh: Math.tanh,
55 | _isnan: Number.isNaN, _isinf: (x: number) => !isFinite(x),
56 | _copysign: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x),
57 |
58 | table: new WebAssembly.Table({ initial: 0, element: "anyfunc" })
59 | }
60 | };
61 | }
62 | async init(options: { compiledDsp?: TCompiledDsp; sampleRate?: number }) {
63 | const { compiledDsp } = options;
64 | if (!compiledDsp) throw new Error("No Dsp input");
65 |
66 | this.dspMeta = compiledDsp.dspMeta;
67 |
68 | this.$ins = null;
69 | this.$outs = null;
70 |
71 | this.dspInChannnels = [];
72 | this.dspOutChannnels = [];
73 |
74 | this.numIn = this.dspMeta.inputs;
75 | this.numOut = this.dspMeta.outputs;
76 | // Memory allocator
77 | this.ptrSize = 4;
78 | this.sampleSize = 4;
79 |
80 | // Create the WASM instance
81 | const dspInstance = await WebAssembly.instantiate(compiledDsp.dspModule, FaustOfflineProcessor.importObject);
82 | this.factory = dspInstance.exports as FaustWebAssemblyExports;
83 | this.HEAP = this.factory.memory.buffer;
84 | this.HEAP32 = new Int32Array(this.HEAP);
85 | this.HEAPF32 = new Float32Array(this.HEAP);
86 |
87 | this.output = new Array(this.numOut).fill(null).map(() => new Float32Array(this.bufferSize));
88 | }
89 | setup(options?: { sampleRate?: number }) {
90 | if (!this.dspMeta) throw new Error("No Dsp");
91 | this.sampleRate = options && options.sampleRate || 48000;
92 |
93 | // DSP is placed first with index 0. Audio buffer start at the end of DSP.
94 | this.$audioHeap = this.dspMeta.size;
95 |
96 | // Setup pointers offset
97 | this.$$audioHeapInputs = this.$audioHeap;
98 | this.$$audioHeapOutputs = this.$$audioHeapInputs + this.numIn * this.ptrSize;
99 |
100 | // Setup buffer offset
101 | this.$audioHeapInputs = this.$$audioHeapOutputs + (this.numOut * this.ptrSize);
102 | this.$audioHeapOutputs = this.$audioHeapInputs + (this.numIn * this.bufferSize * this.sampleSize);
103 | // Start of DSP memory : Mono DSP is placed first with index 0
104 | this.$dsp = 0;
105 |
106 | if (this.numIn > 0) {
107 | this.$ins = this.$$audioHeapInputs;
108 | for (let i = 0; i < this.numIn; i++) {
109 | this.HEAP32[(this.$ins >> 2) + i] = this.$audioHeapInputs + this.bufferSize * this.sampleSize * i;
110 | }
111 | // Prepare Ins buffer tables
112 | const dspInChans = this.HEAP32.subarray(this.$ins >> 2, (this.$ins + this.numIn * this.ptrSize) >> 2);
113 | for (let i = 0; i < this.numIn; i++) {
114 | this.dspInChannnels[i] = this.HEAPF32.subarray(dspInChans[i] >> 2, (dspInChans[i] + this.bufferSize * this.sampleSize) >> 2);
115 | }
116 | }
117 | if (this.numOut > 0) {
118 | this.$outs = this.$$audioHeapOutputs;
119 | for (let i = 0; i < this.numOut; i++) {
120 | this.HEAP32[(this.$outs >> 2) + i] = this.$audioHeapOutputs + this.bufferSize * this.sampleSize * i;
121 | }
122 | // Prepare Out buffer tables
123 | const dspOutChans = this.HEAP32.subarray(this.$outs >> 2, (this.$outs + this.numOut * this.ptrSize) >> 2);
124 | for (let i = 0; i < this.numOut; i++) {
125 | this.dspOutChannnels[i] = this.HEAPF32.subarray(dspOutChans[i] >> 2, (dspOutChans[i] + this.bufferSize * this.sampleSize) >> 2);
126 | }
127 | }
128 | // Init DSP
129 | this.factory.init(this.$dsp, this.sampleRate);
130 | }
131 | compute() {
132 | if (!this.factory) return this.output;
133 | for (let i = 0; i < this.numIn; i++) {
134 | this.dspInChannnels[i].fill(0);
135 | }
136 | this.factory.compute(this.$dsp, this.bufferSize, this.$ins, this.$outs); // Compute
137 | // Copy outputs
138 | if (this.output !== undefined) {
139 | for (let i = 0; i < this.numOut; i++) {
140 | this.output[i].set(this.dspOutChannnels[i]);
141 | }
142 | }
143 | return this.output;
144 | }
145 | async plot(options?: { compiledDsp?: TCompiledDsp; size?: number; sampleRate?: number }) {
146 | if (options && options.compiledDsp) await this.init(options);
147 | this.setup(options);
148 | const size = options && options.size || 128;
149 | const plotted = new Array(this.numOut).fill(null).map(() => new Float32Array(size));
150 | for (let i = 0; i < size; i += this.bufferSize) {
151 | const computed = this.compute();
152 | for (let j = 0; j < plotted.length; j++) {
153 | plotted[j].set(size - i > this.bufferSize ? computed[j] : computed[j].subarray(0, size - i), i);
154 | }
155 | }
156 | return plotted;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/FaustWasmToScriptProcessor.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-properties */
2 | /* eslint-disable object-property-newline */
3 | /* eslint-disable object-curly-newline */
4 | import { Faust } from "./Faust";
5 | import { mixer32Module, createWasmMemory, createWasmImport, midiToFreq, remap } from "./utils";
6 | import { TCompiledDsp, FaustScriptProcessorNode, TAudioNodeOptions, FaustWebAssemblyMixerExports, FaustWebAssemblyExports } from "./types";
7 |
8 | export class FaustWasmToScriptProcessor {
9 | faust: Faust;
10 | constructor(faust: Faust) {
11 | this.faust = faust;
12 | }
13 | private initNode(compiledDsp: TCompiledDsp, dspInstance: WebAssembly.Instance, effectInstance: WebAssembly.Instance, mixerInstance: WebAssembly.Instance, audioCtx: AudioContext, bufferSize?: number, memory?: WebAssembly.Memory, voices?: number, plotHandler?: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any) {
14 | let node: FaustScriptProcessorNode;
15 | const dspMeta = compiledDsp.dspMeta;
16 | const inputs = dspMeta.inputs;
17 | const outputs = dspMeta.outputs;
18 | try {
19 | node = audioCtx.createScriptProcessor(bufferSize, inputs, outputs) as FaustScriptProcessorNode;
20 | } catch (e) {
21 | this.faust.error("Error in createScriptProcessor: " + e.message);
22 | throw e;
23 | }
24 | node.destroyed = false;
25 | node.voices = voices;
26 | node.dspMeta = dspMeta;
27 |
28 | node.outputHandler = null;
29 | node.computeHandler = null;
30 | node.$ins = null;
31 | node.$outs = null;
32 |
33 | node.dspInChannnels = [];
34 | node.dspOutChannnels = [];
35 |
36 | node.fPitchwheelLabel = [];
37 | node.fCtrlLabel = new Array(128).fill(null).map(() => []);
38 |
39 | node.numIn = inputs;
40 | node.numOut = outputs;
41 |
42 | this.faust.log(node.numIn);
43 | this.faust.log(node.numOut);
44 |
45 | // Memory allocator
46 | node.ptrSize = 4;
47 | node.sampleSize = 4;
48 |
49 | node.factory = dspInstance.exports as FaustWebAssemblyExports;
50 | node.HEAP = node.voices ? memory.buffer : node.factory.memory.buffer;
51 | node.HEAP32 = new Int32Array(node.HEAP);
52 | node.HEAPF32 = new Float32Array(node.HEAP);
53 |
54 | this.faust.log(node.HEAP);
55 | this.faust.log(node.HEAP32);
56 | this.faust.log(node.HEAPF32);
57 |
58 | // JSON is as offset 0
59 | /*
60 | var HEAPU8 = new Uint8Array(sp.HEAP);
61 | console.log(this.Heap2Str(HEAPU8));
62 | */
63 | // bargraph
64 | node.outputsTimer = 5;
65 | node.outputsItems = [];
66 |
67 | // input items
68 | node.inputsItems = [];
69 |
70 | // Start of HEAP index
71 |
72 | // DSP is placed first with index 0. Audio buffer start at the end of DSP.
73 | node.$audioHeap = node.voices ? 0 : node.dspMeta.size;
74 |
75 | // Setup pointers offset
76 | node.$$audioHeapInputs = node.$audioHeap;
77 | node.$$audioHeapOutputs = node.$$audioHeapInputs + node.numIn * node.ptrSize;
78 | if (node.voices) {
79 | node.$$audioHeapMixing = node.$$audioHeapOutputs + node.numOut * node.ptrSize;
80 | // Setup buffer offset
81 | node.$audioHeapInputs = node.$$audioHeapMixing + node.numOut * node.ptrSize;
82 | node.$audioHeapOutputs = node.$audioHeapInputs + node.numIn * node.bufferSize * node.sampleSize;
83 | node.$audioHeapMixing = node.$audioHeapOutputs + node.numOut * node.bufferSize * node.sampleSize;
84 | node.$dsp = node.$audioHeapMixing + node.numOut * node.bufferSize * node.sampleSize;
85 | } else {
86 | node.$audioHeapInputs = node.$$audioHeapOutputs + node.numOut * node.ptrSize;
87 | node.$audioHeapOutputs = node.$audioHeapInputs + node.numIn * node.bufferSize * node.sampleSize;
88 | // Start of DSP memory : Mono DSP is placed first with index 0
89 | node.$dsp = 0;
90 | }
91 |
92 | if (node.voices) {
93 | node.effectMeta = compiledDsp.effectMeta;
94 | node.$mixing = null;
95 | node.fFreqLabel$ = [];
96 | node.fKeyLabel$ = [];
97 | node.fGateLabel$ = [];
98 | node.fGainLabel$ = [];
99 | node.fVelLabel$ = [];
100 | node.fDate = 0;
101 |
102 | node.mixer = mixerInstance.exports as FaustWebAssemblyMixerExports;
103 | node.effect = effectInstance ? effectInstance.exports as FaustWebAssemblyExports : null;
104 | this.faust.log(node.mixer);
105 | this.faust.log(node.factory);
106 | this.faust.log(node.effect);
107 | // Start of DSP memory ('polyphony' DSP voices)
108 | node.dspVoices$ = [];
109 | node.dspVoicesState = [];
110 | node.dspVoicesLevel = [];
111 | node.dspVoicesDate = [];
112 |
113 | node.kActiveVoice = 0;
114 | node.kFreeVoice = -1;
115 | node.kReleaseVoice = -2;
116 | node.kNoVoice = -3;
117 |
118 | for (let i = 0; i < node.voices; i++) {
119 | node.dspVoices$[i] = node.$dsp + i * node.dspMeta.size;
120 | node.dspVoicesState[i] = node.kFreeVoice;
121 | node.dspVoicesLevel[i] = 0;
122 | node.dspVoicesDate[i] = 0;
123 | }
124 | // Effect memory starts after last voice
125 | node.$effect = node.dspVoices$[node.voices - 1] + node.dspMeta.size;
126 | }
127 |
128 | node.pathTable$ = {};
129 |
130 | node.$buffer = 0;
131 | node.cachedEvents = [];
132 | node.plotHandler = plotHandler;
133 |
134 | node.updateOutputs = () => {
135 | if (node.outputsItems.length > 0 && node.outputHandler && node.outputsTimer-- === 0) {
136 | node.outputsTimer = 5;
137 | node.outputsItems.forEach(item => node.outputHandler(item, node.factory.getParamValue(node.$dsp, node.pathTable$[item])));
138 | }
139 | };
140 |
141 | // JSON parsing
142 | node.parseUI = ui => ui.forEach(group => node.parseGroup(group));
143 | node.parseGroup = group => (group.items ? node.parseItems(group.items) : null);
144 | node.parseItems = items => items.forEach(item => node.parseItem(item));
145 | node.parseItem = (item) => {
146 | if (item.type === "vgroup" || item.type === "hgroup" || item.type === "tgroup") {
147 | node.parseItems(item.items);
148 | } else if (item.type === "hbargraph" || item.type === "vbargraph") {
149 | // Keep bargraph adresses
150 | node.outputsItems.push(item.address);
151 | node.pathTable$[item.address] = item.index;
152 | } else if (item.type === "vslider" || item.type === "hslider" || item.type === "button" || item.type === "checkbox" || item.type === "nentry") {
153 | // Keep inputs adresses
154 | node.inputsItems.push(item.address);
155 | node.pathTable$[item.address] = item.index;
156 | if (!item.meta) return;
157 | item.meta.forEach((meta) => {
158 | const { midi } = meta;
159 | if (!midi) return;
160 | const strMidi = midi.trim();
161 | if (strMidi === "pitchwheel") {
162 | node.fPitchwheelLabel.push({ path: item.address, min: item.min, max: item.max });
163 | } else {
164 | const matched = strMidi.match(/^ctrl\s(\d+)/);
165 | if (!matched) return;
166 | node.fCtrlLabel[parseInt(matched[1])].push({ path: item.address, min: item.min, max: item.max });
167 | }
168 | });
169 | }
170 | };
171 |
172 | if (node.voices) {
173 | node.getPlayingVoice = (pitch) => {
174 | let voice = node.kNoVoice;
175 | let oldestDatePlaying = Number.MAX_VALUE;
176 | for (let i = 0; i < node.voices; i++) {
177 | if (node.dspVoicesState[i] === pitch) {
178 | // Keeps oldest playing voice
179 | if (node.dspVoicesDate[i] < oldestDatePlaying) {
180 | oldestDatePlaying = node.dspVoicesDate[i];
181 | voice = i;
182 | }
183 | }
184 | }
185 | return voice;
186 | };
187 | // Always returns a voice
188 | node.allocVoice = (voice) => {
189 | // so that envelop is always re-initialized
190 | node.factory.instanceClear(node.dspVoices$[voice]);
191 | node.dspVoicesDate[voice] = node.fDate++;
192 | node.dspVoicesState[voice] = node.kActiveVoice;
193 | return voice;
194 | };
195 | node.getFreeVoice = () => {
196 | for (let i = 0; i < node.voices; i++) {
197 | if (node.dspVoicesState[i] === node.kFreeVoice) return node.allocVoice(i);
198 | }
199 | let voiceRelease = node.kNoVoice;
200 | let voicePlaying = node.kNoVoice;
201 | let oldestDateRelease = Number.MAX_VALUE;
202 | let oldestDatePlaying = Number.MAX_VALUE;
203 | for (let i = 0; i < node.voices; i++) { // Scan all voices
204 | // Try to steal a voice in kReleaseVoice mode...
205 | if (node.dspVoicesState[i] === node.kReleaseVoice) {
206 | // Keeps oldest release voice
207 | if (node.dspVoicesDate[i] < oldestDateRelease) {
208 | oldestDateRelease = node.dspVoicesDate[i];
209 | voiceRelease = i;
210 | }
211 | } else if (node.dspVoicesDate[i] < oldestDatePlaying) {
212 | oldestDatePlaying = node.dspVoicesDate[i];
213 | voicePlaying = i;
214 | }
215 | }
216 | // Then decide which one to steal
217 | if (oldestDateRelease !== Number.MAX_VALUE) {
218 | this.faust.log(`Steal release voice : voice_date = ${node.dspVoicesDate[voiceRelease]} cur_date = ${node.fDate} voice = ${voiceRelease}`);
219 | return node.allocVoice(voiceRelease);
220 | }
221 | if (oldestDatePlaying !== Number.MAX_VALUE) {
222 | this.faust.log(`Steal playing voice : voice_date = ${node.dspVoicesDate[voicePlaying]} cur_date = ${node.fDate} voice = ${voicePlaying}`);
223 | return node.allocVoice(voicePlaying);
224 | }
225 | return node.kNoVoice;
226 | };
227 | node.keyOn = (channel, pitch, velocity) => {
228 | node.cachedEvents.push({ type: "keyOn", data: [channel, pitch, velocity] });
229 | const voice = node.getFreeVoice();
230 | this.faust.log("keyOn voice " + voice);
231 | node.fFreqLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, midiToFreq(pitch)));
232 | node.fKeyLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, pitch));
233 | node.fGateLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, 1));
234 | node.fGainLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, velocity / 127));
235 | node.fVelLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, velocity));
236 | node.dspVoicesState[voice] = pitch;
237 | };
238 | node.keyOff = (channel, pitch, velocity) => { // eslint-disable-line @typescript-eslint/no-unused-vars
239 | node.cachedEvents.push({ type: "keyOff", data: [channel, pitch, velocity] });
240 | const voice = node.getPlayingVoice(pitch);
241 | if (voice === node.kNoVoice) return this.faust.log("Playing voice not found...");
242 | node.fGateLabel$.forEach($ => node.factory.setParamValue(node.dspVoices$[voice], $, 0)); // No use of velocity for now...
243 | node.dspVoicesState[voice] = node.kReleaseVoice; // Release voice
244 | return this.faust.log("keyOff voice " + voice);
245 | };
246 | node.allNotesOff = () => {
247 | node.cachedEvents.push({ type: "ctrlChange", data: [0, 123, 0] });
248 | for (let i = 0; i < node.voices; i++) {
249 | node.fGateLabel$.forEach($gate => node.factory.setParamValue(node.dspVoices$[i], $gate, 0));
250 | node.dspVoicesState[i] = node.kReleaseVoice;
251 | }
252 | };
253 | }
254 | node.midiMessage = (data) => {
255 | node.cachedEvents.push({ data, type: "midi" });
256 | const cmd = data[0] >> 4;
257 | const channel = data[0] & 0xf;
258 | const data1 = data[1];
259 | const data2 = data[2];
260 | if (channel === 9) return undefined;
261 | if (node.voices) {
262 | if (cmd === 8 || (cmd === 9 && data2 === 0)) return node.keyOff(channel, data1, data2);
263 | if (cmd === 9) return node.keyOn(channel, data1, data2);
264 | }
265 | if (cmd === 11) return node.ctrlChange(channel, data1, data2);
266 | if (cmd === 14) return node.pitchWheel(channel, (data2 * 128.0 + data1));
267 | return undefined;
268 | };
269 | node.ctrlChange = (channel, ctrl, value) => {
270 | node.cachedEvents.push({ type: "ctrlChange", data: [channel, ctrl, value] });
271 | if (ctrl === 123 || ctrl === 120) {
272 | node.allNotesOff();
273 | }
274 | if (!node.fCtrlLabel[ctrl].length) return;
275 | node.fCtrlLabel[ctrl].forEach((ctrl) => {
276 | const { path } = ctrl;
277 | node.setParamValue(path, remap(value, 0, 127, ctrl.min, ctrl.max));
278 | if (node.outputHandler) node.outputHandler(path, node.getParamValue(path));
279 | });
280 | };
281 | node.pitchWheel = (channel, wheel) => {
282 | node.cachedEvents.push({ type: "pitchWheel", data: [channel, wheel] });
283 | node.fPitchwheelLabel.forEach((pw) => {
284 | node.setParamValue(pw.path, remap(wheel, 0, 16383, pw.min, pw.max));
285 | if (node.outputHandler) node.outputHandler(pw.path, node.getParamValue(pw.path));
286 | });
287 | };
288 | node.compute = (e) => {
289 | if (node.destroyed) return false;
290 | for (let i = 0; i < node.numIn; i++) { // Read inputs
291 | const input = e.inputBuffer.getChannelData(i);
292 | const dspInput = node.dspInChannnels[i];
293 | dspInput.set(input);
294 | }
295 | // Possibly call an externally given callback (for instance to synchronize playing a MIDIFile...)
296 | if (node.computeHandler) node.computeHandler(node.bufferSize);
297 | if (node.voices) {
298 | node.mixer.clearOutput(node.bufferSize, node.numOut, node.$outs); // First clear the outputs
299 | for (let i = 0; i < node.voices; i++) { // Compute all running voices
300 | node.factory.compute(node.dspVoices$[i], node.bufferSize, node.$ins, node.$mixing); // Compute voice
301 | node.mixer.mixVoice(node.bufferSize, node.numOut, node.$mixing, node.$outs); // Mix it in result
302 | }
303 | if (node.effect) node.effect.compute(node.$effect, node.bufferSize, node.$outs, node.$outs); // Apply effect. Not a typo, effect is applied on the outs.
304 | } else {
305 | node.factory.compute(node.$dsp, node.bufferSize, node.$ins, node.$outs); // Compute
306 | }
307 | node.updateOutputs(); // Update bargraph
308 | const outputs = new Array(node.numOut).fill(null).map(() => new Float32Array(node.bufferSize));
309 | for (let i = 0; i < node.numOut; i++) { // Write outputs
310 | const output = e.outputBuffer.getChannelData(i);
311 | const dspOutput = node.dspOutChannnels[i];
312 | output.set(dspOutput);
313 | outputs[i].set(dspOutput);
314 | }
315 | if (node.plotHandler) node.plotHandler(outputs, node.$buffer++, node.cachedEvents.length ? node.cachedEvents : undefined);
316 | node.cachedEvents = [];
317 | return true;
318 | };
319 | node.setup = () => { // Setup web audio context
320 | this.faust.log("buffer_size " + node.bufferSize);
321 | node.onaudioprocess = node.compute;
322 | if (node.numIn > 0) {
323 | node.$ins = node.$$audioHeapInputs;
324 | for (let i = 0; i < node.numIn; i++) {
325 | node.HEAP32[(node.$ins >> 2) + i] = node.$audioHeapInputs + node.bufferSize * node.sampleSize * i;
326 | }
327 | // Prepare Ins buffer tables
328 | const dspInChans = node.HEAP32.subarray(node.$ins >> 2, (node.$ins + node.numIn * node.ptrSize) >> 2);
329 | for (let i = 0; i < node.numIn; i++) {
330 | node.dspInChannnels[i] = node.HEAPF32.subarray(dspInChans[i] >> 2, (dspInChans[i] + node.bufferSize * node.sampleSize) >> 2);
331 | }
332 | }
333 | if (node.numOut > 0) {
334 | node.$outs = node.$$audioHeapOutputs;
335 | if (node.voices) node.$mixing = node.$$audioHeapMixing;
336 | for (let i = 0; i < node.numOut; i++) {
337 | node.HEAP32[(node.$outs >> 2) + i] = node.$audioHeapOutputs + node.bufferSize * node.sampleSize * i;
338 | if (node.voices) node.HEAP32[(node.$mixing >> 2) + i] = node.$audioHeapMixing + node.bufferSize * node.sampleSize * i;
339 | }
340 | // Prepare Out buffer tables
341 | const dspOutChans = node.HEAP32.subarray(node.$outs >> 2, (node.$outs + node.numOut * node.ptrSize) >> 2);
342 | for (let i = 0; i < node.numOut; i++) {
343 | node.dspOutChannnels[i] = node.HEAPF32.subarray(dspOutChans[i] >> 2, (dspOutChans[i] + node.bufferSize * node.sampleSize) >> 2);
344 | }
345 | }
346 | // Parse JSON UI part
347 | node.parseUI(node.dspMeta.ui);
348 | if (node.effect) node.parseUI(node.effectMeta.ui);
349 |
350 | // keep 'keyOn/keyOff' labels
351 | if (node.voices) {
352 | node.inputsItems.forEach((item) => {
353 | if (item.endsWith("/gate")) node.fGateLabel$.push(node.pathTable$[item]);
354 | else if (item.endsWith("/freq")) node.fFreqLabel$.push(node.pathTable$[item]);
355 | else if (item.endsWith("/key")) node.fKeyLabel$.push(node.pathTable$[item]);
356 | else if (item.endsWith("/gain")) node.fGainLabel$.push(node.pathTable$[item]);
357 | else if (item.endsWith("/vel") || item.endsWith("/velocity")) node.fVelLabel$.push(node.pathTable$[item]);
358 | });
359 | // Init DSP voices
360 | node.dspVoices$.forEach($voice => node.factory.init($voice, audioCtx.sampleRate));
361 | // Init effect
362 | if (node.effect) node.effect.init(node.$effect, audioCtx.sampleRate);
363 | } else {
364 | // Init DSP
365 | node.factory.init(node.$dsp, audioCtx.sampleRate);
366 | }
367 | };
368 | node.getSampleRate = () => audioCtx.sampleRate;
369 | node.getNumInputs = () => node.numIn;
370 | node.getNumOutputs = () => node.numOut;
371 | node.init = (sampleRate) => {
372 | if (node.voices) node.dspVoices$.forEach($voice => node.factory.init($voice, sampleRate));
373 | else node.factory.init(node.$dsp, sampleRate);
374 | };
375 | node.instanceInit = (sampleRate) => {
376 | if (node.voices) node.dspVoices$.forEach($voice => node.factory.instanceInit($voice, sampleRate));
377 | else node.factory.instanceInit(node.$dsp, sampleRate);
378 | };
379 | node.instanceConstants = (sampleRate) => {
380 | if (node.voices) node.dspVoices$.forEach($voice => node.factory.instanceConstants($voice, sampleRate));
381 | else node.factory.instanceConstants(node.$dsp, sampleRate);
382 | };
383 | node.instanceResetUserInterface = () => {
384 | if (node.voices) node.dspVoices$.forEach($voice => node.factory.instanceResetUserInterface($voice));
385 | else node.factory.instanceResetUserInterface(node.$dsp);
386 | };
387 | node.instanceClear = () => {
388 | if (node.voices) node.dspVoices$.forEach($voice => node.factory.instanceClear($voice));
389 | else node.factory.instanceClear(node.$dsp);
390 | };
391 | node.metadata = handler => (node.dspMeta.meta ? node.dspMeta.meta.forEach(meta => handler.declare(Object.keys(meta)[0], meta[Object.keys(meta)[0]])) : undefined);
392 | node.setOutputParamHandler = handler => node.outputHandler = handler;
393 | node.getOutputParamHandler = () => node.outputHandler;
394 | node.setComputeHandler = handler => node.computeHandler = handler;
395 | node.getComputeHandler = () => node.computeHandler;
396 | const findPath = (o: any, p: string) => {
397 | if (typeof o !== "object") return false;
398 | if (o.address) {
399 | if (o.address === p) return true;
400 | return false;
401 | }
402 | for (const k in o) {
403 | if (findPath(o[k], p)) return true;
404 | }
405 | return false;
406 | };
407 | node.setParamValue = (path, value) => {
408 | node.cachedEvents.push({ type: "param", data: { path, value } });
409 | if (node.voices) {
410 | if (node.effect && findPath(node.effectMeta.ui, path)) node.effect.setParamValue(node.$effect, node.pathTable$[path], value);
411 | else node.dspVoices$.forEach($voice => node.factory.setParamValue($voice, node.pathTable$[path], value));
412 | } else {
413 | node.factory.setParamValue(node.$dsp, node.pathTable$[path], value);
414 | }
415 | };
416 | node.getParamValue = (path) => {
417 | if (node.voices) {
418 | if (node.effect && findPath(node.effectMeta.ui, path)) return node.effect.getParamValue(node.$effect, node.pathTable$[path]);
419 | return node.factory.getParamValue(node.dspVoices$[0], node.pathTable$[path]);
420 | }
421 | return node.factory.getParamValue(node.$dsp, node.pathTable$[path]);
422 | };
423 | node.getParams = () => node.inputsItems;
424 | node.getJSON = () => {
425 | if (node.voices) {
426 | const o = node.dspMeta;
427 | const e = node.effectMeta;
428 | const r = { ...o };
429 | if (e) {
430 | r.ui = [{ type: "tgroup", label: "Sequencer", items: [
431 | { type: "vgroup", label: "Instrument", items: o.ui },
432 | { type: "vgroup", label: "Effect", items: e.ui }
433 | ] }];
434 | } else {
435 | r.ui = [{ type: "tgroup", label: "Polyphonic", items: [
436 | { type: "vgroup", label: "Voices", items: o.ui }
437 | ] }];
438 | }
439 | return JSON.stringify(r);
440 | }
441 | return JSON.stringify(node.dspMeta);
442 | };
443 | node.getUI = () => {
444 | if (node.voices) {
445 | const o = node.dspMeta;
446 | const e = node.effectMeta;
447 | if (e) {
448 | return [{ type: "tgroup", label: "Sequencer", items: [
449 | { type: "vgroup", label: "Instrument", items: o.ui },
450 | { type: "vgroup", label: "Effect", items: e.ui }
451 | ] }];
452 | }
453 | return [{ type: "tgroup", label: "Polyphonic", items: [
454 | { type: "vgroup", label: "Voices", items: o.ui }
455 | ] }];
456 | }
457 | return node.dspMeta.ui;
458 | };
459 | node.destroy = () => {
460 | node.destroyed = true;
461 | delete node.outputHandler;
462 | delete node.computeHandler;
463 | delete node.plotHandler;
464 | };
465 | // Init resulting DSP
466 | node.setup();
467 | return node;
468 | }
469 | /**
470 | * Create a ScriptProcessorNode Web Audio object
471 | * by loading and compiling the Faust wasm file
472 | *
473 | * @param {TAudioNodeOptions} optionsIn
474 | * @returns {Promise} a Promise for valid WebAudio ScriptProcessorNode object or null
475 | */
476 | async getNode(optionsIn: TAudioNodeOptions): Promise {
477 | const { compiledDsp, audioCtx, bufferSize: bufferSizeIn, voices, plotHandler } = optionsIn;
478 | const bufferSize = bufferSizeIn || 512;
479 | let node: FaustScriptProcessorNode;
480 | try {
481 | let effectInstance: WebAssembly.Instance;
482 | let mixerInstance: WebAssembly.Instance;
483 | const memory = createWasmMemory(voices, compiledDsp.dspMeta, compiledDsp.effectMeta, bufferSize);
484 | const importObject = createWasmImport(voices, memory);
485 | if (voices) {
486 | const mixerObject = { imports: { print: console.log }, memory: { memory } }; // eslint-disable-line no-console
487 | mixerInstance = new WebAssembly.Instance(mixer32Module, mixerObject);
488 | try {
489 | effectInstance = await WebAssembly.instantiate(compiledDsp.effectModule, importObject);
490 | } catch (e) {} // eslint-disable-line no-empty
491 | }
492 | const dspInstance = await WebAssembly.instantiate(compiledDsp.dspModule, importObject);
493 | node = this.initNode(compiledDsp, dspInstance, effectInstance, mixerInstance, audioCtx, bufferSize, memory, voices, plotHandler);
494 | } catch (e) {
495 | this.faust.error("Faust " + compiledDsp.shaKey + " cannot be loaded or compiled");
496 | throw e;
497 | }
498 | return node;
499 | }
500 | }
501 |
--------------------------------------------------------------------------------
/src/LibFaustLoader.d.ts:
--------------------------------------------------------------------------------
1 | interface FS {
2 | ignorePermissions: boolean;
3 | trackingDelegate: any;
4 | tracking: any;
5 | genericErrors: any;
6 |
7 | //
8 | // paths
9 | //
10 | lookupPath(path: string, opts: any): FS.Lookup;
11 | getPath(node: FS.FSNode): string;
12 |
13 | //
14 | // nodes
15 | //
16 | isFile(mode: number): boolean;
17 | isDir(mode: number): boolean;
18 | isLink(mode: number): boolean;
19 | isChrdev(mode: number): boolean;
20 | isBlkdev(mode: number): boolean;
21 | isFIFO(mode: number): boolean;
22 | isSocket(mode: number): boolean;
23 |
24 | //
25 | // devices
26 | //
27 | major(dev: number): number;
28 | minor(dev: number): number;
29 | makedev(ma: number, mi: number): number;
30 | registerDevice(dev: number, ops: any): void;
31 |
32 | //
33 | // core
34 | //
35 | syncfs(populate: boolean, callback: (e: any) => any): void;
36 | syncfs(callback: (e: any) => any, populate?: boolean): void;
37 | mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
38 | unmount(mountpoint: string): void;
39 |
40 | mkdir(path: string, mode?: number): any;
41 | mkdev(path: string, mode?: number, dev?: number): any;
42 | symlink(oldpath: string, newpath: string): any;
43 | rename(oldpath: string, newpath: string): void;
44 | rmdir(path: string): void;
45 | readdir(path: string): any;
46 | unlink(path: string): void;
47 | readlink(path: string): string;
48 | stat(path: string, dontFollow?: boolean): any;
49 | lstat(path: string): any;
50 | chmod(path: string, mode: number, dontFollow?: boolean): void;
51 | lchmod(path: string, mode: number): void;
52 | fchmod(fd: number, mode: number): void;
53 | chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
54 | lchown(path: string, uid: number, gid: number): void;
55 | fchown(fd: number, uid: number, gid: number): void;
56 | truncate(path: string, len: number): void;
57 | ftruncate(fd: number, len: number): void;
58 | utime(path: string, atime: number, mtime: number): void;
59 | open(path: string, flags: string, mode?: number, fdstart?: number, fdend?: number): FS.FSStream;
60 | close(stream: FS.FSStream): void;
61 | llseek(stream: FS.FSStream, offset: number, whence: number): any;
62 | read(stream: FS.FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
63 | write(stream: FS.FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number, canOwn?: boolean): number;
64 | allocate(stream: FS.FSStream, offset: number, length: number): void;
65 | mmap(stream: FS.FSStream, buffer: ArrayBufferView, offset: number, length: number, position: number, prot: number, flags: number): any;
66 | ioctl(stream: FS.FSStream, cmd: any, arg: any): any;
67 | readFile(path: string, opts?: { encoding?: "binary" | "utf8"; flags?: string }): string | Uint8Array;
68 | writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string }): void;
69 |
70 | //
71 | // module-level FS code
72 | //
73 | cwd(): string;
74 | chdir(path: string): void;
75 | init(
76 | input: null | (() => number | null),
77 | output: null | ((c: number) => any),
78 | error: null | ((c: number) => any),
79 | ): void;
80 |
81 | createLazyFile(parent: string | FS.FSNode, name: string, url: string, canRead: boolean, canWrite: boolean): FS.FSNode;
82 | createPreloadedFile(parent: string | FS.FSNode, name: string, url: string,
83 | canRead: boolean, canWrite: boolean, onload?: () => void, onerror?: () => void, dontCreateFile?: boolean, canOwn?: boolean): void;
84 | }
85 | declare interface LibFaust extends EmscriptenModule {
86 | cwrap(name: string, type: string, args: string[]): (...args: any[]) => any;
87 | UTF8ArrayToString(u8Array: number[], ptr: number, maxBytesToRead?: number): string;
88 | stringToUTF8Array(str: string, outU8Array: number[], outIdx: number, maxBytesToWrite: number): number;
89 | UTF8ToString(ptr: number, maxBytesToRead?: number): string;
90 | stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
91 | lengthBytesUTF8(str: string): number;
92 | allocateUTF8(str: string): number;
93 | UTF16ToString(ptr: number): string;
94 | stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
95 | lengthBytesUTF16(str: string): number;
96 | UTF32ToString(ptr: number): string;
97 | stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
98 | lengthBytesUTF32(str: string): number;
99 | // Undocumented Promise-like, has issue in https://github.com/emscripten-core/emscripten/issues/5820
100 | // then(func: (module: any) => any): LibFaust;
101 | FS: FS;
102 | }
103 | declare function FaustModule(FaustModule: LibFaust, ...args: any[]): LibFaust;
104 | export class LibFaustLoader {
105 | static load(wasmLocation: string, dataLocation: string): Promise;
106 | }
107 |
--------------------------------------------------------------------------------
/src/LibFaustLoader.js:
--------------------------------------------------------------------------------
1 | import * as LibFaust from "./libfaust-wasm";
2 |
3 | class LibFaustLoader {
4 | static async load(wasmLocation, dataLocation) {
5 | const locateFile = (path, dir) => ({
6 | "libfaust-wasm.wasm": wasmLocation,
7 | "libfaust-wasm.data": dataLocation
8 | }[path]) || dir + path;
9 | const libFaust = await LibFaust({ locateFile });
10 | return libFaust;
11 | }
12 | }
13 | export { LibFaust, LibFaustLoader };
14 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Faust } from "./Faust";
2 | export { FaustAudioWorkletNode } from "./FaustAudioWorkletNode";
3 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | AudioWorkletNode?: AudioWorkletNode;
4 | }
5 | }
6 | export type TDspMeta = {
7 | name: string;
8 | filename: string;
9 | compile_options: string;
10 | include_pathnames: string[];
11 | inputs: number;
12 | outputs: number;
13 | size: number;
14 | version: string;
15 | library_list: string[];
16 | meta: { [key: string]: string }[];
17 | ui: TFaustUI;
18 | };
19 | export type TFaustUI = TFaustUIGroup[];
20 | export type TFaustUIItem = TFaustUIInputItem | TFaustUIOutputItem | TFaustUIGroup;
21 | export type TFaustUIInputItem = {
22 | type: TFaustUIInputType;
23 | label: string;
24 | address: string;
25 | index: number;
26 | init?: number;
27 | min?: number;
28 | max?: number;
29 | step?: number;
30 | meta?: TFaustUIMeta[];
31 | };
32 | export type TFaustUIOutputItem = {
33 | type: TFaustUIOutputType;
34 | label: string;
35 | address: string;
36 | index: number;
37 | min?: number;
38 | max?: number;
39 | meta?: TFaustUIMeta[];
40 | };
41 | type TFaustUIMeta = {
42 | [order: number]: string;
43 | style?: string; // "knob" | "menu{'Name0':value0;'Name1':value1}" | "radio{'Name0':value0;'Name1':value1}" | "led";
44 | unit?: string;
45 | scale?: "linear" | "exp" | "log";
46 | tooltip?: string;
47 | hidden?: string;
48 | [key: string]: string;
49 | };
50 | export type TFaustUIGroupType = "vgroup" | "hgroup" | "tgroup";
51 | export type TFaustUIOutputType = "hbargraph" | "vbargraph";
52 | export type TFaustUIInputType = "vslider" | "hslider" | "button" | "checkbox" | "nentry";
53 | export type TFaustUIGroup = {
54 | type: TFaustUIGroupType;
55 | label: string;
56 | items: TFaustUIItem[];
57 | };
58 | export type TFaustUIType = TFaustUIGroupType | TFaustUIOutputType | TFaustUIInputType;
59 | export type TCompiledCode = { ui8Code: ArrayBuffer; code: string; helpersCode: string };
60 | export type TCompiledStrCode = { strCode: string; code: string; helpersCode: string };
61 | export type TCompiledCodes = { dsp: TCompiledCode; effect?: TCompiledCode};
62 | export type TCompiledStrCodes = { dsp: TCompiledStrCode; effect?: TCompiledStrCode};
63 | export type THelpers = { json: string; base64Code: string; meta: TDspMeta };
64 | export type TCompiledDsp = {
65 | shaKey: string;
66 | dspModule: WebAssembly.Module;
67 | dspMeta: TDspMeta;
68 | effectModule?: WebAssembly.Module;
69 | effectMeta?: TDspMeta;
70 | codes: TCompiledCodes;
71 | };
72 | export type TFaustCompileArgs = {
73 | /**
74 | * Flush to zero the code added to recursive signals [0:no (default), 1:fabs based, 2:mask based (fastest)]
75 | *
76 | * @type {(0 | 1 | 2)}
77 | */
78 | "-ftz"?: 0 | 1 | 2;
79 | /**
80 | * Add the directory to the import search path
81 | *
82 | * @type {string | string[]}
83 | */
84 | "-I"?: string | string[];
85 | [key: string]: number | string | string[];
86 | };
87 | export type TFaustCompileOptions = {
88 | audioCtx: AudioContext;
89 | useWorklet?: boolean;
90 | voices?: number;
91 | bufferSize?: 128 | 256 | 512 | 1024 | 2048 | 4096;
92 | args?: TFaustCompileArgs;
93 | /**
94 | * handler for plotted samples
95 | *
96 | * @type {(plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any}
97 | */
98 | plotHandler?: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any;
99 | };
100 | export type TAudioNodeOptions = {
101 | /**
102 | * DSP compiled by libfaust
103 | *
104 | * @type {TCompiledDsp}
105 | */
106 | compiledDsp: TCompiledDsp;
107 | audioCtx: AudioContext;
108 | /**
109 | * Polyphony voices, 0 or undefined for mono DSP
110 | *
111 | * @type {number}
112 | */
113 | voices?: number;
114 | /**
115 | * - the bufferSize in frames
116 | *
117 | * @type {(128 | 256 | 512 | 1024 | 2048 | 4096)}
118 | */
119 | bufferSize?: 128 | 256 | 512 | 1024 | 2048 | 4096;
120 | /**
121 | * handler for plotted samples
122 | *
123 | * @type {(plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any}
124 | */
125 | plotHandler?: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any;
126 | };
127 |
128 | export interface FaustWebAssemblyExports extends WebAssembly.Exports {
129 | getParamValue($dsp: number, $param: number): number;
130 | setParamValue($dsp: number, $param: number, val: number): void;
131 | instanceClear($dsp: number): any;
132 | instanceResetUserInterface($dsp: number): void;
133 | instanceConstants($dsp: number, sampleRate: number): void;
134 | init($dsp: number, sampleRate: number): void;
135 | instanceInit($dsp: number, sampleRate: number): void;
136 | compute($dsp: number, bufferSize: number, $ins: number, $outs: number): any;
137 | memory: WebAssembly.Memory;
138 | }
139 |
140 | export interface FaustWebAssemblyMixerExports extends WebAssembly.Exports {
141 | clearOutput(count: number, channels: number, $outputs: number): void;
142 | mixVoice(count: number, channels: number, $inputs: number, $outputs: number): number;
143 | }
144 |
145 | export interface FaustDspNode {
146 | destroyed: boolean;
147 | bufferSize: number;
148 | voices?: number;
149 | dspMeta: TDspMeta;
150 | $ins: number;
151 | $outs: number;
152 | dspInChannnels: Float32Array[];
153 | dspOutChannnels: Float32Array[];
154 | fPitchwheelLabel: { path: string; min: number; max: number }[];
155 | fCtrlLabel: { path: string; min: number; max: number }[][];
156 | numIn: number;
157 | numOut: number;
158 | ptrSize: number;
159 | sampleSize: number;
160 | outputsTimer: number;
161 | inputsItems: string[];
162 | outputsItems: string[];
163 | pathTable$: { [address: string]: number };
164 | $audioHeap: number;
165 | $$audioHeapInputs: number;
166 | $$audioHeapOutputs: number;
167 | $audioHeapInputs: number;
168 | $audioHeapOutputs: number;
169 | $dsp: number;
170 | factory: FaustWebAssemblyExports;
171 | HEAP: ArrayBuffer;
172 | HEAP32: Int32Array;
173 | HEAPF32: Float32Array;
174 |
175 | effectMeta?: TDspMeta;
176 | $effect?: number;
177 | $mixing?: number;
178 | fFreqLabel$?: number[];
179 | fGateLabel$?: number[];
180 | fGainLabel$?: number[];
181 | fDate?: number;
182 | $$audioHeapMixing?: number;
183 | $audioHeapMixing?: number;
184 | mixer?: FaustWebAssemblyMixerExports;
185 | effect?: FaustWebAssemblyExports;
186 | dspVoices$?: number[];
187 | dspVoicesState?: number[];
188 | dspVoicesLevel?: number[];
189 | dspVoicesDate?: number[];
190 | kActiveVoice?: number;
191 | kFreeVoice?: number;
192 | kReleaseVoice?: number;
193 | kNoVoice?: number;
194 |
195 | outputHandler: (address: string, value: number) => any;
196 | computeHandler: (bufferSize: number) => any;
197 | updateOutputs(): void;
198 |
199 | parseUI(ui: TFaustUI): void;
200 | parseGroup(group: TFaustUIGroup): void;
201 | parseItems(items: TFaustUIItem[]): void;
202 | parseItem(ui: TFaustUIItem): void;
203 |
204 | /**
205 | * Set control value.
206 | *
207 | * @param {string} path - the path to the wanted control (retrieved using 'getParams' method)
208 | * @param {number} val - the float value for the wanted parameter
209 | * @memberof IFaustDspNode
210 | */
211 | setParamValue(path: string, val: number): void;
212 | /**
213 | * Get control value.
214 | *
215 | * @param {string} path - the path to the wanted control (retrieved using 'controls' method)
216 | * @returns {number} the float value
217 | * @memberof IFaustDspNode
218 | */
219 | getParamValue(path: string): number;
220 | /**
221 | * Setup pointers
222 | *
223 | * @memberof IFaustDspNode
224 | */
225 | setup(): void;
226 |
227 | // Poly methods
228 | getPlayingVoice?(pitch: number): number;
229 | allocVoice?(voice: number): number;
230 | getFreeVoice?(): number;
231 | /**
232 | * Instantiates a new polyphonic voice.
233 | *
234 | * @param {number} channel - the MIDI channel (0..15, not used for now)
235 | * @param {number} pitch - the MIDI pitch (0..127)
236 | * @param {number} velocity - the MIDI velocity (0..127)
237 | * @memberof IFaustDspNode
238 | */
239 | keyOn?(channel: number, pitch: number, velocity: number): void;
240 | /**
241 | * De-instantiates a polyphonic voice.
242 | *
243 | * @param {number} channel - the MIDI channel (0..15, not used for now)
244 | * @param {number} pitch - the MIDI pitch (0..127)
245 | * @param {number} velocity - the MIDI velocity (0..127)
246 | * @memberof IFaustDspNode
247 | */
248 | keyOff?(channel: number, pitch: number, velocity: number): void;
249 | /**
250 | * Gently terminates all the active voices.
251 | *
252 | * @memberof IFaustDspNode
253 | */
254 | allNotesOff?(): void;
255 | /**
256 | * Handle Raw MIDI Messages
257 | *
258 | * @param {number[]} data - MIDI message as array
259 | * @memberof IFaustDspNode
260 | */
261 | midiMessage(data: number[]): void;
262 | /**
263 | * Control change
264 | *
265 | * @param {number} channel - the MIDI channel (0..15, not used for now)
266 | * @param {number} ctrl - the MIDI controller number (0..127)
267 | * @param {number} value - the MIDI controller value (0..127)
268 | * @memberof IFaustDspNode
269 | */
270 | ctrlChange(channel: number, ctrl: number, value: number): void;
271 | /**
272 | * PitchWheel
273 | *
274 | * @param {number} channel - the MIDI channel (0..15, not used for now)
275 | * @param {number} value - the MIDI controller value (-1..1)
276 | * @memberof IFaustDspNode
277 | */
278 | pitchWheel(channel: number, wheel: number): void;
279 | }
280 |
281 | export interface FaustScriptProcessorNode extends ScriptProcessorNode, FaustDspNode {
282 | // From FaustDSPNode interface
283 | bufferSize: number;
284 | voices: number;
285 | dspMeta: TDspMeta;
286 | $ins: number;
287 | $outs: number;
288 | dspInChannnels: Float32Array[];
289 | dspOutChannnels: Float32Array[];
290 | fPitchwheelLabel: { path: string; min: number; max: number }[];
291 | fCtrlLabel: { path: string; min: number; max: number }[][];
292 | numIn: number;
293 | numOut: number;
294 | ptrSize: number;
295 | sampleSize: number;
296 | outputsTimer: number;
297 | inputsItems: string[];
298 | outputsItems: string[];
299 | pathTable$: { [address: string]: number };
300 | $audioHeap: number;
301 | $$audioHeapInputs: number;
302 | $$audioHeapOutputs: number;
303 | $audioHeapInputs: number;
304 | $audioHeapOutputs: number;
305 | $dsp: number;
306 | factory: FaustWebAssemblyExports;
307 | HEAP: ArrayBuffer;
308 | HEAP32: Int32Array;
309 | HEAPF32: Float32Array;
310 |
311 | effectMeta?: TDspMeta;
312 | $effect?: number;
313 | $mixing?: number;
314 | fFreqLabel$?: number[];
315 | fGateLabel$?: number[];
316 | fGainLabel$?: number[];
317 | fDate?: number;
318 | $$audioHeapMixing?: number;
319 | $audioHeapMixing?: number;
320 | mixer?: FaustWebAssemblyMixerExports;
321 | effect?: FaustWebAssemblyExports;
322 | dspVoices$?: number[];
323 | dspVoicesState?: number[];
324 | dspVoicesLevel?: number[];
325 | dspVoicesDate?: number[];
326 | kActiveVoice?: number;
327 | kFreeVoice?: number;
328 | kReleaseVoice?: number;
329 | kNoVoice?: number;
330 |
331 | outputHandler: (address: string, value: number) => any;
332 | computeHandler: (bufferSize: number) => any;
333 | updateOutputs: () => void;
334 |
335 | compute: (e: AudioProcessingEvent) => void;
336 | parseUI: (ui: TFaustUI) => void;
337 | parseGroup: (group: TFaustUIGroup) => void;
338 | parseItems: (items: TFaustUIItem[]) => void;
339 | parseItem: (ui: TFaustUIItem) => void;
340 |
341 | setParamValue: (path: string, val: number) => void;
342 | getParamValue: (path: string) => number;
343 |
344 | setup: () => void;
345 |
346 | getPlayingVoice?: (pitch: number) => number;
347 | allocVoice?: (voice: number) => number;
348 | getFreeVoice?: () => number;
349 | keyOn?: (channel: number, pitch: number, velocity: number) => void;
350 | keyOff?: (channel: number, pitch: number, velocity: number) => void;
351 | allNotesOff?: () => void;
352 |
353 | midiMessage: (data: number[] | Uint8Array) => void;
354 | ctrlChange: (channel: number, ctrl: number, value: number) => void;
355 | pitchWheel: (channel: number, wheel: number) => void;
356 |
357 | /**
358 | * Return current sample rate.
359 | *
360 | * @returns {number} current sample rate
361 | * @memberof FaustScriptProcessorNode
362 | */
363 | getSampleRate: () => number;
364 | /**
365 | * Return instance number of audio inputs.
366 | *
367 | * @returns {number} instance number of audio inputs
368 | * @memberof FaustScriptProcessorNode
369 | */
370 | getNumInputs: () => number;
371 | /**
372 | * Return instance number of audio outputs.
373 | *
374 | * @returns {number} instance number of audio outputs
375 | * @memberof FaustScriptProcessorNode
376 | */
377 | getNumOutputs: () => number;
378 | /**
379 | * Global init, doing the following initialization:
380 | * - static tables initialization
381 | * - call 'instanceInit': constants and instance state initialisation
382 | *
383 | * @param {number} sampleRate - the sampling rate in Hertz
384 | * @memberof FaustScriptProcessorNode
385 | */
386 | init: (sampleRate: number) => void;
387 | /**
388 | * Init instance state.
389 | *
390 | * @param {number} sampleRate - the sampling rate in Hertz
391 | * @memberof FaustScriptProcessorNode
392 | */
393 | instanceInit: (sampleRate: number) => void;
394 | /**
395 | * Init instance constant state.
396 | *
397 | * @param {number} sampleRate - the sampling rate in Hertz
398 | * @memberof FaustScriptProcessorNode
399 | */
400 | instanceConstants: (sampleRate: number) => void;
401 | /**
402 | * Init default control parameters values.
403 | *
404 | * @memberof FaustScriptProcessorNode
405 | */
406 | instanceResetUserInterface: () => void;
407 | /**
408 | * Init instance state (delay lines...).
409 | *
410 | * @memberof FaustScriptProcessorNode
411 | */
412 | instanceClear: () => any;
413 | /**
414 | * Trigger the Meta handler with instance specific calls to 'declare' (key, value) metadata.
415 | *
416 | * @param {{ declare: (key: string, value: any) => any }} handler
417 | * @memberof FaustScriptProcessorNode
418 | */
419 | metadata: (handler: { declare: (key: string, value: any) => any }) => void;
420 | /**
421 | * Setup a control output handler with a function of type (path, value)
422 | * to be used on each generated output value. This handler will be called
423 | * each audio cycle at the end of the 'compute' method.
424 | *
425 | * @param {(path: string, value: number) => any} handler - a function of type function(path, value)
426 | * @memberof FaustScriptProcessorNode
427 | */
428 | setOutputParamHandler: (handler: (path: string, value: number) => any) => void;
429 | /**
430 | * Get the current output handler.
431 | *
432 | * @returns {(path: string, value: number) => any} handler - a function of type function(path, value)
433 | * @memberof FaustScriptProcessorNode
434 | */
435 | getOutputParamHandler: () => (path: string, value: number) => any;
436 | /**
437 | * Set a compute handler to be called each audio cycle
438 | * (for instance to synchronize playing a MIDIFile...).
439 | *
440 | * @param {(bufferSize: number) => any} handler - a function of type function(buffer_size)
441 | * @memberof FaustScriptProcessorNode
442 | */
443 | setComputeHandler: (handler: (bufferSize: number) => any) => void;
444 | /**
445 | * Get the current compute handler.
446 | *
447 | * @memberof FaustScriptProcessorNode
448 | */
449 | getComputeHandler: () => (bufferSize: number) => any;
450 | /**
451 | * Get the table of all input parameters paths.
452 | *
453 | * @returns {object} the table of all input parameter paths.
454 | * @memberof FaustScriptProcessorNode
455 | */
456 | getParams: () => string[];
457 | /**
458 | * Get DSP JSON description with its UI and metadata
459 | *
460 | * @returns {string} DSP JSON description
461 | * @memberof FaustScriptProcessorNode
462 | */
463 | getJSON: () => string;
464 | /**
465 | * Get DSP UI description
466 | *
467 | * @returns {TFaustUI} DSP UI description
468 | * @memberof FaustScriptProcessorNode
469 | */
470 | getUI: () => TFaustUI;
471 |
472 | $buffer: number;
473 | cachedEvents: { type: string; data: any }[];
474 | plotHandler: (plotted: Float32Array[], index: number, events?: { type: string; data: any }[]) => any;
475 | /**
476 | * Called on destroy
477 | *
478 | * @memberof FaustScriptProcessorNode
479 | */
480 | destroy: () => any;
481 | }
482 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable object-property-newline */
2 | import { TDspMeta, TFaustCompileArgs } from "./types";
3 | import mixer32DataURI from "./wasm/mixer32.wasm";
4 |
5 | export const ab2str = (buf: ArrayBuffer): string => (buf ? String.fromCharCode.apply(null, new Uint8Array(buf)) : null);
6 | export const str2ab = (str: string): ArrayBuffer => {
7 | if (!str) return null;
8 | const buf = new ArrayBuffer(str.length);
9 | const bufView = new Uint8Array(buf);
10 | for (let i = 0, strLen = str.length; i < strLen; i++) {
11 | bufView[i] = str.charCodeAt(i);
12 | }
13 | return buf;
14 | };
15 | export const atoUint6 = (nChr: number) => { // eslint-disable-line arrow-body-style
16 | return nChr > 64 && nChr < 91
17 | ? nChr - 65
18 | : nChr > 96 && nChr < 123
19 | ? nChr - 71
20 | : nChr > 47 && nChr < 58
21 | ? nChr + 4
22 | : nChr === 43
23 | ? 62
24 | : nChr === 47
25 | ? 63
26 | : 0;
27 | };
28 | export const atoab = (sBase64: string, nBlocksSize?: number) => {
29 | if (typeof window.atob === "function") return str2ab(atob(sBase64));
30 | const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
31 | const nInLen = sB64Enc.length;
32 | const nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2;
33 | const taBytes = new Uint8Array(nOutLen);
34 | for (let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
35 | nMod4 = nInIdx & 3;
36 | nUint24 |= atoUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
37 | if (nMod4 === 3 || nInLen - nInIdx === 1) {
38 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
39 | taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
40 | }
41 | nUint24 = 0;
42 | }
43 | }
44 | return taBytes.buffer;
45 | };
46 | export const heap2Str = (buf: number[]) => {
47 | let str = "";
48 | let i = 0;
49 | while (buf[i] !== 0) {
50 | str += String.fromCharCode(buf[i++]);
51 | }
52 | return str;
53 | };
54 | export const mixer32Module = new WebAssembly.Module(atoab(mixer32DataURI.split(",")[1]));
55 | export const midiToFreq = (note: number) => 440.0 * 2 ** ((note - 69) / 12);
56 | export const remap = (v: number, mn0: number, mx0: number, mn1: number, mx1: number) => (v - mn0) / (mx0 - mn0) * (mx1 - mn1) + mn1;
57 | export const findPath = (o: any, p: string) => {
58 | if (typeof o !== "object") return false;
59 | if (o.address) {
60 | return (o.address === p);
61 | }
62 | for (const k in o) {
63 | if (findPath(o[k], p)) return true;
64 | }
65 | return false;
66 | };
67 | export const findPathClosure = () => {
68 | const findPath = (o: any, p: string) => {
69 | if (typeof o !== "object") return false;
70 | if (o.address) {
71 | return (o.address === p);
72 | }
73 | for (const k in o) {
74 | if (findPath(o[k], p)) return true;
75 | }
76 | return false;
77 | };
78 | return findPath;
79 | };
80 | export const createWasmImport = (voices: number, memory: WebAssembly.Memory) => ({
81 | env: {
82 | memory: voices ? memory : undefined, memoryBase: 0, tableBase: 0,
83 | _abs: Math.abs,
84 | // Float version
85 | _acosf: Math.acos, _asinf: Math.asin, _atanf: Math.atan, _atan2f: Math.atan2,
86 | _ceilf: Math.ceil, _cosf: Math.cos, _expf: Math.exp, _floorf: Math.floor,
87 | _fmodf: (x: number, y: number) => x % y,
88 | _logf: Math.log, _log10f: Math.log10, _max_f: Math.max, _min_f: Math.min,
89 | _remainderf: (x: number, y: number) => x - Math.round(x / y) * y,
90 | _powf: Math.pow, _roundf: Math.fround, _sinf: Math.sin, _sqrtf: Math.sqrt, _tanf: Math.tan,
91 | _acoshf: Math.acosh, _asinhf: Math.asinh, _atanhf: Math.atanh,
92 | _coshf: Math.cosh, _sinhf: Math.sinh, _tanhf: Math.tanh,
93 | _isnanf: Number.isNaN, _isinff: (x: number) => !isFinite(x),
94 | _copysignf: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x),
95 |
96 | // Double version
97 | _acos: Math.acos, _asin: Math.asin, _atan: Math.atan, _atan2: Math.atan2,
98 | _ceil: Math.ceil, _cos: Math.cos, _exp: Math.exp, _floor: Math.floor,
99 | _fmod: (x: number, y: number) => x % y,
100 | _log: Math.log, _log10: Math.log10, _max_: Math.max, _min_: Math.min,
101 | _remainder: (x: number, y: number) => x - Math.round(x / y) * y,
102 | _pow: Math.pow, _round: Math.fround, _sin: Math.sin, _sqrt: Math.sqrt, _tan: Math.tan,
103 | _acosh: Math.acosh, _asinh: Math.asinh, _atanh: Math.atanh,
104 | _cosh: Math.cosh, _sinh: Math.sinh, _tanh: Math.tanh,
105 | _isnan: Number.isNaN, _isinf: (x: number) => !isFinite(x),
106 | _copysign: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x),
107 |
108 | table: new WebAssembly.Table({ initial: 0, element: "anyfunc" })
109 | }
110 | });
111 | export const createWasmMemory = (voicesIn: number, dspMeta: TDspMeta, effectMeta: TDspMeta, bufferSize: number) => {
112 | // Hack : at least 4 voices (to avoid weird wasm memory bug?)
113 | const voices = Math.max(4, voicesIn);
114 | // Memory allocator
115 | const ptrSize = 4;
116 | const sampleSize = 4;
117 | const pow2limit = (x: number) => {
118 | let n = 65536; // Minimum = 64 kB
119 | while (n < x) { n *= 2; }
120 | return n;
121 | };
122 | const effectSize = effectMeta ? effectMeta.size : 0;
123 | let memorySize = pow2limit(
124 | effectSize
125 | + dspMeta.size * voices
126 | + (dspMeta.inputs + dspMeta.outputs * 2)
127 | * (ptrSize + bufferSize * sampleSize)
128 | ) / 65536;
129 | memorySize = Math.max(2, memorySize); // As least 2
130 | return new WebAssembly.Memory({ initial: memorySize, maximum: memorySize });
131 | };
132 | export const toArgv = (args: TFaustCompileArgs) => {
133 | const argv: string[] = [];
134 | for (const key in args) {
135 | const arg = args[key];
136 | if (Array.isArray(arg)) arg.forEach((s: string) => argv.push(key, s));
137 | else if (typeof arg === "number") argv.push(key, arg.toString());
138 | else argv.push(key, arg);
139 | }
140 | return argv;
141 | };
142 |
--------------------------------------------------------------------------------
/src/wasm.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.wasm" {
2 | const value: string;
3 | export default value;
4 | }
5 | declare module "*.data" {
6 | const value: string;
7 | export default value;
8 | }
9 |
--------------------------------------------------------------------------------
/src/wasm/mixer32.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grame-cncm/faust2webaudio/bf7da8fc18f3b53f28e2401d48969c5118bb517b/src/wasm/mixer32.wasm
--------------------------------------------------------------------------------
/test/mono.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
34 |
35 |
--------------------------------------------------------------------------------
/test/plot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
43 |
44 |
--------------------------------------------------------------------------------
/test/poly-key.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
45 |
46 |
--------------------------------------------------------------------------------
/test/poly.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
43 |
44 |
--------------------------------------------------------------------------------
/test/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
363 |
364 |
--------------------------------------------------------------------------------
/test/wmono.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
34 |
35 |
--------------------------------------------------------------------------------
/test/wpoly.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
43 |
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": true,
4 | "module": "commonjs",
5 | "removeComments": true,
6 | "preserveConstEnums": true,
7 | "target": "es2015",
8 | "sourceMap": true,
9 | "lib": ["es2015", "dom", "dom.iterable"],
10 | "jsx": "react",
11 | "outDir": "dist",
12 | "declaration": true,
13 | "declarationDir": "src",
14 | "noUnusedLocals": false,
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "alwaysStrict": true,
18 | "noUnusedParameters": false,
19 | "pretty": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "allowUnreachableCode": false,
22 | "experimentalDecorators": true,
23 | "downlevelIteration": true
24 | },
25 | "include": [
26 | "src/**/*"
27 | ],
28 | "exclude": [
29 | "dist",
30 | "node_modules"
31 | ]
32 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | /** @type {import('webpack').Configuration} */
4 | const config = {
5 | entry: './src/index.ts',
6 | resolve: {
7 | fallback: {
8 | "path": false,
9 | "fs": false,
10 | "ws": false,
11 | "crypto": false
12 | },
13 | extensions: ['.ts', '.js']
14 | },
15 | output: {
16 | path: path.resolve(__dirname, 'dist'),
17 | library: 'Faust2WebAudio',
18 | libraryTarget: 'umd'
19 | },
20 | node: {
21 | },
22 | module: {
23 | rules: [{
24 | test: /\.(ts|js)x?$/,
25 | use: {
26 | loader: "esbuild-loader",
27 | options: {
28 | loader: 'ts',
29 | target: 'es2016'
30 | }
31 | },
32 | exclude: /(node_modules|libfaust-wasm.js)/,
33 | },
34 | {
35 | test: /\.wasm$/,
36 | loader: 'url-loader',
37 | type: 'javascript/auto',
38 | exclude: /node_modules/,
39 | options: {
40 | mimetype: 'application/wasm'
41 | }
42 | },
43 | {
44 | test: /\.data$/,
45 | loader: 'url-loader',
46 | exclude: /node_modules/,
47 | }
48 | ]
49 | }
50 | };
51 | module.exports = (env, argv) => {
52 | if (argv.mode === 'development') {
53 | config.devtool = 'source-map';
54 | config.output.filename = 'index.js';
55 | }
56 | if (argv.mode === 'production') {
57 | config.devtool = 'source-map';
58 | config.output.filename = 'index.min.js';
59 | }
60 | return config;
61 | };
--------------------------------------------------------------------------------