├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── codestyle.yml │ ├── demo.yml │ ├── link-check.yml │ ├── package.yml │ └── spell-check.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── fixtures │ ├── .gitkeep │ └── audio_samples │ │ ├── 9khz_noise_16kHz_ds_30.pcm │ │ ├── 9khz_noise_16kHz_ds_40.pcm │ │ ├── 9khz_noise_16kHz_ds_50.pcm │ │ ├── 9khz_noise_48kHz.pcm │ │ ├── tone-9khz_noise_16kHz_ds_100.pcm │ │ └── tone-9khz_noise_44.1kHz.pcm ├── support │ ├── commands.ts │ ├── component-index.html │ └── index.ts └── tsconfig.json ├── demo ├── .gitignore ├── README.md ├── index.html ├── package.json └── yarn.lock ├── lib └── pv_resampler.wasm ├── module.d.ts ├── package.json ├── resources └── .lint │ └── spell-check │ ├── .cspell.json │ └── dict.txt ├── rollup.config.js ├── src ├── audio_worklet │ └── recorder_processor.js ├── engines │ ├── audio_dump_engine.ts │ ├── vu_meter_engine.ts │ └── vu_meter_worker.ts ├── index.ts ├── polyfill │ └── audioworklet_polyfill.ts ├── resampler.ts ├── resampler_worker.ts ├── resampler_worker_handler.ts ├── types.ts ├── utils.ts ├── wasi_snapshot.ts └── web_voice_processor.ts ├── test ├── resampler.test.ts └── wvp.test.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .* 3 | bamboo 4 | coverage 5 | dist 6 | example 7 | gulpfile.js 8 | tests 9 | *.worker.js 10 | packages/**/dist/* 11 | **/rollup.config.js 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Rules reference: http://eslint.org/docs/rules/ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | mocha: true 8 | }, 9 | 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 2018 13 | }, 14 | 15 | ignorePatterns: ['**/*.js', 'node_modules', 'dist'], 16 | overrides: [ 17 | { 18 | files: ['src/**/*.ts'], 19 | extends: ['plugin:@typescript-eslint/recommended'], 20 | rules: { 21 | '@typescript-eslint/no-parameter-properties': 2, 22 | '@typescript-eslint/no-explicit-any': 0, 23 | '@typescript-eslint/no-var-requires': 2, 24 | '@typescript-eslint/no-non-null-assertion': 2, 25 | '@typescript-eslint/no-use-before-define': 2, 26 | '@typescript-eslint/camelcase': 0, 27 | '@typescript-eslint/no-empty-interface': 2, 28 | '@typescript-eslint/explicit-function-return-type': 1, 29 | '@typescript-eslint/ban-ts-comment': 0, 30 | '@typescript-eslint/no-empty-function': [2, { "allow": ["constructors"] }], 31 | '@typescript-eslint/no-inferrable-types': [ 32 | 2, 33 | { 34 | ignoreParameters: true, 35 | ignoreProperties: true 36 | } 37 | ], 38 | '@typescript-eslint/no-shadow': ["error"] 39 | } 40 | }, 41 | { 42 | files: ['test/**/*.ts', 'cypress/**/*.ts'], 43 | extends: ['plugin:cypress/recommended'], 44 | rules: { 45 | 'no-unused-expressions': 0, 46 | 'no-unused-vars': 0 47 | } 48 | } 49 | ], 50 | 51 | rules: { 52 | //========================================================================= 53 | //==================== Possible Errors ==================================== 54 | //========================================================================= 55 | 56 | // disallow trailing commas in object literals 57 | 'comma-dangle': [0, 'always-multiline'], 58 | // disallow assignment in conditional expressions 59 | 'no-cond-assign': [2, 'always'], 60 | // disallow use of console 61 | 'no-console': 1, 62 | // disallow use of constant expressions in conditions 63 | 'no-constant-condition': [2, { checkLoops: false }], 64 | // disallow control characters in regular expressions 65 | 'no-control-regex': 2, 66 | // disallow use of debugger 67 | 'no-debugger': 2, 68 | // disallow duplicate arguments in functions 69 | 'no-dupe-args': 2, 70 | // disallow duplicate keys when creating object literals 71 | 'no-dupe-keys': 2, 72 | // disallow a duplicate case label. 73 | 'no-duplicate-case': 2, 74 | // disallow the use of empty character classes in regular expressions 75 | 'no-empty-character-class': 2, 76 | // disallow empty statements 77 | 'no-empty': 2, 78 | // disallow assigning to the exception in a catch block 79 | 'no-ex-assign': 2, 80 | // disallow double-negation boolean casts in a boolean context 81 | 'no-extra-boolean-cast': 2, 82 | // disallow unnecessary parentheses 83 | 'no-extra-parens': [2, 'functions'], 84 | // disallow unnecessary semicolons 85 | 'no-extra-semi': 2, 86 | // disallow overwriting functions written as function declarations 87 | 'no-func-assign': 2, 88 | // disallow function or variable declarations in nested blocks 89 | 'no-inner-declarations': 2, 90 | // disallow invalid regular expression strings in the RegExp constructor 91 | 'no-invalid-regexp': 2, 92 | // disallow irregular whitespace outside of strings and comments 93 | 'no-irregular-whitespace': 2, 94 | // disallow negation of the left operand of an in expression 95 | 'no-negated-in-lhs': 2, 96 | // disallow the use of object properties of the global object (Math and JSON) as functions 97 | 'no-obj-calls': 2, 98 | // disallow multiple spaces in a regular expression literal 99 | 'no-regex-spaces': 2, 100 | // disallow sparse arrays 101 | 'no-sparse-arrays': 2, 102 | // Avoid code that looks like two expressions but is actually one 103 | 'no-unexpected-multiline': 2, 104 | // disallow unreachable statements after a return, throw, continue, or break statement 105 | 'no-unreachable': 2, 106 | // disallow comparisons with the value NaN 107 | 'use-isnan': 2, 108 | // ensure JSDoc comments are valid 109 | 'valid-jsdoc': [ 110 | 0, 111 | { 112 | requireReturn: false, 113 | requireReturnDescription: false 114 | } 115 | ], 116 | // ensure that the results of typeof are compared against a valid string 117 | 'valid-typeof': 2, 118 | 119 | //========================================================================= 120 | //==================== Best Practices ===================================== 121 | //========================================================================= 122 | // Enforces getter/setter pairs in objects 123 | 'accessor-pairs': 2, 124 | // treat var statements as if they were block scoped 125 | 'block-scoped-var': 2, 126 | // specify the maximum cyclomatic complexity allowed in a program 127 | complexity: [0, 11], 128 | // require return statements to either always or never specify values 129 | 'consistent-return': 2, 130 | // specify curly brace conventions for all control statements 131 | curly: [2, 'multi-line'], 132 | // require default case in switch statements 133 | 'default-case': 2, 134 | // encourages use of dot notation whenever possible 135 | 'dot-notation': [2, { allowKeywords: true }], 136 | // enforces consistent newlines before or after dots 137 | 'dot-location': [2, 'property'], 138 | // require the use of === and !== 139 | eqeqeq: 2, 140 | // make sure for-in loops have an if statement 141 | 'guard-for-in': 2, 142 | // disallow the use of alert, confirm, and prompt 143 | 'no-alert': 2, 144 | // disallow use of arguments.caller or arguments.callee 145 | 'no-caller': 2, 146 | // disallow lexical declarations in case clauses 147 | 'no-case-declarations': 2, 148 | // disallow division operators explicitly at beginning of regular expression 149 | 'no-div-regex': 2, 150 | // disallow else after a return in an if 151 | 'no-else-return': 2, 152 | // disallow use of empty destructuring patterns 153 | 'no-empty-pattern': 2, 154 | // disallow comparisons to null without a type-checking operator 155 | 'no-eq-null': 2, 156 | // disallow use of eval() 157 | 'no-eval': 2, 158 | // disallow adding to native types 159 | 'no-extend-native': 2, 160 | // disallow unnecessary function binding 161 | 'no-extra-bind': 2, 162 | // disallow fallthrough of case statements 163 | 'no-fallthrough': 2, 164 | // disallow the use of leading or trailing decimal points in numeric literals 165 | 'no-floating-decimal': 2, 166 | // disallow the type conversions with shorter notations 167 | 'no-implicit-coercion': 2, 168 | // disallow use of eval()-like methods 169 | 'no-implied-eval': 2, 170 | // disallow this keywords outside of classes or class-like objects 171 | 'no-invalid-this': 0, 172 | // disallow usage of __iterator__ property 173 | 'no-iterator': 2, 174 | // disallow use of labeled statements 175 | 'no-labels': 2, 176 | // disallow unnecessary nested blocks 177 | 'no-lone-blocks': 2, 178 | // disallow creation of functions within loops 179 | 'no-loop-func': 2, 180 | // disallow the use of magic numbers 181 | 'no-magic-numbers': 0, //TODO: need discussion 182 | // disallow use of multiple spaces 183 | 'no-multi-spaces': 2, 184 | // disallow use of multiline strings 185 | 'no-multi-str': 2, 186 | // disallow reassignments of native objects 187 | 'no-native-reassign': 2, 188 | // disallow use of new operator for Function object 189 | 'no-new-func': 2, 190 | // disallows creating new instances of String,Number, and Boolean 191 | 'no-new-wrappers': 2, 192 | // disallow use of new operator when not part of the assignment or comparison 193 | 'no-new': 2, 194 | // disallow use of octal escape sequences in string literals, such as 195 | // var foo = "Copyright \251"; 196 | 'no-octal-escape': 2, 197 | // disallow use of (old style) octal literals 198 | 'no-octal': 2, 199 | // disallow reassignment of function parameters 200 | 'no-param-reassign': 1, 201 | // disallow use of process.env 202 | 'no-process-env': 2, 203 | // disallow usage of __proto__ property 204 | 'no-proto': 2, 205 | // disallow declaring the same variable more then once 206 | 'no-redeclare': 2, 207 | // disallow use of assignment in return statement 208 | 'no-return-assign': 2, 209 | // disallow use of `javascript:` urls. 210 | 'no-script-url': 2, 211 | // disallow comparisons where both sides are exactly the same 212 | 'no-self-compare': 2, 213 | // disallow use of comma operator 214 | 'no-sequences': 2, 215 | // restrict what can be thrown as an exception 216 | 'no-throw-literal': 0, 217 | // disallow usage of expressions in statement position 218 | 'no-unused-expressions': 2, 219 | // disallow unnecessary .call() and .apply() 220 | 'no-useless-call': 2, 221 | // disallow unnecessary concatenation of literals or template literals 222 | 'no-useless-concat': 2, 223 | // disallow use of void operator 224 | 'no-void': 2, 225 | // disallow usage of configurable warning terms in comments: e.g. todo 226 | 'no-warning-comments': [ 227 | 1, 228 | { terms: ['todo', 'fixme', 'xxx'], location: 'start' } 229 | ], 230 | // disallow use of the with statement 231 | 'no-with': 2, 232 | // require use of the second argument for parseInt() 233 | radix: 2, 234 | // requires to declare all vars on top of their containing scope 235 | 'vars-on-top': 0, 236 | // require immediate function invocation to be wrapped in parentheses 237 | 'wrap-iife': [2, 'any'], 238 | // require or disallow Yoda conditions 239 | yoda: 2, 240 | 241 | // //========================================================================= 242 | // //==================== Strict Mode ======================================== 243 | // //========================================================================= 244 | // require that all functions are run in strict mode 245 | // "strict": [2, "global"], 246 | // 247 | //========================================================================= 248 | //==================== Variables ========================================== 249 | //========================================================================= 250 | // enforce or disallow variable initializations at definition 251 | 'init-declarations': 0, 252 | // disallow the catch clause parameter name being the same as a variable in the outer scope 253 | 'no-catch-shadow': 0, 254 | // disallow deletion of variables 255 | 'no-delete-var': 2, 256 | // disallow labels that share a name with a variable 257 | 'no-label-var': 2, 258 | // disallow shadowing of names such as arguments 259 | 'no-shadow-restricted-names': 2, 260 | // disallow declaration of variables already declared in the outer scope 261 | 'no-shadow': 0, 262 | // disallow use of undefined when initializing variables 263 | 'no-undef-init': 0, 264 | // disallow use of undeclared variables unless mentioned in a /*global */ block 265 | 'no-undef': 2, 266 | // disallow use of undefined variable 267 | 'no-undefined': 0, 268 | // disallow declaration of variables that are not used in the code 269 | 'no-unused-vars': [1, { vars: 'local', args: 'after-used' }], 270 | // disallow use of variables before they are defined 271 | 'no-use-before-define': 0, 272 | 273 | //========================================================================= 274 | //==================== Node.js ============================================ 275 | //========================================================================= 276 | // enforce return after a callback 277 | 'callback-return': 0, 278 | // disallow require() outside of the top-level module scope 279 | 'global-require': 2, 280 | // enforces error handling in callbacks (node environment) 281 | 'handle-callback-err': 2, 282 | // disallow mixing regular variable and require declarations 283 | 'no-mixed-requires': 2, 284 | // disallow use of new operator with the require function 285 | 'no-new-require': 2, 286 | // disallow string concatenation with __dirname and __filename 287 | 'no-path-concat': 1, 288 | // disallow process.exit() 289 | 'no-process-exit': 2, 290 | // restrict usage of specified node modules 291 | 'no-restricted-modules': 0, 292 | // disallow use of synchronous methods (off by default) 293 | 'no-sync': 0, 294 | 295 | //========================================================================= 296 | //==================== Stylistic Issues =================================== 297 | //========================================================================= 298 | // enforce spacing inside array brackets 299 | 'array-bracket-spacing': 0, 300 | // disallow or enforce spaces inside of single line blocks 301 | 'block-spacing': 1, 302 | // enforce one true brace style 303 | 'brace-style': [1, '1tbs', { allowSingleLine: true }], 304 | // require camel case names 305 | camelcase: [1, { properties: 'always' }], 306 | // enforce spacing before and after comma 307 | 'comma-spacing': [1, { before: false, after: true }], 308 | // enforce one true comma style 309 | 'comma-style': [1, 'last'], 310 | // require or disallow padding inside computed properties 311 | 'computed-property-spacing': 0, 312 | // enforces consistent naming when capturing the current execution context 313 | 'consistent-this': 0, 314 | // enforce newline at the end of file, with no multiple empty lines 315 | 'eol-last': 1, 316 | // require function expressions to have a name 317 | 'func-names': 0, 318 | // enforces use of function declarations or expressions 319 | 'func-style': 0, 320 | // this option enforces minimum and maximum identifier lengths (variable names, property names etc.) 321 | 'id-length': 0, 322 | // require identifiers to match the provided regular expression 323 | 'id-match': 0, 324 | // this option sets a specific tab width for your code 325 | indent: [1, 2, { SwitchCase: 1 }], 326 | // specify whether double or single quotes should be used in JSX attributes 327 | 'jsx-quotes': [1, 'prefer-double'], 328 | // enforces spacing between keys and values in object literal properties 329 | 'key-spacing': [1, { beforeColon: false, afterColon: true }], 330 | // disallow mixed "LF" and "CRLF" as linebreaks 331 | 'linebreak-style': 0, 332 | // enforces empty lines around comments 333 | 'lines-around-comment': 0, 334 | // specify the maximum depth that blocks can be nested 335 | 'max-depth': [0, 4], 336 | // specify the maximum length of a line in your program 337 | 'max-len': [0, 80, 4], 338 | // specify the maximum depth callbacks can be nested 339 | 'max-nested-callbacks': 0, 340 | // limits the number of parameters that can be used in the function declaration. 341 | 'max-params': [0, 3], 342 | // specify the maximum number of statement allowed in a function 343 | 'max-statements': [0, 10], 344 | // require a capital letter for constructors 345 | 'new-cap': [1, { newIsCap: true }], 346 | // disallow the omission of parentheses when invoking a constructor with no arguments 347 | 'new-parens': 0, 348 | // allow/disallow an empty newline after var statement 349 | 'newline-after-var': 0, 350 | // disallow use of the Array constructor 351 | 'no-array-constructor': 0, 352 | // disallow use of bitwise operators 353 | 'no-bitwise': 0, 354 | // disallow use of the continue statement 355 | 'no-continue': 0, 356 | // disallow comments inline after code 357 | 'no-inline-comments': 0, 358 | // disallow if as the only statement in an else block 359 | 'no-lonely-if': 0, 360 | // disallow mixed spaces and tabs for indentation 361 | 'no-mixed-spaces-and-tabs': 1, 362 | // disallow multiple empty lines 363 | 'no-multiple-empty-lines': [1, { max: 2, maxEOF: 1 }], 364 | // disallow negated conditions 365 | 'no-negated-condition': 0, 366 | // disallow nested ternary expressions 367 | 'no-nested-ternary': 1, 368 | // disallow use of the Object constructor 369 | 'no-new-object': 1, 370 | // disallow use of unary operators, ++ and -- 371 | 'no-plusplus': 0, 372 | // disallow use of certain syntax in code 373 | 'no-restricted-syntax': 0, 374 | // disallow space between function identifier and application 375 | 'no-spaced-func': 1, 376 | // disallow the use of ternary operators 377 | 'no-ternary': 0, 378 | // disallow trailing whitespace at the end of lines 379 | 'no-trailing-spaces': 1, 380 | // disallow dangling underscores in identifiers 381 | 'no-underscore-dangle': 0, 382 | // disallow the use of Boolean literals in conditional expressions 383 | 'no-unneeded-ternary': 0, 384 | // require or disallow padding inside curly braces 385 | 'object-curly-spacing': 0, 386 | // allow just one var statement per function 387 | 'one-var': [1, 'never'], 388 | // require assignment operator shorthand where possible or prohibit it entirely 389 | 'operator-assignment': 0, 390 | // enforce operators to be placed before or after line breaks 391 | 'operator-linebreak': 0, 392 | // enforce padding within blocks 393 | 'padded-blocks': [1, 'never'], 394 | // require quotes around object literal property names 395 | 'quote-props': 0, 396 | // specify whether double or single quotes should be used 397 | quotes: 0, 398 | // Require JSDoc comment 399 | 'require-jsdoc': 0, 400 | // enforce spacing before and after semicolons 401 | 'semi-spacing': [1, { before: false, after: true }], 402 | // require or disallow use of semicolons instead of ASI 403 | semi: [1, 'always'], 404 | // sort variables within the same declaration block 405 | 'sort-vars': 0, 406 | // require a space after certain keywords 407 | 'keyword-spacing': 1, 408 | // require or disallow space before blocks 409 | 'space-before-blocks': 1, 410 | // require or disallow space before function opening parenthesis 411 | 'space-before-function-paren': [0, { anonymous: 'always', named: 'never' }], 412 | // require or disallow space before blocks 413 | 'space-in-parens': 0, 414 | // require spaces around operators 415 | 'space-infix-ops': 1, 416 | // Require or disallow spaces before/after unary operators 417 | 'space-unary-ops': 0, 418 | // require or disallow a space immediately following the // or /* in a comment 419 | 'spaced-comment': [ 420 | 1, 421 | 'always', 422 | { 423 | exceptions: ['-', '+', '/', '='], 424 | markers: ['=', '!', '/'] // space here to support sprockets directives 425 | } 426 | ], 427 | // require regex literals to be wrapped in parentheses 428 | 'wrap-regex': 0, 429 | 430 | //========================================================================= 431 | //==================== ES6 Rules ========================================== 432 | //========================================================================= 433 | 'arrow-body-style': [1, 'as-needed'], 434 | // require parens in arrow function arguments 435 | 'arrow-parens': [1, 'as-needed'], 436 | // require space before/after arrow function"s arrow 437 | 'arrow-spacing': 1, 438 | // verify super() callings in constructors 439 | 'constructor-super': 1, 440 | // enforce the spacing around the * in generator functions 441 | 'generator-star-spacing': 1, 442 | // disallow arrow functions where a condition is expected 443 | 'no-confusing-arrow': 1, 444 | // disallow modifying variables of class declarations 445 | 'no-class-assign': 1, 446 | // disallow modifying variables that are declared using const 447 | 'no-const-assign': 1, 448 | // disallow duplicate name in class members 449 | 'no-dupe-class-members': 1, 450 | // disallow to use this/super before super() calling in constructors. 451 | 'no-this-before-super': 1, 452 | // require let or const instead of var 453 | 'no-var': 0, //TODO: enable on full migration to es6 454 | // require method and property shorthand syntax for object literals 455 | 'object-shorthand': 0, 456 | // suggest using arrow functions as callbacks 457 | 'prefer-arrow-callback': 0, //TODO: enable on full migration to es6 458 | // suggest using of const declaration for variables that are never modified after declared 459 | 'prefer-const': 0, //TODO: enable on full migration to es6 460 | // suggest using Reflect methods where applicable 461 | 'prefer-reflect': 0, 462 | // suggest using the spread operator instead of .apply() 463 | 'prefer-spread': 0, 464 | // suggest using template literals instead of strings concatenation 465 | 'prefer-template': 0, //TODO: enable on full migration to es6 466 | // disallow generator functions that do not have yield 467 | 'require-yield': 0 468 | } 469 | }; 470 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yml: -------------------------------------------------------------------------------- 1 | name: Codestyle 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | paths: 8 | - '**/src/*.js' 9 | - '**/src/*.ts' 10 | - '.github/workflows/codestyle.yml' 11 | pull_request: 12 | branches: [ master, 'v[0-9]+.[0-9]+' ] 13 | paths: 14 | - '**/src/*.js' 15 | - '**/src/*.ts' 16 | - '.github/workflows/codestyle.yml' 17 | 18 | jobs: 19 | check-web-codestyle: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Node.js LTS 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: lts/* 29 | 30 | - name: Pre-build dependencies 31 | run: npm install yarn 32 | 33 | - name: Run Binding Linter 34 | run: yarn && yarn lint 35 | -------------------------------------------------------------------------------- /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: Package-build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | paths: 8 | - "demo/**" 9 | - '.github/workflows/demo.yml' 10 | pull_request: 11 | branches: [ master, 'v[0-9]+.[0-9]+' ] 12 | paths: 13 | - "demo/**" 14 | - '.github/workflows/demo.yml' 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [ 14.x, 16.x, 18.x, 20.x ] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v3 29 | 30 | - name: Pre-build dependencies 31 | run: npm install yarn 32 | 33 | - name: Install dependencies 34 | run: yarn install 35 | -------------------------------------------------------------------------------- /.github/workflows/link-check.yml: -------------------------------------------------------------------------------- 1 | name: Check Markdown links 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master, 'v[0-9]+.[0-9]+' ] 9 | 10 | jobs: 11 | markdown-link-check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | - uses: gaurav-nelson/github-action-markdown-link-check@1.0.14 16 | with: 17 | use-quiet-mode: 'yes' 18 | use-verbose-mode: 'yes' 19 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package-build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | paths: 8 | - "audio/**" 9 | - "src/**" 10 | - "lib/**" 11 | - '.github/workflows/package.yml' 12 | pull_request: 13 | branches: [ master, 'v[0-9]+.[0-9]+' ] 14 | paths: 15 | - "audio/**" 16 | - "src/**" 17 | - "lib/**" 18 | - '.github/workflows/package.yml' 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | node-version: [ 14.x, 16.x, 18.x, 20.x ] 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v3 33 | 34 | - name: Pre-build dependencies 35 | run: npm install yarn 36 | 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Generate the package 41 | run: yarn build 42 | 43 | - name: Build 44 | run: yarn && yarn build 45 | 46 | - name: Test 47 | run: yarn test 48 | -------------------------------------------------------------------------------- /.github/workflows/spell-check.yml: -------------------------------------------------------------------------------- 1 | name: SpellCheck 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master, 'v[0-9]+.[0-9]+' ] 9 | 10 | jobs: 11 | markdown: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - name: Install CSpell 22 | run: npm install -g cspell 23 | 24 | - name: Run CSpell 25 | run: cspell --config resources/.lint/spell-check/.cspell.json "**/*" 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | dist 5 | package-lock.json 6 | cypress/downloads 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | demo 5 | test 6 | .github 7 | audio 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Voice Processor 2 | 3 | [![GitHub release](https://img.shields.io/github/release/Picovoice/web-voice-processor.svg)](https://github.com/Picovoice/web-voice-processor/releases) 4 | [![GitHub](https://img.shields.io/github/license/Picovoice/web-voice-processor)](https://github.com/Picovoice/web-voice-processor/releases) 5 | [![npm](https://img.shields.io/npm/v/@picovoice/web-voice-processor?label=npm%20%5Bweb%5D)](https://www.npmjs.com/package/@picovoice/web-voice-processor) 6 | 7 | Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) 8 | 9 | A library for real-time voice processing in web browsers. 10 | 11 | - Uses the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) to access microphone audio. 12 | - Leverages [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker) to offload compute-intensive tasks off of the main thread. 13 | - Converts the microphone sampling rate to 16kHz, the _de facto_ standard for voice processing engines. 14 | - Provides a flexible interface to pass in arbitrary voice processing workers. 15 | 16 | - [Web Voice Processor](#web-voice-processor) 17 | - [Browser compatibility](#browser-compatibility) 18 | - [Browser features](#browser-features) 19 | - [Installation](#installation) 20 | - [How to use](#how-to-use) 21 | - [Via ES Modules (Create React App, Angular, Webpack, etc.)](#via-es-modules-create-react-app-angular-webpack-etc) 22 | - [Via HTML script tag](#via-html-script-tag) 23 | - [Start listening](#start-listening) 24 | - [Stop listening](#stop-listening) 25 | - [Build from source](#build-from-source) 26 | 27 | ## Browser compatibility 28 | 29 | All modern browsers (Chrome/Edge/Opera, Firefox, Safari) are supported, including on mobile. Internet Explorer is _not_ supported. 30 | 31 | Using the Web Audio API requires a secure context (HTTPS connection), with the exception of `localhost`, for local development. 32 | 33 | This library includes the utility function `browserCompatibilityCheck` which can be used to perform feature detection on the current browser and return an object 34 | indicating browser capabilities. 35 | 36 | ESM: 37 | 38 | ```javascript 39 | import { browserCompatibilityCheck } from '@picovoice/web-voice-processor'; 40 | browserCompatibilityCheck(); 41 | ``` 42 | 43 | IIFE: 44 | 45 | ```javascript 46 | window.WebVoiceProcessor.browserCompatibilityCheck(); 47 | ``` 48 | 49 | ### Browser features 50 | 51 | - '\_picovoice' : whether all Picovoice requirements are met 52 | - 'AudioWorklet' (not currently used; intended for the future) 53 | - 'isSecureContext' (required for microphone permission for non-localhost) 54 | - 'mediaDevices' (basis for microphone enumeration / access) 55 | - 'WebAssembly' (required for all Picovoice engines) 56 | - 'webKitGetUserMedia' (legacy predecessor to getUserMedia) 57 | - 'Worker' (required for resampler and for all engine processing) 58 | 59 | ## Installation 60 | 61 | ```console 62 | npm install @picovoice/web-voice-processor 63 | ``` 64 | 65 | (or) 66 | 67 | ```console 68 | yarn add @picovoice/web-voice-processor 69 | ``` 70 | 71 | ## How to use 72 | 73 | ### Via ES Modules (Create React App, Angular, Webpack, etc.) 74 | 75 | ```javascript 76 | import { WebVoiceProcessor } from '@picovoice/web-voice-processor'; 77 | ``` 78 | 79 | ### Via HTML script tag 80 | 81 | Add the following to your HTML: 82 | 83 | ```html 84 | 85 | ``` 86 | 87 | The IIFE version of the library adds `WebVoiceProcessor` to the `window` global scope. 88 | 89 | ### Start listening 90 | 91 | WebVoiceProcessor follows the subscribe/unsubscribe pattern. WebVoiceProcessor 92 | will automatically start recording audio as soon as an engine is subscribed. 93 | 94 | ```javascript 95 | const worker = new Worker('${WORKER_PATH}'); 96 | const engine = { 97 | onmessage: function(e) { 98 | /// ... handle inputFrame 99 | } 100 | } 101 | 102 | await WebVoiceProcessor.subscribe(engine); 103 | await WebVoiceProcessor.subscribe(worker); 104 | // or 105 | await WebVoiceProcessor.subscribe([engine, worker]); 106 | ``` 107 | 108 | An `engine` is either a [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker) or an object 109 | implementing the following interface within their `onmessage` method: 110 | 111 | ```javascript 112 | onmessage = function (e) { 113 | switch (e.data.command) { 114 | case 'process': 115 | process(e.data.inputFrame); 116 | break; 117 | } 118 | }; 119 | ``` 120 | 121 | where `e.data.inputFrame` is an `Int16Array` of `frameLength` audio samples. 122 | 123 | For examples of using engines, look at [src/engines](src/engines). 124 | 125 | This is async due to its [Web Audio API microphone request](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia). The promise will be rejected if the user refuses permission, no suitable devices are found, etc. Your calling code should anticipate the possibility of rejection. When the promise resolves, the WebVoiceProcessor is running. 126 | 127 | ### Stop Listening 128 | 129 | Unsubscribing the engines initially subscribed will stop audio recorder. 130 | 131 | ```javascript 132 | await WebVoiceProcessor.unsubscribe(engine); 133 | await WebVoiceProcessor.unsubscribe(worker); 134 | //or 135 | await WebVoiceProcessor.unsubscribe([engine, worker]); 136 | ``` 137 | 138 | ### Reset 139 | 140 | Use the `reset` function to remove all engines and stop recording audio. 141 | 142 | ```javascript 143 | await WebVoiceProcessor.reset(); 144 | ``` 145 | 146 | ### Options 147 | 148 | To update the audio settings in `WebVoiceProcessor`, use the `setOptions` function: 149 | 150 | ```javascript 151 | // Override default options 152 | let options = { 153 | frameLength: 512, 154 | outputSampleRate: 16000, 155 | deviceId: null, 156 | filterOrder: 50, 157 | }; 158 | 159 | WebVoiceProcessor.setOptions(options); 160 | ``` 161 | 162 | ### Custom Recorder Processor 163 | 164 | **NOTE**: Issues related to custom recorder processor implementations are out of the scope of this repo. 165 | 166 | Take a look at [recorder_processor.js](src/audio_worklet/recorder_processor.js) in this repo as a reference 167 | on how to create a simple recorder processor. To learn more about creating a recorder processor, 168 | check out [AudioWorkletProcessor](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor) docs. 169 | 170 | Add the option `customRecorderProcessorURL` to options object to use your own recorder processor. 171 | Enter the string to the custom recorder processor URL or leave it blank to use the default recorder processor. 172 | 173 | ```javascript 174 | // Override default options 175 | let options = { 176 | frameLength: 512, 177 | outputSampleRate: 16000, 178 | deviceId: null, 179 | filterOrder: 50, 180 | customRecorderProcessorURL: "${URL_PATH_TO_RECORDER_PROCESSOR}" 181 | }; 182 | 183 | WebVoiceProcessor.setOptions(options); 184 | ``` 185 | 186 | ### VuMeter 187 | 188 | `WebVoiceProcessor` includes a built-in engine which returns the [VU meter](https://en.wikipedia.org/wiki/VU_meter). 189 | To capture the VU meter value, create a VuMeterEngine instance and subscribe it to the engine: 190 | 191 | ```javascript 192 | function vuMeterCallback(dB) { 193 | console.log(dB) 194 | } 195 | 196 | const vuMeterEngine = new VuMeterEngine(vuMeterCallback); 197 | WebVoiceProcessor.subscribe(vuMeterEngine); 198 | ``` 199 | 200 | The `vuMeterCallback` should expected a number in terms of [dBFS](https://en.wikipedia.org/wiki/DBFS) within the range of [-96, 0]. 201 | 202 | ## Build from source 203 | 204 | Use `yarn` or `npm` to build WebVoiceProcessor: 205 | 206 | ```console 207 | yarn 208 | yarn build 209 | ``` 210 | 211 | (or) 212 | 213 | ```console 214 | npm install 215 | npm run-script build 216 | ``` 217 | 218 | The build script outputs minified and non-minified versions of the IIFE and ESM formats to the `dist` folder. It also will output the TypeScript type definitions. 219 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | env: { 5 | "DEBUG": false, 6 | }, 7 | e2e: { 8 | defaultCommandTimeout: 30000, 9 | supportFile: "cypress/support/index.ts", 10 | specPattern: "test/*.test.{js,jsx,ts,tsx}", 11 | video: false, 12 | screenshotOnRunFailure: false, 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /cypress/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/.gitkeep -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_30.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_30.pcm -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_40.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_40.pcm -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_50.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/9khz_noise_16kHz_ds_50.pcm -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/9khz_noise_48kHz.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/9khz_noise_48kHz.pcm -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/tone-9khz_noise_16kHz_ds_100.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/tone-9khz_noise_16kHz_ds_100.pcm -------------------------------------------------------------------------------- /cypress/fixtures/audio_samples/tone-9khz_noise_44.1kHz.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/cypress/fixtures/audio_samples/tone-9khz_noise_44.1kHz.pcm -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | 2 | Cypress.Commands.add("getFramesFromFile", (path: string) => { 3 | cy.fixture(path, 'base64').then(Cypress.Blob.base64StringToBlob).then(async blob => { 4 | return new Int16Array(await blob.arrayBuffer()); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | import "./commands"; 2 | 3 | declare global { 4 | namespace Cypress { 5 | interface Chainable { 6 | getFramesFromFile(path: string): Chainable; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["cypress"] 5 | }, 6 | "include": [ 7 | "../test/**/*.ts", 8 | "./**/*.ts" 9 | ], 10 | "exclude": [] 11 | } 12 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # WebVoiceProcessor - Demo 2 | 3 | This is a basic demo to show how to use WebVoiceProcessor. It passes in a worker that returns the volume level of the downsampled signal. It also allows you to dump raw PCM data that has passed through the resampler. 4 | 5 | ## Install / run 6 | 7 | Use `yarn` or `npm` to install the dependencies, and the `start` script to start a local web server hosting the demo. 8 | 9 | ```bash 10 | yarn 11 | yarn start 12 | ``` 13 | 14 | Open `localhost:5000` in your web browser, as hinted at in the output: 15 | 16 | ```console 17 | Available on: 18 | http://localhost:5000 19 | Hit CTRL-C to stop the server 20 | ``` 21 | 22 | You will see the VU meter responding to microphone volume in real time. 23 | 24 | ### Audio Dump 25 | 26 | Press the "Start Audio Dump" button to activate web voice processor's audio dump feature. When it's ready, you can click "Download raw PCM" to download the data. You can use a tool like Audacity to open this file (signed 16-bit, 16000Hz). 27 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 91 | 92 | 93 |

WebVoiceProcessor demo: RMS audio VU meter

94 |
95 |
 
96 |
97 | 98 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-voice-processor-demo", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "yarn run http-server -a localhost -p 5000" 8 | }, 9 | "keywords": [ 10 | "pcm", 11 | "downsampling", 12 | "microphone", 13 | "web audio api", 14 | "speech recognition", 15 | "voice ai" 16 | ], 17 | "author": "Picovoice Inc", 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "@picovoice/web-voice-processor": "^4.0.8", 21 | "http-server": "^14.0.0", 22 | "wavefile": "^11.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@picovoice/web-utils@=1.3.1": 6 | version "1.3.1" 7 | resolved "https://registry.yarnpkg.com/@picovoice/web-utils/-/web-utils-1.3.1.tgz#d417e98604a650b54a8e03669015ecf98c2383ec" 8 | integrity sha512-jcDqdULtTm+yJrnHDjg64hARup+Z4wNkYuXHNx6EM8+qZkweBq9UA6XJrHAlUkPnlkso4JWjaIKhz3x8vZcd3g== 9 | dependencies: 10 | commander "^9.2.0" 11 | 12 | "@picovoice/web-voice-processor@^4.0.8": 13 | version "4.0.8" 14 | resolved "https://registry.yarnpkg.com/@picovoice/web-voice-processor/-/web-voice-processor-4.0.8.tgz#95247a5393cac4d16490a53feb0f413c902ee5fa" 15 | integrity sha512-/OSHn8YKniMo0jP5EwGimLOxvLQl/Yx4Hs+LydNmoSu4hfBrDdzhhfhB79118uDiK4aUUKx2A/RAD9TG0mQ/ng== 16 | dependencies: 17 | "@picovoice/web-utils" "=1.3.1" 18 | 19 | ansi-styles@^4.1.0: 20 | version "4.3.0" 21 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 22 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 23 | dependencies: 24 | color-convert "^2.0.1" 25 | 26 | async@^2.6.4: 27 | version "2.6.4" 28 | resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" 29 | integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== 30 | dependencies: 31 | lodash "^4.17.14" 32 | 33 | basic-auth@^2.0.1: 34 | version "2.0.1" 35 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 36 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 37 | dependencies: 38 | safe-buffer "5.1.2" 39 | 40 | call-bind@^1.0.0: 41 | version "1.0.2" 42 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 43 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 44 | dependencies: 45 | function-bind "^1.1.1" 46 | get-intrinsic "^1.0.2" 47 | 48 | chalk@^4.1.2: 49 | version "4.1.2" 50 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 51 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 52 | dependencies: 53 | ansi-styles "^4.1.0" 54 | supports-color "^7.1.0" 55 | 56 | color-convert@^2.0.1: 57 | version "2.0.1" 58 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 59 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 60 | dependencies: 61 | color-name "~1.1.4" 62 | 63 | color-name@~1.1.4: 64 | version "1.1.4" 65 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 66 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 67 | 68 | commander@^9.2.0: 69 | version "9.5.0" 70 | resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" 71 | integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== 72 | 73 | corser@^2.0.1: 74 | version "2.0.1" 75 | resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" 76 | integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ== 77 | 78 | debug@^3.2.7: 79 | version "3.2.7" 80 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" 81 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 82 | dependencies: 83 | ms "^2.1.1" 84 | 85 | eventemitter3@^4.0.0: 86 | version "4.0.7" 87 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" 88 | integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== 89 | 90 | follow-redirects@^1.0.0: 91 | version "1.15.6" 92 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" 93 | integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== 94 | 95 | function-bind@^1.1.1: 96 | version "1.1.1" 97 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 98 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 99 | 100 | get-intrinsic@^1.0.2: 101 | version "1.1.3" 102 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" 103 | integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== 104 | dependencies: 105 | function-bind "^1.1.1" 106 | has "^1.0.3" 107 | has-symbols "^1.0.3" 108 | 109 | has-flag@^4.0.0: 110 | version "4.0.0" 111 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 112 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 113 | 114 | has-symbols@^1.0.3: 115 | version "1.0.3" 116 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 117 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 118 | 119 | has@^1.0.3: 120 | version "1.0.3" 121 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 122 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 123 | dependencies: 124 | function-bind "^1.1.1" 125 | 126 | he@^1.2.0: 127 | version "1.2.0" 128 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 129 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 130 | 131 | html-encoding-sniffer@^3.0.0: 132 | version "3.0.0" 133 | resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" 134 | integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== 135 | dependencies: 136 | whatwg-encoding "^2.0.0" 137 | 138 | http-proxy@^1.18.1: 139 | version "1.18.1" 140 | resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" 141 | integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== 142 | dependencies: 143 | eventemitter3 "^4.0.0" 144 | follow-redirects "^1.0.0" 145 | requires-port "^1.0.0" 146 | 147 | http-server@^14.0.0: 148 | version "14.1.1" 149 | resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e" 150 | integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A== 151 | dependencies: 152 | basic-auth "^2.0.1" 153 | chalk "^4.1.2" 154 | corser "^2.0.1" 155 | he "^1.2.0" 156 | html-encoding-sniffer "^3.0.0" 157 | http-proxy "^1.18.1" 158 | mime "^1.6.0" 159 | minimist "^1.2.6" 160 | opener "^1.5.1" 161 | portfinder "^1.0.28" 162 | secure-compare "3.0.1" 163 | union "~0.5.0" 164 | url-join "^4.0.1" 165 | 166 | iconv-lite@0.6.3: 167 | version "0.6.3" 168 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" 169 | integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== 170 | dependencies: 171 | safer-buffer ">= 2.1.2 < 3.0.0" 172 | 173 | lodash@^4.17.14: 174 | version "4.17.21" 175 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 176 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 177 | 178 | mime@^1.6.0: 179 | version "1.6.0" 180 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 181 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 182 | 183 | minimist@^1.2.6: 184 | version "1.2.7" 185 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" 186 | integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== 187 | 188 | mkdirp@^0.5.6: 189 | version "0.5.6" 190 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" 191 | integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== 192 | dependencies: 193 | minimist "^1.2.6" 194 | 195 | ms@^2.1.1: 196 | version "2.1.3" 197 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 198 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 199 | 200 | object-inspect@^1.9.0: 201 | version "1.12.3" 202 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" 203 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== 204 | 205 | opener@^1.5.1: 206 | version "1.5.2" 207 | resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" 208 | integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== 209 | 210 | portfinder@^1.0.28: 211 | version "1.0.32" 212 | resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" 213 | integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== 214 | dependencies: 215 | async "^2.6.4" 216 | debug "^3.2.7" 217 | mkdirp "^0.5.6" 218 | 219 | qs@^6.4.0: 220 | version "6.11.0" 221 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 222 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 223 | dependencies: 224 | side-channel "^1.0.4" 225 | 226 | requires-port@^1.0.0: 227 | version "1.0.0" 228 | resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 229 | integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== 230 | 231 | safe-buffer@5.1.2: 232 | version "5.1.2" 233 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 234 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 235 | 236 | "safer-buffer@>= 2.1.2 < 3.0.0": 237 | version "2.1.2" 238 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 239 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 240 | 241 | secure-compare@3.0.1: 242 | version "3.0.1" 243 | resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" 244 | integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw== 245 | 246 | side-channel@^1.0.4: 247 | version "1.0.4" 248 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 249 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 250 | dependencies: 251 | call-bind "^1.0.0" 252 | get-intrinsic "^1.0.2" 253 | object-inspect "^1.9.0" 254 | 255 | supports-color@^7.1.0: 256 | version "7.2.0" 257 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 258 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 259 | dependencies: 260 | has-flag "^4.0.0" 261 | 262 | union@~0.5.0: 263 | version "0.5.0" 264 | resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" 265 | integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== 266 | dependencies: 267 | qs "^6.4.0" 268 | 269 | url-join@^4.0.1: 270 | version "4.0.1" 271 | resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" 272 | integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== 273 | 274 | wavefile@^11.0.0: 275 | version "11.0.0" 276 | resolved "https://registry.yarnpkg.com/wavefile/-/wavefile-11.0.0.tgz#9302165874327ff63a704d00b154c753eaa1b8e7" 277 | integrity sha512-/OBiAALgWU24IG7sC84cDO/KfFuvajWc5Uec0oV2zrpOOZZDgGdOwHwgEzOrwh8jkubBk7PtZfQBIcI1OaE5Ng== 278 | 279 | whatwg-encoding@^2.0.0: 280 | version "2.0.0" 281 | resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" 282 | integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== 283 | dependencies: 284 | iconv-lite "0.6.3" 285 | -------------------------------------------------------------------------------- /lib/pv_resampler.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Picovoice/web-voice-processor/56c4207286a4a0c9aaf4b4199280e23d3cf505b4/lib/pv_resampler.wasm -------------------------------------------------------------------------------- /module.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.wasm" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.js" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module 'web-worker:*' { 12 | const WorkerFactory: new () => Worker; 13 | export default WorkerFactory; 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@picovoice/web-voice-processor", 3 | "version": "4.0.9", 4 | "description": "Real-time audio processing for voice, in web browsers", 5 | "entry": "src/index.ts", 6 | "module": "dist/esm/index.js", 7 | "iife": "dist/iife/index.js", 8 | "types": "dist/types/index.d.ts", 9 | "scripts": { 10 | "build:all": "rollup --config", 11 | "build:types": "tsc --declaration --declarationMap --emitDeclarationOnly --outDir ./dist/types", 12 | "build": "npm-run-all --parallel build:**", 13 | "lint": "eslint . --ext .js,.ts,.jsx,.tsx", 14 | "prepack": "npm-run-all build", 15 | "start": "cross-env TARGET='debug' rollup --config --watch", 16 | "watch": "rollup --config --watch", 17 | "format": "prettier --write \"**/*.{js,ts,json}\"", 18 | "test": "cypress run" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Picovoice/web-voice-processor.git" 23 | }, 24 | "keywords": [ 25 | "realtime", 26 | "real-time", 27 | "voice processing", 28 | "audio processing", 29 | "speech recognition", 30 | "browser", 31 | "web browser", 32 | "private", 33 | "web audio api", 34 | "microphone", 35 | "downsampling" 36 | ], 37 | "author": { 38 | "name": "Picovoice Inc.", 39 | "email": "hello@picovoice.ai", 40 | "url": "https://picovoice.ai" 41 | }, 42 | "license": "Apache-2.0", 43 | "bugs": { 44 | "url": "https://github.com/Picovoice/web-voice-processor/issues" 45 | }, 46 | "homepage": "https://github.com/Picovoice/web-voice-processor#readme", 47 | "dependencies": { 48 | "@picovoice/web-utils": "=1.3.1" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.18.10", 52 | "@babel/plugin-transform-runtime": "^7.18.2", 53 | "@babel/preset-env": "^7.18.2", 54 | "@babel/runtime": "^7.18.3", 55 | "@rollup/plugin-babel": "^6.0.3", 56 | "@rollup/plugin-commonjs": "^24.0.1", 57 | "@rollup/plugin-node-resolve": "^15.0.1", 58 | "@rollup/plugin-terser": "^0.4.0", 59 | "@rollup/pluginutils": "^5.0.2", 60 | "@typescript-eslint/eslint-plugin": "^5.51.0", 61 | "@typescript-eslint/parser": "^5.51.0", 62 | "async-mutex": "^0.4.0", 63 | "cross-env": "^7.0.3", 64 | "cypress": "~12.8.1", 65 | "eslint": "^8.13.0", 66 | "eslint-plugin-cypress": "^2.12.1", 67 | "npm-run-all": "^4.1.5", 68 | "prettier": "^2.8.3", 69 | "rollup": "^2.79.1", 70 | "rollup-plugin-typescript2": "^0.34.1", 71 | "rollup-plugin-web-worker-loader": "^1.6.1", 72 | "tslib": "^2.5.0", 73 | "typescript": "^4.9.5" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/.lint/spell-check/.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "dictionaries": [ 4 | "dict" 5 | ], 6 | "dictionaryDefinitions": [ 7 | { 8 | "name": "dict", 9 | "path": "./dict.txt", 10 | "addWords": true 11 | } 12 | ], 13 | "ignorePaths": [ 14 | "**/package.json", 15 | "**/packages-lock.json", 16 | "**/tsconfig.json", 17 | "**/tslint.json", 18 | "**/node_modules/*", 19 | "**/dist/*", 20 | "**/lib/*", 21 | "**/resources/audio/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /resources/.lint/spell-check/dict.txt: -------------------------------------------------------------------------------- 1 | camelcase 2 | downsampled 3 | fdstat 4 | filestat 5 | iife 6 | oneoff 7 | picovoice 8 | pread 9 | prestat 10 | pwrite 11 | resampler 12 | sched 13 | wavefile 14 | worklet 15 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 4 | const commonjs = require('@rollup/plugin-commonjs'); 5 | const typescript = require('rollup-plugin-typescript2'); 6 | const workerLoader = require('rollup-plugin-web-worker-loader'); 7 | const pkg = require('./package.json'); 8 | const { babel } = require('@rollup/plugin-babel'); 9 | const terser = require('@rollup/plugin-terser'); 10 | const { DEFAULT_EXTENSIONS } = require('@babel/core'); 11 | const { base64 } = require('@picovoice/web-utils/plugins'); 12 | 13 | const extensions = [...DEFAULT_EXTENSIONS, '.ts']; 14 | 15 | console.log(process.env.TARGET); 16 | console.log(extensions); 17 | 18 | function capitalizeFirstLetter(string) { 19 | return string.charAt(0).toUpperCase() + string.slice(1); 20 | } 21 | 22 | const iifeBundleName = pkg.name 23 | .split('@picovoice/')[1] 24 | .split('-') 25 | .map(word => capitalizeFirstLetter(word)) 26 | .join(''); 27 | console.log(iifeBundleName); 28 | 29 | export default { 30 | input: [path.resolve(__dirname, pkg.entry)], 31 | output: [ 32 | { 33 | file: path.resolve(__dirname, pkg.module), 34 | format: 'esm', 35 | sourcemap: false, 36 | }, 37 | { 38 | file: path.resolve(__dirname, 'dist', 'esm', 'index.min.js'), 39 | format: 'esm', 40 | sourcemap: false, 41 | plugins: [terser()], 42 | }, 43 | { 44 | file: path.resolve(__dirname, pkg.iife), 45 | format: 'iife', 46 | name: iifeBundleName, 47 | sourcemap: false, 48 | }, 49 | { 50 | file: path.resolve(__dirname, 'dist', 'iife', 'index.min.js'), 51 | format: 'iife', 52 | name: iifeBundleName, 53 | sourcemap: false, 54 | plugins: [terser()], 55 | }, 56 | ], 57 | plugins: [ 58 | nodeResolve({ extensions }), 59 | commonjs(), 60 | workerLoader({ targetPlatform: 'browser', sourcemap: false }), 61 | typescript({ 62 | typescript: require('typescript'), 63 | cacheRoot: path.resolve(__dirname, '.rts2_cache'), 64 | clean: true, 65 | }), 66 | babel({ 67 | extensions: extensions, 68 | babelHelpers: 'runtime', 69 | exclude: '**/node_modules/**', 70 | }), 71 | base64({ 72 | include: ['lib/**/*.wasm', './src/**/*.js'] 73 | }) 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /src/audio_worklet/recorder_processor.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | class RecorderProcessor extends AudioWorkletProcessor { 13 | constructor(options) { 14 | super(); 15 | 16 | const { numberOfChannels = 1 } = options?.processorOptions; 17 | 18 | this._numberOfChannels = numberOfChannels; 19 | } 20 | 21 | process(inputs, outputs, parameters) { 22 | let input = inputs[0]; // get first input 23 | if (input.length === 0) { 24 | return true; 25 | } 26 | 27 | this.port.postMessage({ 28 | buffer: input.slice(0, this._numberOfChannels) 29 | }); 30 | return true; 31 | } 32 | } 33 | 34 | registerProcessor('recorder-processor', RecorderProcessor); 35 | -------------------------------------------------------------------------------- /src/engines/audio_dump_engine.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | import { WvpMessageEvent } from '../types'; 13 | 14 | export class AudioDumpEngine { 15 | private _buffers: Array = []; 16 | 17 | onmessage(e: MessageEvent): void { 18 | switch (e.data.command) { 19 | case 'process': 20 | this._buffers.push(e.data.inputFrame); 21 | break; 22 | default: 23 | } 24 | } 25 | 26 | onend(): Blob { 27 | return new Blob(this._buffers); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/engines/vu_meter_engine.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | import VmWorker from 'web-worker:./vu_meter_worker.ts'; 13 | 14 | export class VuMeterEngine { 15 | private readonly _vuMeterCallback: (dB: number) => void; 16 | private readonly _worker: Worker; 17 | 18 | constructor(vuMeterCallback: (db: number) => void) { 19 | this._vuMeterCallback = vuMeterCallback; 20 | this._worker = new VmWorker(); 21 | this._worker.onmessage = (e: MessageEvent): void => { 22 | this._vuMeterCallback(e.data); 23 | }; 24 | } 25 | 26 | get worker(): Worker { 27 | return this._worker; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/engines/vu_meter_worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | import { WvpMessageEvent } from '../types'; 13 | 14 | const INT_16_MAX = 32767; 15 | const EPSILON = 1e-9; 16 | 17 | const process = (frames: Int16Array): number => { 18 | const sum = [...frames].reduce( 19 | (accumulator, frame) => accumulator + frame ** 2, 20 | 0, 21 | ); 22 | const rms = (sum / frames.length) / INT_16_MAX / INT_16_MAX; 23 | return 10 * Math.log10(Math.max(rms, EPSILON)); 24 | }; 25 | 26 | onmessage = (e: MessageEvent): void => { 27 | switch (e.data.command) { 28 | case 'process': 29 | postMessage(process(e.data.inputFrame)); 30 | break; 31 | default: 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './polyfill/audioworklet_polyfill'; 2 | 3 | import { WvpMessageEvent, WebVoiceProcessorOptions } from './types'; 4 | 5 | import { WebVoiceProcessor, WvpError } from './web_voice_processor'; 6 | import { browserCompatibilityCheck } from './utils'; 7 | 8 | import { VuMeterEngine } from './engines/vu_meter_engine'; 9 | 10 | import resamplerWasm from '../lib/pv_resampler.wasm'; 11 | 12 | import Resampler from './resampler'; 13 | import ResamplerWorker from './resampler_worker'; 14 | 15 | Resampler.setWasm(resamplerWasm); 16 | ResamplerWorker.setWasm(resamplerWasm); 17 | 18 | export { 19 | Resampler, 20 | ResamplerWorker, 21 | VuMeterEngine, 22 | WvpError, 23 | WebVoiceProcessor, 24 | WebVoiceProcessorOptions, 25 | WvpMessageEvent, 26 | browserCompatibilityCheck, 27 | }; 28 | -------------------------------------------------------------------------------- /src/polyfill/audioworklet_polyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | type ProcessorPolyfill = { 13 | port?: { 14 | onmessage?: (event: MessageEvent<{ buffer: Float32Array[] }>) => void; 15 | } 16 | }; 17 | 18 | if (typeof window !== "undefined") { 19 | // @ts-ignore window.webkitAudioContext 20 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 21 | 22 | if (typeof AudioWorkletNode !== 'function' || !('audioWorklet' in AudioContext.prototype)) { 23 | if (AudioContext) { 24 | if (!('audioWorklet' in AudioContext.prototype)) { 25 | // @ts-ignore 26 | AudioContext.prototype.audioWorklet = { 27 | // eslint-disable-next-line 28 | addModule: async function (moduleURL: string | URL, options?: WorkletOptions): Promise { 29 | return; 30 | }, 31 | }; 32 | } 33 | 34 | // @ts-ignore 35 | // eslint-disable-next-line no-native-reassign 36 | window.AudioWorkletNode = window.AudioWorkletNode || function (context: AudioContext, processorName: string, options: any): ScriptProcessorNode { 37 | const {numberOfChannels = 1, frameLength = 512} = options && options.processorOptions; 38 | const scriptProcessor: ScriptProcessorNode & ProcessorPolyfill = context.createScriptProcessor(frameLength, numberOfChannels, numberOfChannels); 39 | 40 | if (!scriptProcessor.port) { 41 | scriptProcessor.port = {}; 42 | } 43 | 44 | scriptProcessor.onaudioprocess = (event: AudioProcessingEvent): void => { 45 | if (scriptProcessor.port && scriptProcessor.port.onmessage) { 46 | const buffer = []; 47 | for (let i = 0; i < event.inputBuffer.numberOfChannels; i++) { 48 | buffer.push(event.inputBuffer.getChannelData(i)); 49 | } 50 | scriptProcessor.port.onmessage({data: {buffer}} as MessageEvent); 51 | } 52 | }; 53 | 54 | // @ts-ignore 55 | // eslint-disable-next-line arrow-body-style 56 | scriptProcessor.port.close = (): void => { 57 | return; 58 | }; 59 | 60 | return scriptProcessor; 61 | }; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/resampler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | /* eslint camelcase: 0 */ 13 | 14 | import { arrayBufferToStringAtIndex, base64ToUint8Array } from '@picovoice/web-utils'; 15 | import { wasiSnapshotPreview1Emulator } from './wasi_snapshot'; 16 | 17 | const PV_STATUS_SUCCESS = 10000; 18 | 19 | type pv_resampler_convert_num_samples_to_input_sample_rate_type = (objectAddress: number, frameLength: number) => number; 20 | type pv_resampler_convert_num_samples_to_output_sample_rate_type = (objectAddress: number, frameLength: number) => number; 21 | type pv_resampler_init_type = (inputFrequency: number, outputFrequency: number, order: number, objectAddressAddress: number) => number; 22 | type pv_resampler_process_type = (objectAddress: number, inputBufferAddress: number, inputBufferSize: number, outputBufferAddress: number) => number; 23 | type pv_resampler_reset_type = (objectAddress: number) => void; 24 | type pv_resampler_delete_type = (objectAddress: number) => number; 25 | type pv_resampler_version_type = () => number; 26 | type aligned_alloc_type = (alignment: number, size: number) => number; 27 | 28 | type ResamplerWasmOutput = { 29 | cAlignedAlloc: aligned_alloc_type; 30 | frameLength: number; 31 | inputBufferAddress: number; 32 | inputFrameLength: number; 33 | memory: WebAssembly.Memory; 34 | objectAddress: number; 35 | outputBufferAddress: number; 36 | pvResamplerConvertNumSamplesToInputSampleRate: pv_resampler_convert_num_samples_to_input_sample_rate_type; 37 | pvResamplerConvertNumSamplesToOutputSampleRate: pv_resampler_convert_num_samples_to_output_sample_rate_type; 38 | pvResamplerDelete: pv_resampler_delete_type; 39 | pvResamplerInit: pv_resampler_init_type; 40 | pvResamplerProcess: pv_resampler_process_type; 41 | pvResamplerReset: pv_resampler_reset_type; 42 | version: string; 43 | }; 44 | 45 | class Resampler { 46 | private readonly _pvResamplerConvertNumSamplesToInputSampleRate: pv_resampler_convert_num_samples_to_input_sample_rate_type; 47 | private readonly _pvResamplerConvertNumSamplesToOutputSampleRate: pv_resampler_convert_num_samples_to_output_sample_rate_type; 48 | private readonly _pvResamplerDelete: pv_resampler_delete_type; 49 | private readonly _pvResamplerProcess: pv_resampler_process_type; 50 | private readonly _pvResamplerReset: pv_resampler_reset_type; 51 | 52 | private readonly _cAlignedAlloc: aligned_alloc_type; 53 | 54 | private readonly _inputBufferAddress: number; 55 | private readonly _objectAddress: number; 56 | private readonly _outputBufferAddress: number; 57 | 58 | private _wasmMemory: WebAssembly.Memory; 59 | 60 | private readonly _frameLength: number; 61 | private readonly _inputBufferLength: number; 62 | 63 | private static _wasm: string; 64 | public static _version: string; 65 | 66 | private constructor(handleWasm: ResamplerWasmOutput) { 67 | Resampler._version = handleWasm.version; 68 | 69 | this._pvResamplerConvertNumSamplesToInputSampleRate = 70 | handleWasm.pvResamplerConvertNumSamplesToInputSampleRate; 71 | this._pvResamplerConvertNumSamplesToOutputSampleRate = 72 | handleWasm.pvResamplerConvertNumSamplesToOutputSampleRate; 73 | this._pvResamplerReset = handleWasm.pvResamplerReset; 74 | this._pvResamplerProcess = handleWasm.pvResamplerProcess; 75 | this._pvResamplerDelete = handleWasm.pvResamplerDelete; 76 | 77 | this._cAlignedAlloc = handleWasm.cAlignedAlloc; 78 | 79 | this._wasmMemory = handleWasm.memory; 80 | this._inputBufferAddress = handleWasm.inputBufferAddress; 81 | this._objectAddress = handleWasm.objectAddress; 82 | this._outputBufferAddress = handleWasm.outputBufferAddress; 83 | 84 | this._frameLength = handleWasm.frameLength; 85 | this._inputBufferLength = handleWasm.inputFrameLength; 86 | } 87 | 88 | public static setWasm(wasm: string): void { 89 | if (this._wasm === undefined) { 90 | this._wasm = wasm; 91 | } 92 | } 93 | 94 | public static async create( 95 | inputFrequency: number, 96 | outputFrequency: number, 97 | order: number, 98 | frameLength: number, 99 | ): Promise { 100 | const wasmOutput = await Resampler.initWasm( 101 | inputFrequency, 102 | outputFrequency, 103 | order, 104 | frameLength, 105 | ); 106 | 107 | return new Resampler(wasmOutput); 108 | } 109 | 110 | private static async initWasm( 111 | inputFrequency: number, 112 | outputFrequency: number, 113 | order: number, 114 | frameLength: number, 115 | ): Promise { 116 | // A WebAssembly page has a constant size of 64KiB. -> 4MiB ~= 64 pages 117 | // minimum memory requirements for init: 2 pages 118 | const memory = new WebAssembly.Memory({ initial: 64 }); 119 | 120 | const memoryBufferUint8 = new Uint8Array(memory.buffer); 121 | 122 | const pvConsoleLogWasm = function(index: number): void { 123 | // eslint-disable-next-line no-console 124 | console.log(arrayBufferToStringAtIndex(memoryBufferUint8, index)); 125 | }; 126 | 127 | const pvAssertWasm = function( 128 | expr: number, 129 | line: number, 130 | fileNameAddress: number, 131 | ): void { 132 | if (expr === 0) { 133 | const fileName = arrayBufferToStringAtIndex( 134 | memoryBufferUint8, 135 | fileNameAddress, 136 | ); 137 | throw new Error(`assertion failed at line ${line} in "${fileName}"`); 138 | } 139 | }; 140 | 141 | const importObject = { 142 | // eslint-disable-next-line camelcase 143 | wasi_snapshot_preview1: wasiSnapshotPreview1Emulator, 144 | env: { 145 | memory: memory, 146 | // eslint-disable-next-line camelcase 147 | pv_console_log_wasm: pvConsoleLogWasm, 148 | // eslint-disable-next-line camelcase 149 | pv_assert_wasm: pvAssertWasm, 150 | }, 151 | }; 152 | 153 | const wasmCodeArray = base64ToUint8Array(this._wasm); 154 | const { instance } = await WebAssembly.instantiate( 155 | wasmCodeArray, 156 | importObject, 157 | ); 158 | 159 | const cAlignedAlloc = instance.exports.aligned_alloc as aligned_alloc_type; 160 | 161 | const pvResamplerInit = instance.exports.pv_resampler_init as pv_resampler_init_type; 162 | const pvResamplerConvertNumSamplesToInputSampleRate = 163 | instance.exports.pv_resampler_convert_num_samples_to_input_sample_rate as 164 | pv_resampler_convert_num_samples_to_input_sample_rate_type; 165 | const pvResamplerConvertNumSamplesToOutputSampleRate = 166 | instance.exports.pv_resampler_convert_num_samples_to_output_sample_rate as 167 | pv_resampler_convert_num_samples_to_output_sample_rate_type; 168 | const pvResamplerVersion = instance.exports.pv_resampler_version as pv_resampler_version_type; 169 | 170 | const objectAddressAddress = cAlignedAlloc( 171 | Int32Array.BYTES_PER_ELEMENT, 172 | Int32Array.BYTES_PER_ELEMENT, 173 | ); 174 | if (objectAddressAddress === 0) { 175 | throw new Error('malloc failed: Cannot allocate memory'); 176 | } 177 | const status = pvResamplerInit( 178 | inputFrequency, 179 | outputFrequency, 180 | order, 181 | objectAddressAddress, 182 | ); 183 | 184 | const versionAddress = pvResamplerVersion(); 185 | const version = arrayBufferToStringAtIndex( 186 | memoryBufferUint8, 187 | versionAddress, 188 | ); 189 | 190 | if (status !== PV_STATUS_SUCCESS) { 191 | throw new Error(`pv_resampler_init failed with status ${status}`); 192 | } 193 | const memoryBufferView = new DataView(memory.buffer); 194 | const objectAddress = memoryBufferView.getInt32(objectAddressAddress, true); 195 | 196 | const inputFrameLength = pvResamplerConvertNumSamplesToInputSampleRate(objectAddress, frameLength) + 1; 197 | 198 | const inputBufferAddress = cAlignedAlloc( 199 | Int16Array.BYTES_PER_ELEMENT, 200 | inputFrameLength * Int16Array.BYTES_PER_ELEMENT, 201 | ); 202 | if (inputBufferAddress === 0) { 203 | throw new Error('malloc failed: Cannot allocate memory'); 204 | } 205 | const outputBufferAddress = cAlignedAlloc( 206 | Int16Array.BYTES_PER_ELEMENT, 207 | frameLength * Int16Array.BYTES_PER_ELEMENT, 208 | ); 209 | if (outputBufferAddress === 0) { 210 | throw new Error('malloc failed: Cannot allocate memory'); 211 | } 212 | 213 | const pvResamplerReset = instance.exports 214 | .pv_resampler_reset as pv_resampler_reset_type; 215 | const pvResamplerProcess = instance.exports 216 | .pv_resampler_process as pv_resampler_process_type; 217 | const pvResamplerDelete = instance.exports 218 | .pv_resampler_delete as pv_resampler_delete_type; 219 | 220 | return { 221 | cAlignedAlloc: cAlignedAlloc, 222 | frameLength: frameLength, 223 | inputBufferAddress: inputBufferAddress, 224 | inputFrameLength: inputFrameLength, 225 | memory: memory, 226 | objectAddress: objectAddress, 227 | outputBufferAddress: outputBufferAddress, 228 | pvResamplerConvertNumSamplesToInputSampleRate, 229 | pvResamplerConvertNumSamplesToOutputSampleRate, 230 | pvResamplerDelete: pvResamplerDelete, 231 | pvResamplerInit: pvResamplerInit, 232 | pvResamplerProcess: pvResamplerProcess, 233 | pvResamplerReset: pvResamplerReset, 234 | version: version, 235 | }; 236 | } 237 | 238 | public process( 239 | inputFrame: Int16Array | Float32Array, 240 | outputBuffer: Int16Array, 241 | ): number { 242 | if (inputFrame.length > this._inputBufferLength) { 243 | throw new Error(`InputFrame length '${inputFrame.length}' must be smaller than ${this._inputBufferLength}.`); 244 | } 245 | 246 | let inputBuffer = new Int16Array(inputFrame.length); 247 | if (inputFrame.constructor === Float32Array) { 248 | for (let i = 0; i < inputFrame.length; i++) { 249 | if (inputFrame[i] < 0) { 250 | inputBuffer[i] = 0x8000 * inputFrame[i]; 251 | } else { 252 | inputBuffer[i] = 0x7fff * inputFrame[i]; 253 | } 254 | } 255 | } else if (inputFrame.constructor === Int16Array) { 256 | inputBuffer = inputFrame; 257 | } else { 258 | throw new Error(`Invalid inputFrame type: ${typeof inputFrame}. Expected Float32Array or Int16Array.`); 259 | } 260 | 261 | const memoryBuffer = new Int16Array(this._wasmMemory.buffer); 262 | 263 | memoryBuffer.set( 264 | inputBuffer, 265 | this._inputBufferAddress / Int16Array.BYTES_PER_ELEMENT, 266 | ); 267 | 268 | const processedSamples = this._pvResamplerProcess( 269 | this._objectAddress, 270 | this._inputBufferAddress, 271 | inputFrame.length, 272 | this._outputBufferAddress, 273 | ); 274 | 275 | const memoryBufferView = new DataView(this._wasmMemory.buffer); 276 | 277 | for (let i = 0; i < processedSamples; i++) { 278 | outputBuffer[i] = memoryBufferView.getInt16( 279 | this._outputBufferAddress + i * Int16Array.BYTES_PER_ELEMENT, 280 | true, 281 | ); 282 | } 283 | return processedSamples; 284 | } 285 | 286 | public reset(): void { 287 | this._pvResamplerReset(this._objectAddress); 288 | } 289 | 290 | public release(): void { 291 | this._pvResamplerDelete(this._objectAddress); 292 | } 293 | 294 | 295 | get inputBufferLength(): number { 296 | return this._inputBufferLength; 297 | } 298 | 299 | get frameLength(): number { 300 | return this._frameLength; 301 | } 302 | 303 | get version(): string { 304 | return Resampler._version; 305 | } 306 | 307 | public getNumRequiredInputSamples(numSample: number): number { 308 | return this._pvResamplerConvertNumSamplesToInputSampleRate( 309 | this._objectAddress, 310 | numSample, 311 | ); 312 | } 313 | 314 | public getNumRequiredOutputSamples(numSample: number): number { 315 | return this._pvResamplerConvertNumSamplesToOutputSampleRate( 316 | this._objectAddress, 317 | numSample, 318 | ); 319 | } 320 | } 321 | 322 | export default Resampler; 323 | -------------------------------------------------------------------------------- /src/resampler_worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | import ResampleWorker from 'web-worker:./resampler_worker_handler.ts'; 13 | 14 | import { 15 | ResamplerWorkerInitResponse, 16 | ResamplerWorkerNumRequiredInputSamplesResponse, 17 | ResamplerWorkerProcessResponse, 18 | ResamplerWorkerReleaseResponse, 19 | ResamplerWorkerResetResponse, 20 | } from './types'; 21 | 22 | export default class ResamplerWorker { 23 | private readonly _worker: Worker; 24 | private readonly _version: string; 25 | 26 | private static _wasm: string; 27 | 28 | private constructor(worker: Worker, version: string) { 29 | this._worker = worker; 30 | this._version = version; 31 | } 32 | 33 | public static setWasm(wasm: string): void { 34 | if (this._wasm === undefined) { 35 | this._wasm = wasm; 36 | } 37 | } 38 | 39 | public static async create( 40 | inputSampleRate: number, 41 | outputSampleRate: number, 42 | filterOrder: number, 43 | frameLength: number, 44 | resampleCallback: (inputFrame: Int16Array) => void, 45 | ): Promise { 46 | const worker = new ResampleWorker(); 47 | const returnPromise: Promise = new Promise((resolve, reject) => { 48 | // @ts-ignore - block from GC 49 | this.worker = worker; 50 | worker.onmessage = (event: MessageEvent): void => { 51 | switch (event.data.command) { 52 | case 'ok': 53 | worker.onmessage = (ev: MessageEvent): void => { 54 | switch (ev.data.command) { 55 | case 'ok': 56 | resampleCallback(ev.data.result); 57 | break; 58 | case 'failed': 59 | case 'error': 60 | // eslint-disable-next-line no-console 61 | console.error(ev.data.message); 62 | break; 63 | default: 64 | // @ts-ignore 65 | // eslint-disable-next-line no-console 66 | console.error(`Unrecognized command: ${event.data.command}`); 67 | } 68 | }; 69 | resolve(new ResamplerWorker(worker, event.data.version)); 70 | break; 71 | case 'failed': 72 | case 'error': 73 | reject(event.data.message); 74 | break; 75 | default: 76 | // @ts-ignore 77 | reject(`Unrecognized command: ${event.data.command}`); 78 | } 79 | }; 80 | }); 81 | 82 | worker.postMessage({ 83 | command: 'init', 84 | wasm: this._wasm, 85 | inputSampleRate: inputSampleRate, 86 | outputSampleRate: outputSampleRate, 87 | filterOrder: filterOrder, 88 | frameLength: frameLength, 89 | }); 90 | 91 | return returnPromise; 92 | } 93 | 94 | public process(inputFrame: Int16Array | Float32Array): void { 95 | this._worker.postMessage({ 96 | command: 'process', 97 | inputFrame: inputFrame, 98 | }, [inputFrame.buffer]); 99 | } 100 | 101 | public reset(): Promise { 102 | const returnPromise: Promise = new Promise((resolve, reject) => { 103 | this._worker.onmessage = (event: MessageEvent): void => { 104 | switch (event.data.command) { 105 | case 'ok': 106 | resolve(); 107 | break; 108 | case 'failed': 109 | case 'error': 110 | reject(event.data.message); 111 | break; 112 | default: 113 | // @ts-ignore 114 | reject(`Unrecognized command: ${event.data.command}`); 115 | } 116 | }; 117 | }); 118 | 119 | this._worker.postMessage({ 120 | command: 'reset' 121 | }); 122 | 123 | return returnPromise; 124 | } 125 | 126 | public release(): Promise { 127 | const returnPromise: Promise = new Promise((resolve, reject) => { 128 | this._worker.onmessage = (event: MessageEvent): void => { 129 | switch (event.data.command) { 130 | case 'ok': 131 | resolve(); 132 | break; 133 | case 'failed': 134 | case 'error': 135 | reject(event.data.message); 136 | break; 137 | default: 138 | // @ts-ignore 139 | reject(`Unrecognized command: ${event.data.command}`); 140 | } 141 | }; 142 | }); 143 | 144 | this._worker.postMessage({ 145 | command: 'release' 146 | }); 147 | 148 | return returnPromise; 149 | } 150 | 151 | public terminate(): void { 152 | this._worker.terminate(); 153 | } 154 | 155 | public getNumRequiredInputSamples(numSample: number): Promise { 156 | const returnPromise: Promise = new Promise((resolve, reject) => { 157 | this._worker.onmessage = (event: MessageEvent): void => { 158 | switch (event.data.command) { 159 | case 'ok': 160 | resolve(event.data.result); 161 | break; 162 | case 'failed': 163 | case 'error': 164 | reject(event.data.message); 165 | break; 166 | default: 167 | // @ts-ignore 168 | reject(`Unrecognized command: ${event.data.command}`); 169 | } 170 | }; 171 | }); 172 | 173 | this._worker.postMessage({ 174 | command: 'numRequiredInputSamples', 175 | numSample: numSample, 176 | }); 177 | 178 | return returnPromise; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/resampler_worker_handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018-2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | // @ts-ignore 13 | declare const self: ServiceWorkerGlobalScope; 14 | 15 | import {ResamplerWorkerRequest} from './types'; 16 | import Resampler from './resampler'; 17 | 18 | let accumulator: BufferAccumulator | null = null; 19 | let resampler: Resampler | null = null; 20 | 21 | class BufferAccumulator { 22 | private readonly _frameLength: number; 23 | private readonly _inputBufferLength: number; 24 | 25 | private _buffer: Int16Array; 26 | 27 | private _copied: number; 28 | 29 | constructor(frameLength: number, inputBufferLength: number) { 30 | this._frameLength = frameLength; 31 | this._inputBufferLength = inputBufferLength; 32 | this._buffer = new Int16Array(frameLength); 33 | this._copied = 0; 34 | } 35 | 36 | public process(frames: Int16Array | Float32Array): void { 37 | let remaining = frames.length; 38 | 39 | while (remaining > 0) { 40 | const toProcess = Math.min(remaining, this._inputBufferLength); 41 | const outputBuffer = new Int16Array(this._frameLength); 42 | const processedSamples = resampler?.process(frames.slice(0, toProcess), outputBuffer) ?? 0; 43 | 44 | const toCopy = Math.min(processedSamples, this._frameLength - this._copied); 45 | this._buffer.set(outputBuffer.slice(0, toCopy), this._copied); 46 | if (toCopy < processedSamples) { 47 | self.postMessage({ 48 | command: 'ok', 49 | result: this._buffer, 50 | }); 51 | this._copied = 0; 52 | this._buffer = new Int16Array(this._frameLength); 53 | this._buffer.set(outputBuffer.slice(toCopy, processedSamples), 0); 54 | this._copied = processedSamples - toCopy; 55 | } else { 56 | this._copied += toCopy; 57 | } 58 | frames = frames.slice(toProcess, frames.length); 59 | remaining -= toProcess; 60 | } 61 | } 62 | } 63 | 64 | onmessage = async function (event: MessageEvent): Promise { 65 | switch (event.data.command) { 66 | case 'init': 67 | if (resampler !== null) { 68 | self.postMessage({ 69 | command: 'error', 70 | message: 'Resampler already initialized', 71 | }); 72 | return; 73 | } 74 | try { 75 | Resampler.setWasm(event.data.wasm); 76 | resampler = await Resampler.create( 77 | event.data.inputSampleRate, 78 | event.data.outputSampleRate, 79 | event.data.filterOrder, 80 | event.data.frameLength, 81 | ); 82 | accumulator = new BufferAccumulator( 83 | resampler.frameLength, 84 | resampler.inputBufferLength); 85 | 86 | self.postMessage({ 87 | command: 'ok', 88 | version: resampler.version, 89 | }); 90 | } catch (e: any) { 91 | self.postMessage({ 92 | command: 'error', 93 | message: e.message, 94 | }); 95 | } 96 | break; 97 | case 'process': 98 | if (resampler === null) { 99 | self.postMessage({ 100 | command: 'error', 101 | message: 'Resampler not initialized', 102 | }); 103 | return; 104 | } 105 | try { 106 | const {inputFrame} = event.data; 107 | accumulator?.process(inputFrame); 108 | } catch (e: any) { 109 | self.postMessage({ 110 | command: 'error', 111 | message: e.message, 112 | }); 113 | return; 114 | } 115 | break; 116 | case 'reset': 117 | if (resampler === null) { 118 | self.postMessage({ 119 | command: 'error', 120 | message: 'Resampler not initialized', 121 | }); 122 | return; 123 | } 124 | resampler.reset(); 125 | self.postMessage({ 126 | command: 'ok', 127 | }); 128 | break; 129 | case 'release': 130 | if (resampler === null) { 131 | self.postMessage({ 132 | command: 'error', 133 | message: 'Resampler not initialized', 134 | }); 135 | return; 136 | } 137 | resampler.release(); 138 | resampler = null; 139 | accumulator = null; 140 | self.postMessage({ 141 | command: 'ok', 142 | }); 143 | break; 144 | case 'numRequiredInputSamples': 145 | if (resampler === null) { 146 | self.postMessage({ 147 | command: 'error', 148 | message: 'Resampler not initialized', 149 | }); 150 | return; 151 | } 152 | try { 153 | self.postMessage({ 154 | command: 'ok', 155 | result: resampler.getNumRequiredInputSamples(event.data.numSample), 156 | }); 157 | } catch (e: any) { 158 | self.postMessage({ 159 | command: 'error', 160 | message: e.message, 161 | }); 162 | } 163 | break; 164 | default: 165 | // @ts-ignore 166 | // eslint-disable-next-line no-console 167 | console.warn(`Unhandled message in resampler_worker.ts: ${event.data.command}`); 168 | break; 169 | } 170 | }; 171 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018-2024 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | export enum WvpState { 13 | STARTED, 14 | STOPPED, 15 | } 16 | 17 | export type WvpMessageEvent = { 18 | command: 'process', 19 | inputFrame: Int16Array 20 | }; 21 | 22 | export type PvEngine = { 23 | onmessage?: ((e: MessageEvent) => any) | null; 24 | postMessage?: (e: any) => void; 25 | worker?: { 26 | onmessage?: ((e: MessageEvent) => any) | null; 27 | postMessage?: (e: any) => void; 28 | } 29 | } 30 | 31 | export type WebVoiceProcessorOptions = { 32 | /** Size of pcm frames (default: 512) */ 33 | frameLength?: number; 34 | /** Which sample rate to convert to (default: 16000) */ 35 | outputSampleRate?: number; 36 | /** Microphone id to use (can be fetched with mediaDevices.enumerateDevices) */ 37 | deviceId?: string | null; 38 | /** Filter order (default: 50) */ 39 | filterOrder?: number; 40 | /** Custom made recorder processor */ 41 | customRecorderProcessorURL?: string; 42 | }; 43 | 44 | export type ResamplerWorkerInitRequest = { 45 | command: 'init'; 46 | wasm: string; 47 | inputSampleRate: number; 48 | outputSampleRate: number; 49 | frameLength: number; 50 | filterOrder: number; 51 | }; 52 | 53 | export type ResamplerWorkerProcessRequest = { 54 | command: 'process'; 55 | inputFrame: Float32Array | Int16Array; 56 | }; 57 | 58 | export type ResamplerWorkerResetRequest = { 59 | command: 'reset'; 60 | }; 61 | 62 | export type ResamplerWorkerReleaseRequest = { 63 | command: 'release'; 64 | }; 65 | 66 | export type ResamplerWorkerNumRequiredInputSamplesRequest = { 67 | command: 'numRequiredInputSamples'; 68 | numSample: number; 69 | }; 70 | 71 | export type ResamplerWorkerRequest = 72 | | ResamplerWorkerInitRequest 73 | | ResamplerWorkerProcessRequest 74 | | ResamplerWorkerResetRequest 75 | | ResamplerWorkerReleaseRequest 76 | | ResamplerWorkerNumRequiredInputSamplesRequest; 77 | 78 | export type ResamplerWorkerFailureResponse = { 79 | command: 'failed' | 'error'; 80 | message: string; 81 | }; 82 | 83 | export type ResamplerWorkerInitResponse = ResamplerWorkerFailureResponse | { 84 | command: 'ok'; 85 | version: string; 86 | }; 87 | 88 | export type ResamplerWorkerProcessResponse = ResamplerWorkerFailureResponse | { 89 | command: 'ok'; 90 | result: Int16Array; 91 | }; 92 | 93 | export type ResamplerWorkerResetResponse = ResamplerWorkerFailureResponse | { 94 | command: 'ok'; 95 | }; 96 | 97 | export type ResamplerWorkerReleaseResponse = ResamplerWorkerFailureResponse | { 98 | command: 'ok'; 99 | }; 100 | 101 | export type ResamplerWorkerNumRequiredInputSamplesResponse = ResamplerWorkerFailureResponse | { 102 | command: 'ok'; 103 | result: number; 104 | }; 105 | 106 | export type ResamplerWorkerResponse = 107 | ResamplerWorkerInitResponse | 108 | ResamplerWorkerProcessResponse | 109 | ResamplerWorkerResetResponse | 110 | ResamplerWorkerReleaseResponse; 111 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | export type BrowserFeatures = { 13 | _picovoice: boolean; 14 | AudioWorklet: boolean; 15 | isSecureContext: boolean; 16 | mediaDevices: boolean; 17 | WebAssembly: boolean; 18 | webKitGetUserMedia: boolean; 19 | Worker: boolean; 20 | }; 21 | 22 | /** 23 | * Check for browser compatibility with Picovoice: WebAssembly, Web Audio API, etc. 24 | * 25 | * @return object with compatibility details, with special key '_picovoice' offering a yes/no answer. 26 | */ 27 | export function browserCompatibilityCheck(): BrowserFeatures { 28 | // Are we in a secure context? Microphone access requires HTTPS (with the exception of localhost, for development) 29 | const _isSecureContext = window.isSecureContext; 30 | 31 | // Web Audio API 32 | const _mediaDevices = navigator.mediaDevices !== undefined; 33 | const _webkitGetUserMedia = 34 | // @ts-ignore 35 | navigator.webkitGetUserMedia !== undefined; 36 | 37 | // Web Workers 38 | const _Worker = window.Worker !== undefined; 39 | 40 | // WebAssembly 41 | const _WebAssembly = typeof WebAssembly === 'object'; 42 | 43 | // AudioWorklet (not yet used, due to lack of Safari support) 44 | const _AudioWorklet = typeof AudioWorklet === 'function'; 45 | 46 | // Picovoice requirements met? 47 | const _picovoice = _mediaDevices && _WebAssembly && _Worker; 48 | 49 | return { 50 | _picovoice: _picovoice, 51 | AudioWorklet: _AudioWorklet, 52 | isSecureContext: _isSecureContext, 53 | mediaDevices: _mediaDevices, 54 | WebAssembly: _WebAssembly, 55 | webKitGetUserMedia: _webkitGetUserMedia, 56 | Worker: _Worker, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/wasi_snapshot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | /* eslint camelcase: 0, arrow-body-style: 0, @typescript-eslint/no-unused-vars: 0, @typescript-eslint/explicit-module-boundary-types: 0 */ 13 | 14 | export const wasiSnapshotPreview1Emulator = { 15 | args_get: (input: any): any => { 16 | return 0; 17 | }, 18 | args_sizes_get: (input: any): any => { 19 | return 0; 20 | }, 21 | environ_get: (input: any): any => { 22 | return 0; 23 | }, 24 | environ_sizes_get: (input: any): any => { 25 | return 0; 26 | }, 27 | clock_res_get: (input: any): any => { 28 | return 0; 29 | }, 30 | clock_time_get: (input: any): any => { 31 | return 0; 32 | }, 33 | fd_advise: (input: any): any => { 34 | return 0; 35 | }, 36 | fd_allocate: (input: any): any => { 37 | return 0; 38 | }, 39 | fd_close: (input: any): any => { 40 | return 0; 41 | }, 42 | fd_datasync: (input: any): any => { 43 | return 0; 44 | }, 45 | fd_fdstat_get: (input: any): any => { 46 | return 0; 47 | }, 48 | fd_fdstat_set_flags: (input: any): any => { 49 | return 0; 50 | }, 51 | fd_fdstat_set_rights: (input: any): any => { 52 | return 0; 53 | }, 54 | fd_filestat_get: (input: any): any => { 55 | return 0; 56 | }, 57 | fd_filestat_set_size: (input: any): any => { 58 | return 0; 59 | }, 60 | fd_filestat_set_times: (input: any): any => { 61 | return 0; 62 | }, 63 | fd_pread: (input: any): any => { 64 | return 0; 65 | }, 66 | fd_prestat_get: (input: any): any => { 67 | return 0; 68 | }, 69 | fd_prestat_dir_name: (input: any): any => { 70 | return 0; 71 | }, 72 | fd_pwrite: (input: any): any => { 73 | return 0; 74 | }, 75 | fd_read: (input: any): any => { 76 | return 0; 77 | }, 78 | fd_readdir: (input: any): any => { 79 | return 0; 80 | }, 81 | fd_renumber: (input: any): any => { 82 | return 0; 83 | }, 84 | fd_seek: (input: any): any => { 85 | return 0; 86 | }, 87 | fd_sync: (input: any): any => { 88 | return 0; 89 | }, 90 | fd_tell: (input: any): any => { 91 | return 0; 92 | }, 93 | fd_write: (input: any): any => { 94 | return 0; 95 | }, 96 | path_create_directory: (input: any): any => { 97 | return 0; 98 | }, 99 | path_filestat_get: (input: any): any => { 100 | return 0; 101 | }, 102 | path_filestat_set_times: (input: any): any => { 103 | return 0; 104 | }, 105 | path_link: (input: any): any => { 106 | return 0; 107 | }, 108 | path_open: (input: any): any => { 109 | return 0; 110 | }, 111 | path_readlink: (input: any): any => { 112 | return 0; 113 | }, 114 | path_remove_directory: (input: any): any => { 115 | return 0; 116 | }, 117 | path_rename: (input: any): any => { 118 | return 0; 119 | }, 120 | path_symlink: (input: any): any => { 121 | return 0; 122 | }, 123 | path_unlink_file: (input: any): any => { 124 | return 0; 125 | }, 126 | poll_oneoff: (input: any): any => { 127 | return 0; 128 | }, 129 | proc_exit: (input: any): any => { 130 | return 0; 131 | }, 132 | proc_raise: (input: any): any => { 133 | return 0; 134 | }, 135 | sched_yield: (input: any): any => { 136 | return 0; 137 | }, 138 | random_get: (input: any): any => { 139 | return 0; 140 | }, 141 | sock_recv: (input: any): any => { 142 | return 0; 143 | }, 144 | sock_send: (input: any): any => { 145 | return 0; 146 | }, 147 | sock_shutdown: (input: any): any => { 148 | return 0; 149 | }, 150 | }; 151 | -------------------------------------------------------------------------------- /src/web_voice_processor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018-2023 Picovoice Inc. 3 | 4 | You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" 5 | file accompanying this source. 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 8 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | */ 11 | 12 | import { Mutex } from 'async-mutex'; 13 | 14 | import { base64ToUint8Array } from '@picovoice/web-utils'; 15 | 16 | import ResamplerWorker from './resampler_worker'; 17 | import recorderProcessor from './audio_worklet/recorder_processor.js'; 18 | 19 | import { PvEngine, WebVoiceProcessorOptions, WvpState } from './types'; 20 | 21 | import { AudioDumpEngine } from './engines/audio_dump_engine'; 22 | 23 | /** 24 | * WebVoiceProcessor Error Class 25 | */ 26 | export class WvpError extends Error { 27 | constructor(name: string, message: string) { 28 | super(message); 29 | this.name = name; 30 | } 31 | } 32 | 33 | /** 34 | * Obtain microphone permission and audio stream; 35 | * Down sample audio into 16kHz single-channel PCM for speech recognition (via ResamplerWorker). 36 | * Continuously send audio frames to voice processing engines. 37 | */ 38 | export class WebVoiceProcessor { 39 | private _mutex = new Mutex(); 40 | 41 | private _audioContext: AudioContext | null = null; 42 | private _microphoneStream: MediaStream | null = null; 43 | private _recorderNode: AudioWorkletNode | null = null; 44 | private _resamplerWorker: ResamplerWorker | null = null; 45 | 46 | private readonly _engines: Set; 47 | private _options: WebVoiceProcessorOptions = {}; 48 | private _state: WvpState; 49 | 50 | private static _instance: WebVoiceProcessor | undefined; 51 | 52 | private constructor() { 53 | this._engines = new Set(); 54 | this._options = {}; 55 | this._state = WvpState.STOPPED; 56 | } 57 | 58 | /** 59 | * Gets the WebVoiceProcessor singleton instance. 60 | * 61 | * @return WebVoiceProcessor singleton. 62 | */ 63 | private static instance(): WebVoiceProcessor { 64 | if (!this._instance) { 65 | this._instance = new WebVoiceProcessor(); 66 | } 67 | return this._instance; 68 | } 69 | 70 | /** 71 | * Record some sample raw signed 16-bit PCM data for some duration, then pack it as a Blob. 72 | * 73 | * @param durationMs the duration of the recording, in milliseconds 74 | * @return the data in Blob format, wrapped in a promise 75 | */ 76 | public static async audioDump(durationMs: number = 3000): Promise { 77 | const audioDumpEngine = new AudioDumpEngine(); 78 | await this.subscribe(audioDumpEngine); 79 | return new Promise(resolve => { 80 | // @ts-ignore 81 | this.audioDumpEngine = audioDumpEngine; 82 | setTimeout(() => { 83 | this.unsubscribe(audioDumpEngine); 84 | resolve(audioDumpEngine.onend()); 85 | }, durationMs); 86 | }); 87 | } 88 | 89 | /** 90 | * Subscribe an engine. A subscribed engine will receive audio frames via 91 | * `.postMessage({command: 'process', inputFrame: inputFrame})`. 92 | * @param engines The engine(s) to subscribe. 93 | */ 94 | public static async subscribe(engines: PvEngine | PvEngine[]): Promise { 95 | for (const engine of (Array.isArray(engines) ? engines : [engines])) { 96 | if (!engine) { 97 | throw new WvpError("InvalidEngine", "Null or undefined engine."); 98 | } 99 | 100 | if (engine.worker) { 101 | if (engine.worker.postMessage && typeof engine.worker.postMessage === 'function') { 102 | this.instance()._engines.add(engine); 103 | } else { 104 | throw new WvpError("InvalidEngine", "Engine must have a 'onmessage' handler."); 105 | } 106 | } else { 107 | if (engine.postMessage && typeof engine.postMessage === 'function') { 108 | this.instance()._engines.add(engine); 109 | } else if (engine.onmessage && typeof engine.onmessage === 'function') { 110 | this.instance()._engines.add(engine); 111 | } else { 112 | throw new WvpError("InvalidEngine", "Engine must have a 'onmessage' handler."); 113 | } 114 | } 115 | } 116 | 117 | if (this.instance()._engines.size > 0 && this.instance()._state !== WvpState.STARTED) { 118 | await this.instance().start(); 119 | } 120 | } 121 | 122 | /** 123 | * Unsubscribe an engine. 124 | * @param engines The engine(s) to unsubscribe. 125 | */ 126 | public static async unsubscribe(engines: PvEngine | PvEngine[]): Promise { 127 | for (const engine of (Array.isArray(engines) ? engines : [engines])) { 128 | this.instance()._engines.delete(engine); 129 | } 130 | 131 | if (this.instance()._engines.size === 0 && this.instance()._state !== WvpState.STOPPED) { 132 | await this.instance().stop(); 133 | } 134 | } 135 | 136 | /** 137 | * Removes all engines and stops recording audio. 138 | */ 139 | static async reset(): Promise { 140 | this.instance()._engines.clear(); 141 | await this.instance().stop(); 142 | } 143 | 144 | /** 145 | * Set new WebVoiceProcessor options. 146 | * If forceUpdate is not set to true, all engines must be unsubscribed and subscribed 147 | * again in order for the recorder to take the new changes. 148 | * Using forceUpdate might allow a small gap where audio frames is not received. 149 | * 150 | * @param options WebVoiceProcessor recording options. 151 | * @param forceUpdate Flag to force update recorder with new options. 152 | */ 153 | static setOptions(options: WebVoiceProcessorOptions, forceUpdate = false): void { 154 | this.instance()._options = options; 155 | if (forceUpdate) { 156 | this.instance().stop().then(async () => { 157 | await this.instance().start(); 158 | }); 159 | } 160 | } 161 | 162 | /** 163 | * Gets the current audio context. 164 | */ 165 | static get audioContext(): AudioContext | null { 166 | return this.instance()._audioContext; 167 | } 168 | 169 | /** 170 | * Flag to check if it is currently recording. 171 | */ 172 | static get isRecording(): boolean { 173 | return this.instance()._state === WvpState.STARTED; 174 | } 175 | 176 | /** 177 | * Resumes or starts audio context. Also initializes resampler, capture device and other configurations 178 | * based on `options`. 179 | */ 180 | private start(): Promise { 181 | return new Promise((resolve, reject) => { 182 | this._mutex 183 | .runExclusive(async () => { 184 | try { 185 | if (this._audioContext === null || this._state === WvpState.STOPPED || this.isReleased) { 186 | const { audioContext, microphoneStream, recorderNode, resamplerWorker } = await this.setupRecorder(this._options); 187 | this._audioContext = audioContext; 188 | this._microphoneStream = microphoneStream; 189 | this._recorderNode = recorderNode; 190 | this._resamplerWorker = resamplerWorker; 191 | 192 | recorderNode.port.onmessage = (event: MessageEvent): void => { 193 | resamplerWorker.process(event.data.buffer[0]); 194 | }; 195 | this._state = WvpState.STARTED; 196 | } 197 | 198 | if (this._audioContext !== null && this.isSuspended) { 199 | await this._audioContext.resume(); 200 | } 201 | } catch (error: any) { 202 | if (error && error.name) { 203 | if (error.name === 'SecurityError' || error.name === 'NotAllowedError') { 204 | throw new WvpError( 205 | 'PermissionError', 206 | 'Failed to record audio: microphone permissions denied.' 207 | ); 208 | } else if (error.name === 'NotFoundError' || error.name === 'OverconstrainedError') { 209 | throw new WvpError( 210 | 'DeviceMissingError', 211 | 'Failed to record audio: audio recording device was not found.' 212 | ); 213 | } else if (error.name === 'NotReadableError') { 214 | throw new WvpError( 215 | 'DeviceReadError', 216 | 'Failed to record audio: audio recording device is not working correctly.' 217 | ); 218 | } 219 | } else { 220 | throw error; 221 | } 222 | } 223 | }) 224 | .then(() => { 225 | resolve(); 226 | }) 227 | .catch((error: any) => { 228 | reject(error); 229 | }); 230 | }); 231 | } 232 | 233 | /** 234 | * Stops and closes resources used. Furthermore, terminates and stops any other 235 | * instance created initially. 236 | * AudioContext is kept alive to be used when starting again. 237 | */ 238 | private stop(): Promise { 239 | return new Promise((resolve, reject) => { 240 | this._mutex 241 | .runExclusive(async () => { 242 | if (this._audioContext !== null && this._state !== WvpState.STOPPED) { 243 | this._resamplerWorker?.terminate(); 244 | this._recorderNode?.port.close(); 245 | this._microphoneStream?.getAudioTracks().forEach(track => { 246 | track.stop(); 247 | }); 248 | 249 | this._state = WvpState.STOPPED; 250 | } 251 | }) 252 | .then(() => { 253 | resolve(); 254 | }) 255 | .catch((error: any) => { 256 | reject(error); 257 | }); 258 | }); 259 | } 260 | 261 | /** 262 | * Flag to check if audio context has been suspended. 263 | */ 264 | private get isSuspended(): boolean { 265 | return this._audioContext?.state === "suspended"; 266 | } 267 | 268 | /** 269 | * Flag to check if audio context has been released. 270 | */ 271 | private get isReleased(): boolean { 272 | return this._audioContext?.state === "closed"; 273 | } 274 | 275 | private recorderCallback(inputFrame: Int16Array): void { 276 | for (const engine of this._engines) { 277 | if (engine.worker && engine.worker.postMessage) { 278 | engine.worker.postMessage({ 279 | command: 'process', 280 | inputFrame: inputFrame 281 | }); 282 | } else if (engine.postMessage) { 283 | engine.postMessage({ 284 | command: 'process', 285 | inputFrame: inputFrame 286 | }); 287 | } else if (engine.onmessage) { 288 | engine.onmessage({ 289 | data: { 290 | command: 'process', 291 | inputFrame: inputFrame 292 | } 293 | } as MessageEvent); 294 | } 295 | } 296 | } 297 | 298 | private async getAudioContext(): Promise { 299 | if (this._audioContext === null || this.isReleased) { 300 | this._audioContext = new AudioContext(); 301 | if (this._options.customRecorderProcessorURL) { 302 | await this._audioContext.audioWorklet.addModule(this._options.customRecorderProcessorURL); 303 | } else { 304 | const objectURL = URL.createObjectURL(new Blob([base64ToUint8Array(recorderProcessor).buffer], {type: 'application/javascript'})); 305 | await this._audioContext.audioWorklet.addModule(objectURL); 306 | } 307 | } 308 | return this._audioContext; 309 | } 310 | 311 | private async setupRecorder( 312 | options: WebVoiceProcessorOptions, 313 | ) { 314 | if (navigator.mediaDevices === undefined) { 315 | throw new WvpError("DeviceDisabledError", "Audio recording is not allowed or disabled."); 316 | } 317 | 318 | const { 319 | outputSampleRate = 16000, 320 | frameLength = 512, 321 | deviceId = null, 322 | filterOrder = 50, 323 | } = options; 324 | const numberOfChannels = 1; 325 | 326 | const audioContext = await this.getAudioContext(); 327 | 328 | // Get microphone access and ask user permission 329 | const microphoneStream = await navigator.mediaDevices.getUserMedia({ 330 | audio: { 331 | deviceId: deviceId ? { exact: deviceId } : undefined, 332 | }, 333 | }); 334 | 335 | const audioSource = audioContext.createMediaStreamSource(microphoneStream); 336 | 337 | const resamplerWorker = await ResamplerWorker.create( 338 | audioSource.context.sampleRate, 339 | outputSampleRate, 340 | filterOrder, 341 | frameLength, 342 | this.recorderCallback.bind(this), 343 | ); 344 | 345 | const recorderNode = new window.AudioWorkletNode( 346 | audioContext, 347 | 'recorder-processor', 348 | { 349 | processorOptions: { 350 | frameLength, 351 | numberOfChannels 352 | } 353 | } 354 | ); 355 | 356 | audioSource.connect(recorderNode); 357 | recorderNode.connect(audioContext.destination); 358 | 359 | return { 360 | audioContext, 361 | microphoneStream, 362 | recorderNode, 363 | resamplerWorker 364 | }; 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /test/resampler.test.ts: -------------------------------------------------------------------------------- 1 | import { Resampler, ResamplerWorker } from "../"; 2 | 3 | const DEBUG: boolean = Cypress.env('DEBUG'); 4 | 5 | const frameLength = 512; 6 | const testData = [ 7 | { 8 | inputFile: "9khz_noise", 9 | inputFrequency: 48000, 10 | outputFrequency: 16000, 11 | filterOrders: [30, 40, 50] 12 | }, 13 | { 14 | inputFile: "tone-9khz_noise", 15 | inputFrequency: 44100, 16 | outputFrequency: 16000, 17 | filterOrders: [100] 18 | }, 19 | ]; 20 | 21 | const frequencyToStr = (frequency: number) => `${frequency / 1000}kHz`; 22 | 23 | const avgFrameDiff = (input: Int16Array, output: Int16Array) => { 24 | let diff = 0; 25 | for (let i = 0; i < input.length; i++) { 26 | diff += Math.abs(input[i] - output[i]); 27 | } 28 | return diff / input.length; 29 | }; 30 | 31 | describe("Resampler", () => { 32 | it("Should be able to resample (main)", () => { 33 | for (const testParam of testData) { 34 | const inputFile = `audio_samples/${testParam.inputFile}_${frequencyToStr(testParam.inputFrequency)}.pcm`; 35 | 36 | for (const filterOrder of testParam.filterOrders) { 37 | const outputFile = `audio_samples/${testParam.inputFile}_${frequencyToStr(testParam.outputFrequency)}_ds_${filterOrder}.pcm`; 38 | const output = new Int16Array(frameLength); 39 | 40 | cy.getFramesFromFile(inputFile).then(async inputFrames => { 41 | const resampler = await Resampler.create( 42 | testParam.inputFrequency, 43 | testParam.outputFrequency, 44 | filterOrder, 45 | frameLength, 46 | ); 47 | 48 | cy.getFramesFromFile(outputFile).then(outputFrames => { 49 | const data = new Int16Array(outputFrames.length); 50 | 51 | for (let i = 0, j = 0; j < outputFrames.length; i += frameLength) { 52 | const processed = resampler.process(inputFrames.slice(i, i + frameLength), output); 53 | 54 | data.set(output.slice(0, processed), j); 55 | j += processed; 56 | } 57 | resampler.release(); 58 | 59 | if (DEBUG) { 60 | const blob = new Blob([data], {type: "application/blob"});// change resultByte to bytes 61 | 62 | const link = document.createElement('a'); 63 | link.href = window.URL.createObjectURL(blob); 64 | link.download = `main-${outputFile}`; 65 | link.click(); 66 | } 67 | 68 | const diff = avgFrameDiff(data, outputFrames); 69 | expect(diff).to.be.lte(1, `${outputFile} comparison`); 70 | }); 71 | }); 72 | } 73 | } 74 | }); 75 | 76 | it("Should be able to resample (worker)", () => { 77 | for (const testParam of testData) { 78 | const inputFile = `audio_samples/${testParam.inputFile}_${frequencyToStr(testParam.inputFrequency)}.pcm`; 79 | for (const filterOrder of testParam.filterOrders) { 80 | cy.getFramesFromFile(inputFile).then(async inputFrames => { 81 | let data = new Int16Array(); 82 | let processedFrames = 0; 83 | 84 | const resamplerCallback = (frames: Int16Array) => { 85 | data.set(frames, processedFrames); 86 | processedFrames += frames.length; 87 | }; 88 | 89 | const resampler = await ResamplerWorker.create( 90 | testParam.inputFrequency, 91 | testParam.outputFrequency, 92 | filterOrder, 93 | frameLength, 94 | resamplerCallback 95 | ); 96 | 97 | const outputFile = `audio_samples/${testParam.inputFile}_${frequencyToStr(testParam.outputFrequency)}_ds_${filterOrder}.pcm`; 98 | 99 | cy.getFramesFromFile(outputFile).then(async outputFrames => { 100 | data = new Int16Array(outputFrames.length); 101 | 102 | for (let i = 0; i < inputFrames.length; i += frameLength) { 103 | resampler.process(inputFrames.slice(i, i + frameLength)); 104 | } 105 | 106 | const waitFor = () => new Promise(resolve => { 107 | const timer = setInterval(() => { 108 | if (processedFrames >= outputFrames.length - frameLength) { 109 | clearInterval(timer); 110 | resolve(); 111 | } 112 | }, 100); 113 | }); 114 | 115 | await waitFor(); 116 | 117 | await resampler.terminate(); 118 | 119 | if (DEBUG) { 120 | const blob = new Blob([data], {type: "application/blob"});// change resultByte to bytes 121 | 122 | const link = document.createElement('a'); 123 | link.href = window.URL.createObjectURL(blob); 124 | link.download = `worker-${outputFile}`; 125 | link.click(); 126 | } 127 | 128 | const diff = avgFrameDiff(data, outputFrames); 129 | expect(diff).to.be.lte(1, `${outputFile} comparison`); 130 | }); 131 | }); 132 | } 133 | } 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/wvp.test.ts: -------------------------------------------------------------------------------- 1 | import { WebVoiceProcessor, VuMeterEngine } from "../"; 2 | 3 | const engine = { 4 | onmessage: function(e: MessageEvent) { 5 | expect(e).to.not.be.null; 6 | } 7 | }; 8 | 9 | const vuMeter = new VuMeterEngine(db => { 10 | expect(db).to.not.be.null; 11 | }); 12 | 13 | const emptyObj = {}; 14 | 15 | describe("Web Voice Processor", () => { 16 | it("Should be able to handle engine", async () => { 17 | await WebVoiceProcessor.subscribe(engine); 18 | // @ts-ignore 19 | expect(WebVoiceProcessor.instance()._engines.size).to.be.gt(0); 20 | }); 21 | 22 | it("Should be able to remove engine", async () => { 23 | await WebVoiceProcessor.unsubscribe(engine); 24 | // @ts-ignore 25 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 26 | }); 27 | 28 | 29 | it("Should be able to add VU meter", async () => { 30 | await WebVoiceProcessor.subscribe(vuMeter); 31 | // @ts-ignore 32 | expect(WebVoiceProcessor.instance()._engines.size).to.be.gt(0); 33 | }); 34 | 35 | it("Should be able to remove VU meter", async () => { 36 | await WebVoiceProcessor.unsubscribe(vuMeter); 37 | // @ts-ignore 38 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 39 | }); 40 | 41 | it("Should be able to add/remove multiple engines", async () => { 42 | await WebVoiceProcessor.subscribe([engine, vuMeter]); 43 | // @ts-ignore 44 | expect(WebVoiceProcessor.instance()._engines.size).to.be.gt(0); 45 | 46 | await WebVoiceProcessor.unsubscribe([engine, vuMeter]); 47 | // @ts-ignore 48 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 49 | }); 50 | 51 | it("Should be able to reset", async () => { 52 | await WebVoiceProcessor.subscribe([engine, vuMeter]); 53 | // @ts-ignore 54 | expect(WebVoiceProcessor.instance()._engines.size).to.be.gt(0); 55 | 56 | await WebVoiceProcessor.reset(); 57 | // @ts-ignore 58 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 59 | }); 60 | 61 | it("Should be able to handle unexpected objects", async () => { 62 | try { 63 | await WebVoiceProcessor.subscribe(emptyObj); 64 | // @ts-ignore 65 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 66 | } catch (e) { 67 | // @ts-ignore 68 | expect(WebVoiceProcessor.instance()._engines.size).to.be.eq(0); 69 | } 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "isolatedModules": false, 7 | "lib": ["esnext", "dom"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noEmit": false, 11 | "outDir": "./dist", 12 | "removeComments": false, 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "esnext", 17 | "types": ["node"] 18 | }, 19 | "include": ["src", "module.d.ts"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | --------------------------------------------------------------------------------