diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 000000000..ecff3d69a --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,90 @@ +module.exports = { + parser: "@typescript-eslint/parser", + plugins: ["jsdoc", "html"], + extends: ["plugin:@typescript-eslint/recommended"], + rules: { + "prefer-rest-params": "off", + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "jsdoc/check-alignment": 1, + "jsdoc/check-param-names": ["error"], + // "jsdoc/check-examples": ["error"], + "jsdoc/check-indentation": [ + "error", + { excludeTags: ["example", "param"] }, + ], + "dot-location": ["error", "property"], + "linebreak-style": ["error", "unix"], + eqeqeq: ["error"], + curly: ["error", "all"], + "dot-notation": ["error"], + "no-throw-literal": ["error"], + "no-useless-call": ["error"], + "no-unmodified-loop-condition": ["error"], + "quote-props": ["error", "as-needed"], + quotes: ["error", "double"], + "no-shadow": "error", + "no-console": ["error", { allow: ["warn"] }], + "@typescript-eslint/no-object-literal-type-assertion": "off", + "@typescript-eslint/no-unused-vars": "off", + "sort-imports": [ + "error", + { + ignoreCase: true, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ["none", "all", "multiple", "single"], + }, + ], + "no-lonely-if": ["error"], + semi: ["error", "always"], + "no-cond-assign": ["error", "always"], + indent: "off", + "no-var": "error", + "prefer-arrow-callback": "error", + "@typescript-eslint/indent": [ + "error", + "tab", + { SwitchCase: 1, MemberExpression: 2 }, + ], + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "no-multi-spaces": ["error"], + "array-bracket-spacing": ["error", "never"], + "block-spacing": ["error", "always"], + "func-call-spacing": ["error", "never"], + "key-spacing": ["error", { beforeColon: false, afterColon: true }], + "brace-style": ["error", "1tbs"], + "space-in-parens": ["error", "never"], + "eol-last": ["error", "always"], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/array-type": "off", + "spaced-comment": [ + "error", + "always", + { + line: { exceptions: ["-"] }, + block: { balanced: true }, + }, + ], + "lines-between-class-members": "off", + "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1, maxBOF: 0 }], + "no-unneeded-ternary": ["error"], + "object-curly-spacing": ["error", "always"], + "space-unary-ops": ["error", { words: true, nonwords: false }], + "block-spacing": ["error", "always"], + "keyword-spacing": ["error", { before: true }], + "space-before-function-paren": [ + "error", + { anonymous: "never", named: "never", asyncArrow: "always" }, + ], + "comma-spacing": ["error", { before: false, after: true }], + "arrow-spacing": ["error", { before: true, after: true }], + "space-before-blocks": [ + "error", + { functions: "always", keywords: "always", classes: "always" }, + ], + }, +}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index accaf850a..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,73 +0,0 @@ -module.exports = { - "parser": '@typescript-eslint/parser', - "plugins": [ - '@typescript-eslint', - "jsdoc", - "html" - ], - "extends": ["plugin:@typescript-eslint/recommended"], - "rules": { - "prefer-rest-params": "off", - "@typescript-eslint/ban-ts-ignore": "off", - "@typescript-eslint/no-empty-function": "off", - "jsdoc/check-alignment": 1, - "jsdoc/check-param-names": ["error"], - "jsdoc/check-examples": ["error"], - "jsdoc/check-indentation": ["error", { "excludeTags": ["example", "param"] }], - "dot-location": ["error", "property"], - "linebreak-style": ["error", "unix"], - "eqeqeq": ["error"], - "curly": ["error", "all"], - "dot-notation": ["error"], - "no-throw-literal": ["error"], - "no-useless-call": ["error"], - "no-unmodified-loop-condition": ["error"], - "quote-props": ["error", "as-needed"], - "quotes": ["error", "double"], - "no-shadow": "error", - "no-console": ["error", { "allow": ["warn"] }], - "@typescript-eslint/no-object-literal-type-assertion": "off", - "@typescript-eslint/no-unused-vars": "off", - "sort-imports": ["error", { - "ignoreCase": true, - "ignoreDeclarationSort": true, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] - }], - "no-lonely-if": ["error"], - "semi": ["error", "always"], - "no-cond-assign": ["error", "always"], - "indent": "off", - "no-var": "error", - "prefer-arrow-callback": "error", - "@typescript-eslint/indent": ["error", "tab", { "SwitchCase": 1, "MemberExpression": 2 }], - "@typescript-eslint/explicit-member-accessibility": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "no-multi-spaces": ["error"], - "array-bracket-spacing": ["error", "never"], - "block-spacing": ["error", "always"], - "func-call-spacing": ["error", "never"], - "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], - "brace-style": ["error", "1tbs"], - "space-in-parens": ["error", "never"], - "eol-last": ["error", "always"], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/array-type": "off", - "spaced-comment": ["error", "always", { - "line": { "exceptions": ["-"] }, - "block": { "balanced": true }, - }], - "lines-between-class-members": "off", - "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1, "maxBOF": 0 }], - "no-unneeded-ternary": ["error"], - "object-curly-spacing": ["error", "always"], - "space-unary-ops": ["error", { "words": true, "nonwords": false }], - "block-spacing": ["error", "always"], - "keyword-spacing": ["error", { "before": true }], - "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], - "comma-spacing": ["error", { "before": false, "after": true }], - "arrow-spacing": ["error", { "before": true, "after": true }], - "space-before-blocks": ["error", { "functions": "always", "keywords": "always", "classes": "always" }] - } -}; diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 41209a877..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Bugs and feature requests only please. For help questions, check out the [forum](https://groups.google.com/forum/#!forum/tonejs). - -**Note**: Browsers' [Autoplay Policy](https://github.com/Tonejs/Tone.js/wiki/Autoplay) leads to a lot of subtle and inconsistent bugs where Tone.js produces no sound. Check out the link for more information and the solution. - -Please include a way to reproduce your issue - -https://jsfiddle.net/1f60jkq4/ (tone@latest) -https://jsfiddle.net/z9marxbt/ (tone@next) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..0717616a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Report an issue with Tone.js +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** + +A description of what the bug is. + +For help questions, check out the [forum](https://groups.google.com/forum/#!forum/tonejs). + +Note: Browsers' [Autoplay Policy](https://github.com/Tonejs/Tone.js/wiki/Autoplay) leads to a lot of subtle and inconsistent bugs where Tone.js produces no sound. Check out the link for more information and the solution. + +If you are experiencing loose or inaccurate timing, double check that you are [correctly scheduling events](https://github.com/Tonejs/Tone.js/wiki/Accurate-Timing). + + +**To Reproduce** + +Please include a way to reproduce your issue. If possible, please includes a link to some example code using a platform like jsfiddle or codesandbox where the code can be edited. This makes it much easier to debug the issue and also create a validation test to verify the bug was fixed. + +**Expected behavior** +A description of what you expected to happen. + +**What I've tried** +How have you tried resolving/debugging this issue? This can be helpful context for getting to the heart of the issue faster and not duplicating effort. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..baf987a0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature request +assignees: '' + +--- + +**The feature you'd like** +A description of a module or method which you'd like to be included in Tone.js + +**Any alternatives you've considered** +Are there existing modules or methods within Tone.js which can be combined to do the same thing? Are there other libraries or reference implementations which do a similar thing? + +**Additional context** +Add any other context or screenshots about the feature request here. + +**Feature Requests will eventually be closed if inactive** +Consider submitted a Pull Request for the feature you want. If no one addresses your feature, it will eventually be closed due to inactivity. Though, someone could always implement your feature request after it's closed. Closing issues automatically keeps features requests from piling up. diff --git a/.github/stale.yaml b/.github/stale.yaml new file mode 100644 index 000000000..12a751d06 --- /dev/null +++ b/.github/stale.yaml @@ -0,0 +1,9 @@ +daysUntilStale: 90 +daysUntilClose: 14 +onlyLabels: + - feature request +staleLabel: stale +markComment: > + Feature requests which don't have any contributors or activity in the past + 90 days are marked as stale. Comment on this issue if you intend on contributing + to this feature, otherwise it will be closed in two weeks. \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..6577cce09 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish +on: + workflow_run: + workflows: ["Tests"] + types: + - completed +jobs: + publish: + runs-on: ubuntu-latest + # not on PRs + if: github.event_name != 'pull_request' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_CI: true + steps: + - uses: actions/checkout@v4 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Increment version + run: npm run increment + - name: Publish @next + run: npm publish --tag next + if: ${{ github.ref == 'refs/heads/dev' }} + - name: Publish @latest + run: npm publish + if: ${{ github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..266563e4c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,118 @@ +name: Tests + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: + - dev + push: + branches: + - dev + - main +jobs: + run-tests: + name: All tests + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + env: + BROWSER: chrome + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Setup Nodejs + uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: All tests + run: npm run test + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + test-code-examples: + name: Check typedocs + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Setup Nodejs + uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build Docs + run: npm run build && npm run docs:json + - name: tsdoc @example checks + run: npm run test:examples + test-html-examples: + name: Run HTML Examples + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Setup Nodejs + uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Code example tests + run: npm run test:html + test-lint: + name: Linting and environment checks + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Setup Nodejs + uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Linting + run: npm run lint + test-readme: + name: Ensure that examples in the README compile + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Setup Nodejs + uses: actions/setup-node@v4 + with: + node-version: 18.12.0 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Test + run: npm run test:readme \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5827ab2f9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,92 +0,0 @@ -sudo: false -dist: bionic -language: node_js -node_js: -- '11' -install: -- npm install -- git config --global user.email "travis@travis-ci.org" -- git config --global user.name "Travis CI" -script: - - npm run test - - npm run codecov -before_deploy: - - npm run test:node - - npm run increment - - npm run docs -jobs: - include: - - stage: test - env : BROWSER=chrome - os: linux - addons: - chrome: stable - - stage: test - env : BROWSER=chrome - os: linux - addons: - chrome: beta - - stage: test - env : BROWSER=firefox - os: linux - addons: - firefox: latest - - stage: test - env : BROWSER=firefox - os: linux - addons: - firefox: latest-beta - - stage: test - script: - - npm run build - - npm run docs - - npm run test:examples - env : TEST_EXAMPLES=1 - - stage: test - script: - - npm run build - - npm run docs - - npm run test:examples - env : TEST_EXAMPLES=2 - - stage: test - script: - - npm run build - - npm run test:html - - npm run test:readme - env : TEST_HTML=1 - - stage: deploy - os: linux - script: npm run build - deploy: - - provider: npm - skip_cleanup: true - email: yotammann@gmail.com - api_key: $NPM_TOKEN - tag: next - on: - repo: Tonejs/Tone.js - branch: dev - # don't publish on cron or PRs - condition: $TRAVIS_EVENT_TYPE != cron && $TRAVIS_EVENT_TYPE != pull_request - # publish without @next when pushing on master - - provider: npm - skip_cleanup: true - email: yotammann@gmail.com - api_key: $NPM_TOKEN - on: - repo: Tonejs/Tone.js - branch: master - # don't publish on cron or PRs - condition: $TRAVIS_EVENT_TYPE != cron && $TRAVIS_EVENT_TYPE != pull_request - # publish build files for releases - - provider: releases - api-key: $GH_TOKEN - file_glob: true - file: build/* - skip_cleanup: true - on: - tags: true -# cache node_modules to speed up build -cache: - directories: - - node_modules diff --git a/README.md b/README.md index 6def938ff..657aadcc6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Tone.js ========= -[![Build Status](https://travis-ci.org/Tonejs/Tone.js.svg?branch=dev)](https://travis-ci.org/Tonejs/Tone.js) [![codecov](https://codecov.io/gh/Tonejs/Tone.js/branch/dev/graph/badge.svg)](https://codecov.io/gh/Tonejs/Tone.js) +[![codecov](https://codecov.io/gh/Tonejs/Tone.js/branch/dev/graph/badge.svg)](https://codecov.io/gh/Tonejs/Tone.js) Tone.js is a Web Audio framework for creating interactive music in the browser. The architecture of Tone.js aims to be familiar to both musicians and audio programmers creating web-based audio applications. On the high-level, Tone offers common DAW (digital audio workstation) features like a global transport for synchronizing and scheduling events as well as prebuilt synths and effects. Additionally, Tone provides high-performance building blocks to create your own synthesizers, effects, and complex control signals. @@ -12,22 +12,24 @@ Tone.js is a Web Audio framework for creating interactive music in the browser. # Installation -To install the latest stable version. +There are two ways to incorporate Tone.js into a project. First, it can be installed locally into a project using `npm`: ```bash -npm install tone +npm install tone // Install the latest stable version +npm install tone@next // Or, alternatively, use the 'next' version ``` -Or to install the 'next' version +Add Tone.js to a project using the JavaScript `import` syntax: -```bash -npm install tone@next +```js +import * as Tone from 'tone'; ``` -To import Tone.js: +Tone.js is also hosted at unpkg.com. It can be added directly within an HTML document, as long as it precedes any project scripts. [See the example here](https://github.com/Tonejs/Tone.js/blob/master/examples/simpleHtml.html) for more details. -```js -import * as Tone from 'tone' +```html + + ``` # Hello Tone @@ -253,7 +255,7 @@ Tone.js makes extensive use of the native Web Audio Nodes such as the GainNode a # Testing -Tone.js runs an extensive test suite using [mocha](https://mochajs.org/) and [chai](http://chaijs.com/) with nearly 100% coverage. Each commit and pull request is run on [Travis-CI](https://travis-ci.org/Tonejs/Tone.js/) across browsers and versions. Passing builds on the 'dev' branch are published on npm as `tone@next`. +Tone.js runs an extensive test suite using [mocha](https://mochajs.org/) and [chai](http://chaijs.com/) with nearly 100% coverage. Each commit and pull request is run on [Travis-CI](https://app.travis-ci.com/github/Tonejs/Tone.js) across browsers and versions. Passing builds on the 'dev' branch are published on npm as `tone@next`. # Contributing diff --git a/Tone/component/analysis/Analyser.ts b/Tone/component/analysis/Analyser.ts index 6b4c0b491..9718cb23b 100644 --- a/Tone/component/analysis/Analyser.ts +++ b/Tone/component/analysis/Analyser.ts @@ -79,6 +79,7 @@ export class Analyser extends ToneAudioNode { // set the values initially this.size = options.size; this.type = options.type; + this.smoothing = options.smoothing; } static getDefaults(): AnalyserOptions { diff --git a/Tone/component/analysis/DCMeter.ts b/Tone/component/analysis/DCMeter.ts index f2fad8d05..d36c9ed89 100644 --- a/Tone/component/analysis/DCMeter.ts +++ b/Tone/component/analysis/DCMeter.ts @@ -4,7 +4,7 @@ import { MeterBase, MeterBaseOptions } from "./MeterBase"; export type DCMeterOptions = MeterBaseOptions; /** - * DCMeter gets the raw value of the input signal at the current time. + * DCMeter gets the raw value of the input signal at the current time. See also {@link Meter}. * * @example * const meter = new Tone.DCMeter(); diff --git a/Tone/component/analysis/FFT.ts b/Tone/component/analysis/FFT.ts index 9d137a27e..3aca60d66 100644 --- a/Tone/component/analysis/FFT.ts +++ b/Tone/component/analysis/FFT.ts @@ -13,6 +13,7 @@ export interface FFTOptions extends MeterBaseOptions { /** * Get the current frequency data of the connected audio source using a fast Fourier transform. + * Read more about FFT algorithms on [Wikipedia] (https://en.wikipedia.org/wiki/Fast_Fourier_transform). * @category Component */ export class FFT extends MeterBase { diff --git a/Tone/component/analysis/Meter.test.ts b/Tone/component/analysis/Meter.test.ts index c272437d2..3b2398fb4 100644 --- a/Tone/component/analysis/Meter.test.ts +++ b/Tone/component/analysis/Meter.test.ts @@ -5,6 +5,8 @@ import { ONLINE_TESTING } from "test/helper/Supports"; import { Signal } from "Tone/signal/Signal"; import { Oscillator } from "Tone/source/oscillator/Oscillator"; import { Meter } from "./Meter"; +import { Panner } from "Tone/component/channel/Panner"; +import { Merge } from "Tone/component/channel/Merge"; describe("Meter", () => { @@ -30,7 +32,7 @@ describe("Meter", () => { it("returns an array of channels if channels > 1", () => { const meter = new Meter({ - channels: 4, + channelCount: 4, }); expect((meter.getValue() as number[]).length).to.equal(4); meter.dispose(); @@ -86,6 +88,29 @@ describe("Meter", () => { done(); }, 400); }); + + it("can get the rms levels for multiple channels", (done) => { + const meter = new Meter({ + channelCount: 2, + smoothing: 0.5, + }); + const merge = new Merge().connect(meter); + const osc0 = new Oscillator().connect(merge, 0, 0).start(); + const osc1 = new Oscillator().connect(merge, 0, 1).start(); + osc0.volume.value = -6; + osc1.volume.value = -18; + setTimeout(() => { + const values = meter.getValue(); + expect(values).to.have.lengthOf(2); + expect(values[0]).to.be.closeTo(-9, 1); + expect(values[1]).to.be.closeTo(-21, 1); + meter.dispose(); + merge.dispose(); + osc0.dispose(); + osc1.dispose(); + done(); + }, 400); + }); } }); }); diff --git a/Tone/component/analysis/Meter.ts b/Tone/component/analysis/Meter.ts index d90ceb7c9..2709c0744 100644 --- a/Tone/component/analysis/Meter.ts +++ b/Tone/component/analysis/Meter.ts @@ -8,12 +8,15 @@ import { Analyser } from "./Analyser"; export interface MeterOptions extends MeterBaseOptions { smoothing: NormalRange; normalRange: boolean; - channels: number; + channelCount: number; } /** * Meter gets the [RMS](https://en.wikipedia.org/wiki/Root_mean_square) * of an input signal. It can also get the raw value of the input signal. + * Setting `normalRange` to `true` will covert the output to a range of + * 0-1. See an example using a graphical display + * [here](https://tonejs.github.io/examples/meter). See also {@link DCMeter}. * * @example * const meter = new Tone.Meter(); @@ -42,9 +45,9 @@ export class Meter extends MeterBase { smoothing: number; /** - * The previous frame's value + * The previous frame's value for each channel. */ - private _rms = 0; + private _rms: number[]; /** * @param smoothing The amount of smoothing applied between frames. @@ -59,18 +62,20 @@ export class Meter extends MeterBase { context: this.context, size: 256, type: "waveform", - channels: options.channels, + channels: options.channelCount, }); this.smoothing = options.smoothing, this.normalRange = options.normalRange; + this._rms = new Array(options.channelCount); + this._rms.fill(0); } static getDefaults(): MeterOptions { return Object.assign(MeterBase.getDefaults(), { smoothing: 0.8, normalRange: false, - channels: 1, + channelCount: 1, }); } @@ -93,13 +98,13 @@ export class Meter extends MeterBase { getValue(): number | number[] { const aValues = this._analyser.getValue(); const channelValues = this.channels === 1 ? [aValues as Float32Array] : aValues as Float32Array[]; - const vals = channelValues.map(values => { + const vals = channelValues.map((values, index) => { const totalSquared = values.reduce((total, current) => total + current * current, 0); const rms = Math.sqrt(totalSquared / values.length); // the rms can only fall at the rate of the smoothing // but can jump up instantly - this._rms = Math.max(rms, this._rms * this.smoothing); - return this.normalRange ? this._rms : gainToDb(this._rms); + this._rms[index] = Math.max(rms, this._rms[index] * this.smoothing); + return this.normalRange ? this._rms[index] : gainToDb(this._rms[index]); }); if (this.channels === 1) { return vals[0]; diff --git a/Tone/component/channel/Channel.ts b/Tone/component/channel/Channel.ts index faa32cf94..b7741bf5d 100644 --- a/Tone/component/channel/Channel.ts +++ b/Tone/component/channel/Channel.ts @@ -91,7 +91,7 @@ export class Channel extends ToneAudioNode { } /** - * Solo/unsolo the channel. Soloing is only relative to other [[Channels]] and [[Solo]] instances + * Solo/unsolo the channel. Soloing is only relative to other [[Channel]]s and [[Solo]] instances */ get solo(): boolean { return this._solo.solo; diff --git a/Tone/component/channel/Recorder.ts b/Tone/component/channel/Recorder.ts index 2fd99f595..44ce02844 100644 --- a/Tone/component/channel/Recorder.ts +++ b/Tone/component/channel/Recorder.ts @@ -111,7 +111,7 @@ export class Recorder extends ToneAudioNode { */ async start() { assert(this.state !== "started", "Recorder is already started"); - const startPromise = new Promise(done => { + const startPromise = new Promise(done => { const handleStart = () => { this._recorder.removeEventListener("start", handleStart, false); diff --git a/Tone/component/dynamics/Limiter.ts b/Tone/component/dynamics/Limiter.ts index f67cf9d75..c9a9076b6 100644 --- a/Tone/component/dynamics/Limiter.ts +++ b/Tone/component/dynamics/Limiter.ts @@ -7,7 +7,7 @@ import { readOnly } from "../../core/util/Interface"; export interface LimiterOptions extends ToneAudioNodeOptions { threshold: Decibels; -}; +} /** * Limiter will limit the loudness of an incoming signal. diff --git a/Tone/component/envelope/Envelope.ts b/Tone/component/envelope/Envelope.ts index 94ca0ac33..f633c234a 100644 --- a/Tone/component/envelope/Envelope.ts +++ b/Tone/component/envelope/Envelope.ts @@ -141,7 +141,7 @@ export class Envelope extends ToneAudioNode { /** * The automation curve type for the decay */ - private _decayCurve!: BasicEnvelopeCurve; + private _decayCurve!: InternalEnvelopeCurve; /** * The automation curve type for the release @@ -322,12 +322,11 @@ export class Envelope extends ToneAudioNode { * env.triggerAttack(); * }, 1, 1); */ - get decayCurve(): BasicEnvelopeCurve { - return this._decayCurve; + get decayCurve(): EnvelopeCurve { + return this._getCurve(this._decayCurve, "Out"); } set decayCurve(curve) { - assert(["linear", "exponential"].some(c => c === curve), `Invalid envelope curve: ${curve}`); - this._decayCurve = curve; + this._setCurve("_decayCurve", "Out", curve); } /** @@ -475,7 +474,7 @@ export class Envelope extends ToneAudioNode { } /** - * Render the envelope curve to an array of the given length. + * Render the envelope curve to an array of the given length. * Good for visualizing the envelope curve. Rescales the duration of the * envelope to fit the length. */ @@ -605,8 +604,8 @@ const EnvelopeCurves: EnvelopeCurveMap = (() => { In: cosineCurve, Out: reverseCurve(cosineCurve), }, - exponential: "exponential" as "exponential", - linear: "linear" as "linear", + exponential: "exponential" as const, + linear: "linear" as const, ripple: { In: rippleCurve, Out: invertCurve(rippleCurve), diff --git a/Tone/component/filter/FeedbackCombFilter.test.ts b/Tone/component/filter/FeedbackCombFilter.test.ts index 7cb73993b..1dd94d709 100644 --- a/Tone/component/filter/FeedbackCombFilter.test.ts +++ b/Tone/component/filter/FeedbackCombFilter.test.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; import { FeedbackCombFilter } from "./FeedbackCombFilter"; +import { BitCrusher } from "Tone/effect/BitCrusher"; import { BasicTests } from "test/helper/Basic"; import { PassAudio } from "test/helper/PassAudio"; import { Offline } from "test/helper/Offline"; @@ -76,5 +77,20 @@ describe("FeedbackCombFilter", () => { }); }); }); + + it("should be usable with the BitCrusher", (done) => { + new FeedbackCombFilter(); + new BitCrusher(4); + + const handle = setTimeout(() => { + window.onunhandledrejection = null; + done(); + }, 100); + + window.onunhandledrejection = (event) => { + done(event.reason); + clearTimeout(handle); + }; + }); }); diff --git a/Tone/component/filter/FeedbackCombFilter.ts b/Tone/component/filter/FeedbackCombFilter.ts index c44022579..d7668ccbe 100644 --- a/Tone/component/filter/FeedbackCombFilter.ts +++ b/Tone/component/filter/FeedbackCombFilter.ts @@ -88,9 +88,9 @@ export class FeedbackCombFilter extends ToneAudioWorklet { expect(source.getTicksAtTime(6)).to.be.closeTo(6, 0.01); source.dispose(); }); + + it("will recompute memoized values when events are modified", () => { + const source = new TickSource(1); + source.start(3).pause(4); + expect(source.getTicksAtTime(1)).to.be.closeTo(0, 0.01); + expect(source.getTicksAtTime(2)).to.be.closeTo(0, 0.01); + expect(source.getTicksAtTime(3)).to.be.closeTo(0, 0.01); + expect(source.getTicksAtTime(4)).to.be.closeTo(1, 0.01); + expect(source.getTicksAtTime(5)).to.be.closeTo(1, 0.01); + source.start(1).pause(2); + expect(source.getTicksAtTime(1)).to.be.closeTo(0, 0.01); + expect(source.getTicksAtTime(2)).to.be.closeTo(1, 0.01); + expect(source.getTicksAtTime(3)).to.be.closeTo(1, 0.01); + expect(source.getTicksAtTime(4)).to.be.closeTo(2, 0.01); + expect(source.getTicksAtTime(5)).to.be.closeTo(2, 0.01); + source.setTicksAtTime(3, 1); + expect(source.getTicksAtTime(1)).to.be.closeTo(3, 0.01); + expect(source.getTicksAtTime(2)).to.be.closeTo(4, 0.01); + expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01); + expect(source.getTicksAtTime(4)).to.be.closeTo(5, 0.01); + expect(source.getTicksAtTime(5)).to.be.closeTo(5, 0.01); + source.cancel(4); + expect(source.getTicksAtTime(1)).to.be.closeTo(3, 0.01); + expect(source.getTicksAtTime(2)).to.be.closeTo(4, 0.01); + expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01); + expect(source.getTicksAtTime(4)).to.be.closeTo(5, 0.01); + expect(source.getTicksAtTime(5)).to.be.closeTo(6, 0.01); + source.dispose(); + }); }); context("forEachTickBetween", () => { @@ -591,6 +620,29 @@ describe("TickSource", () => { expect(source.getSecondsAtTime(1.5)).to.be.closeTo(0, 0.01); source.dispose(); }); + + it("will recompute memoized values when events are modified", () => { + const source = new TickSource(1); + source.start(3).pause(4); + expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01); + expect(source.getSecondsAtTime(2)).to.be.closeTo(0, 0.01); + expect(source.getSecondsAtTime(3)).to.be.closeTo(0, 0.01); + expect(source.getSecondsAtTime(4)).to.be.closeTo(1, 0.01); + expect(source.getSecondsAtTime(5)).to.be.closeTo(1, 0.01); + source.start(1).pause(2); + expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01); + expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.01); + expect(source.getSecondsAtTime(3)).to.be.closeTo(1, 0.01); + expect(source.getSecondsAtTime(4)).to.be.closeTo(2, 0.01); + expect(source.getSecondsAtTime(5)).to.be.closeTo(2, 0.01); + source.cancel(4); + expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01); + expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.01); + expect(source.getSecondsAtTime(3)).to.be.closeTo(1, 0.01); + expect(source.getSecondsAtTime(4)).to.be.closeTo(2, 0.01); + expect(source.getSecondsAtTime(5)).to.be.closeTo(3, 0.01); + source.dispose(); + }); }); context("Frequency", () => { diff --git a/Tone/core/clock/TickSource.ts b/Tone/core/clock/TickSource.ts index 119ae4b67..223f5e839 100644 --- a/Tone/core/clock/TickSource.ts +++ b/Tone/core/clock/TickSource.ts @@ -3,7 +3,7 @@ import { Seconds, Ticks, Time } from "../type/Units"; import { optionsFromArguments } from "../util/Defaults"; import { readOnly } from "../util/Interface"; import { PlaybackState, StateTimeline, StateTimelineEvent } from "../util/StateTimeline"; -import { Timeline } from "../util/Timeline"; +import { Timeline, TimelineEvent } from "../util/Timeline"; import { isDefined } from "../util/TypeCheck"; import { TickSignal } from "./TickSignal"; import { EQ } from "../util/Math"; @@ -13,12 +13,24 @@ interface TickSourceOptions extends ToneWithContextOptions { units: "bpm" | "hertz"; } -interface TickSourceOffsetEvent { +interface TickSourceOffsetEvent extends TimelineEvent { ticks: number; time: number; seconds: number; } +interface TickSourceTicksAtTimeEvent extends TimelineEvent { + state: PlaybackState; + time: number; + ticks: number; +} + +interface TickSourceSecondsAtTimeEvent extends TimelineEvent { + state: PlaybackState; + time: number; + seconds: number; +} + /** * Uses [TickSignal](TickSignal) to track elapsed ticks with complex automation curves. */ @@ -41,6 +53,16 @@ export class TickSource extends ToneWithContex */ private _tickOffset: Timeline = new Timeline(); + /** + * Memoized values of getTicksAtTime at events with state other than "started" + */ + private _ticksAtTime: Timeline = new Timeline(); + + /** + * Memoized values of getSecondsAtTime at events with state other than "started" + */ + private _secondsAtTime: Timeline = new Timeline(); + /** * @param frequency The initial frequency that the signal ticks at */ @@ -66,7 +88,7 @@ export class TickSource extends ToneWithContex static getDefaults(): TickSourceOptions { return Object.assign({ frequency: 1, - units: "hertz" as "hertz", + units: "hertz" as const, }, ToneWithContext.getDefaults()); } @@ -90,6 +112,8 @@ export class TickSource extends ToneWithContex if (isDefined(offset)) { this.setTicksAtTime(offset, computedTime); } + this._ticksAtTime.cancel(computedTime); + this._secondsAtTime.cancel(computedTime); } return this; } @@ -111,6 +135,8 @@ export class TickSource extends ToneWithContex this._state.cancel(computedTime); this._state.setStateAtTime("stopped", computedTime); this.setTicksAtTime(0, computedTime); + this._ticksAtTime.cancel(computedTime); + this._secondsAtTime.cancel(computedTime); return this; } @@ -122,6 +148,8 @@ export class TickSource extends ToneWithContex const computedTime = this.toSeconds(time); if (this._state.getValueAtTime(computedTime) === "started") { this._state.setStateAtTime("paused", computedTime); + this._ticksAtTime.cancel(computedTime); + this._secondsAtTime.cancel(computedTime); } return this; } @@ -134,6 +162,8 @@ export class TickSource extends ToneWithContex time = this.toSeconds(time); this._state.cancel(time); this._tickOffset.cancel(time); + this._ticksAtTime.cancel(time); + this._secondsAtTime.cancel(time); return this; } @@ -145,16 +175,21 @@ export class TickSource extends ToneWithContex getTicksAtTime(time?: Time): Ticks { const computedTime = this.toSeconds(time); const stopEvent = this._state.getLastState("stopped", computedTime) as StateTimelineEvent; + + // get previously memoized ticks if available + const memoizedEvent = this._ticksAtTime.get(computedTime); + // this event allows forEachBetween to iterate until the current time const tmpEvent: StateTimelineEvent = { state: "paused", time: computedTime }; this._state.add(tmpEvent); // keep track of the previous offset event - let lastState = stopEvent; - let elapsedTicks = 0; + let lastState = memoizedEvent ? memoizedEvent : stopEvent; + let elapsedTicks = memoizedEvent ? memoizedEvent.ticks : 0; + let eventToMemoize : TickSourceTicksAtTimeEvent | null = null; // iterate through all the events since the last stop - this._state.forEachBetween(stopEvent.time, computedTime + this.sampleTime, e => { + this._state.forEachBetween(lastState.time, computedTime + this.sampleTime, e => { let periodStartTime = lastState.time; // if there is an offset event in this period use that const offsetEvent = this._tickOffset.get(e.time); @@ -164,6 +199,10 @@ export class TickSource extends ToneWithContex } if (lastState.state === "started" && e.state !== "started") { elapsedTicks += this.frequency.getTicksAtTime(e.time) - this.frequency.getTicksAtTime(periodStartTime); + // do not memoize the temporary event + if (e.time !== tmpEvent.time) { + eventToMemoize = { state: e.state, time: e.time, ticks: elapsedTicks }; + } } lastState = e; }); @@ -171,6 +210,11 @@ export class TickSource extends ToneWithContex // remove the temporary event this._state.remove(tmpEvent); + // memoize the ticks at the most recent event with state other than "started" + if (eventToMemoize) { + this._ticksAtTime.add(eventToMemoize); + } + // return the ticks return elapsedTicks; } @@ -211,12 +255,16 @@ export class TickSource extends ToneWithContex const tmpEvent: StateTimelineEvent = { state: "paused", time }; this._state.add(tmpEvent); + // get previously memoized seconds if available + const memoizedEvent = this._secondsAtTime.get(time); + // keep track of the previous offset event - let lastState = stopEvent; - let elapsedSeconds = 0; + let lastState = memoizedEvent ? memoizedEvent : stopEvent; + let elapsedSeconds = memoizedEvent ? memoizedEvent.seconds : 0; + let eventToMemoize : TickSourceSecondsAtTimeEvent | null = null; // iterate through all the events since the last stop - this._state.forEachBetween(stopEvent.time, time + this.sampleTime, e => { + this._state.forEachBetween(lastState.time, time + this.sampleTime, e => { let periodStartTime = lastState.time; // if there is an offset event in this period use that const offsetEvent = this._tickOffset.get(e.time); @@ -226,6 +274,10 @@ export class TickSource extends ToneWithContex } if (lastState.state === "started" && e.state !== "started") { elapsedSeconds += e.time - periodStartTime; + // do not memoize the temporary event + if (e.time !== tmpEvent.time) { + eventToMemoize = { state: e.state, time: e.time, seconds: elapsedSeconds }; + } } lastState = e; }); @@ -233,7 +285,12 @@ export class TickSource extends ToneWithContex // remove the temporary event this._state.remove(tmpEvent); - // return the ticks + // memoize the seconds at the most recent event with state other than "started" + if (eventToMemoize) { + this._secondsAtTime.add(eventToMemoize); + } + + // return the seconds return elapsedSeconds; } @@ -250,6 +307,8 @@ export class TickSource extends ToneWithContex ticks, time, }); + this._ticksAtTime.cancel(time); + this._secondsAtTime.cancel(time); return this; } @@ -332,6 +391,8 @@ export class TickSource extends ToneWithContex super.dispose(); this._state.dispose(); this._tickOffset.dispose(); + this._ticksAtTime.dispose(); + this._secondsAtTime.dispose(); this.frequency.dispose(); return this; } diff --git a/Tone/core/clock/Ticker.ts b/Tone/core/clock/Ticker.ts index 0dc49baab..51b97410b 100644 --- a/Tone/core/clock/Ticker.ts +++ b/Tone/core/clock/Ticker.ts @@ -16,7 +16,12 @@ export class Ticker { /** * The update interval of the worker */ - private _updateInterval: Seconds; + private _updateInterval!: Seconds; + + /** + * The lowest allowable interval, preferably calculated from context sampleRate + */ + private _minimumUpdateInterval: Seconds; /** * The callback to invoke at regular intervals @@ -33,11 +38,12 @@ export class Ticker { */ private _worker!: Worker; - constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds) { + constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds, contextSampleRate?: number) { this._callback = callback; this._type = type; - this._updateInterval = updateInterval; + this._minimumUpdateInterval = Math.max(128/(contextSampleRate || 44100), .001); + this.updateInterval = updateInterval; // create the clock source for the first time this._createClock(); @@ -107,7 +113,6 @@ export class Ticker { private _disposeClock(): void { if (this._timeout) { clearTimeout(this._timeout); - this._timeout = 0; } if (this._worker) { this._worker.terminate(); @@ -122,9 +127,9 @@ export class Ticker { return this._updateInterval; } set updateInterval(interval: Seconds) { - this._updateInterval = Math.max(interval, 128 / 44100); + this._updateInterval = Math.max(interval, this._minimumUpdateInterval); if (this._type === "worker") { - this._worker.postMessage(Math.max(interval * 1000, 1)); + this._worker?.postMessage(this._updateInterval * 1000); } } diff --git a/Tone/core/clock/Transport.test.ts b/Tone/core/clock/Transport.test.ts index 1cf34542a..3c7039f9e 100644 --- a/Tone/core/clock/Transport.test.ts +++ b/Tone/core/clock/Transport.test.ts @@ -7,6 +7,8 @@ import { TransportTime } from "../type/TransportTime"; import { Transport } from "./Transport"; // importing for side affects import "../context/Destination"; +import { warns } from "test/helper/Basic"; +import { Synth } from "Tone/instrument/Synth"; describe("Transport", () => { @@ -546,6 +548,19 @@ describe("Transport", () => { }); }); + it("warns if the scheduled time was not used in the callback", async () => { + return Offline(({ transport }) => { + const synth = new Synth(); + transport.schedule(() => { + warns(() => { + synth.triggerAttackRelease("C2", 0.1); + }); + }, 0); + transport.start(0); + }, 0.3).then(() => { + }); + }); + }); context("scheduleRepeat", () => { @@ -636,7 +651,7 @@ describe("Transport", () => { invocations++; }, 0.1, 0); transport.start(); - }, 0.5).then(() => { + }, 0.51).then(() => { expect(invocations).to.equal(6); }); }); @@ -688,8 +703,8 @@ describe("Transport", () => { repeatCount++; }, 0.1, 0, 0.5); transport.start(); - }, 0.6).then(() => { - expect(repeatCount).to.equal(6); + }, 0.61).then(() => { + expect(repeatCount).to.equal(5); }); }); diff --git a/Tone/core/clock/Transport.ts b/Tone/core/clock/Transport.ts index b61ecfa83..cbd6fb0a7 100644 --- a/Tone/core/clock/Transport.ts +++ b/Tone/core/clock/Transport.ts @@ -1,16 +1,32 @@ import { TimeClass } from "../../core/type/Time"; import { PlaybackState } from "../../core/util/StateTimeline"; import { TimelineValue } from "../../core/util/TimelineValue"; +import { ToneAudioNode } from "../../core/context/ToneAudioNode"; +import { Pow } from "../../signal/Pow"; import { Signal } from "../../signal/Signal"; -import { onContextClose, onContextInit } from "../context/ContextInitialization"; +import { + onContextClose, + onContextInit, +} from "../context/ContextInitialization"; import { Gain } from "../context/Gain"; -import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext"; +import { + ToneWithContext, + ToneWithContextOptions, +} from "../context/ToneWithContext"; import { TicksClass } from "../type/Ticks"; import { TransportTimeClass } from "../type/TransportTime"; import { - BarsBeatsSixteenths, BPM, NormalRange, Seconds, - Subdivision, Ticks, Time, TimeSignature, TransportTime + BarsBeatsSixteenths, + BPM, + NormalRange, + Seconds, + Subdivision, + Ticks, + Time, + TimeSignature, + TransportTime, } from "../type/Units"; +import { enterScheduledCallback } from "../util/Debug"; import { optionsFromArguments } from "../util/Defaults"; import { Emitter } from "../util/Emitter"; import { readOnly, writable } from "../util/Interface"; @@ -32,12 +48,19 @@ interface TransportOptions extends ToneWithContextOptions { ppq: number; } -type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart"; +type TransportEventNames = + | "start" + | "stop" + | "pause" + | "loop" + | "loopEnd" + | "loopStart" + | "ticks"; interface SyncedSignalEvent { signal: Signal; initial: number; - ratio: Gain; + nodes: ToneAudioNode[]; } type TransportCallback = (time: Seconds) => void; @@ -64,8 +87,9 @@ type TransportCallback = (time: Seconds) => void; * Tone.Transport.start(); * @category Core */ -export class Transport extends ToneWithContext implements Emitter { - +export class Transport + extends ToneWithContext + implements Emitter { readonly name: string = "Transport"; //------------------------------------- @@ -163,9 +187,11 @@ export class Transport extends ToneWithContext implements Emit constructor(options?: Partial); constructor() { - super(optionsFromArguments(Transport.getDefaults(), arguments)); - const options = optionsFromArguments(Transport.getDefaults(), arguments); + const options = optionsFromArguments( + Transport.getDefaults(), + arguments + ); // CLOCK/TEMPO this._ppq = options.ppq; @@ -213,21 +239,34 @@ export class Transport extends ToneWithContext implements Emit this.emit("loopEnd", tickTime); this._clock.setTicksAtTime(this._loopStart, tickTime); ticks = this._loopStart; - this.emit("loopStart", tickTime, this._clock.getSecondsAtTime(tickTime)); + this.emit( + "loopStart", + tickTime, + this._clock.getSecondsAtTime(tickTime) + ); this.emit("loop", tickTime); } } // handle swing - if (this._swingAmount > 0 && + if ( + this._swingAmount > 0 && ticks % this._ppq !== 0 && // not on a downbeat - ticks % (this._swingTicks * 2) !== 0) { + ticks % (this._swingTicks * 2) !== 0 + ) { // add some swing - const progress = (ticks % (this._swingTicks * 2)) / (this._swingTicks * 2); - const amount = Math.sin((progress) * Math.PI) * this._swingAmount; - tickTime += new TicksClass(this.context, this._swingTicks * 2 / 3).toSeconds() * amount; + const progress = + (ticks % (this._swingTicks * 2)) / (this._swingTicks * 2); + const amount = Math.sin(progress * Math.PI) * this._swingAmount; + tickTime += + new TicksClass( + this.context, + (this._swingTicks * 2) / 3 + ).toSeconds() * amount; } // invoke the timeline events scheduled on this tick - this._timeline.forEachAtTime(ticks, event => event.invoke(tickTime)); + enterScheduledCallback(true); + this._timeline.forEachAtTime(ticks, (event) => event.invoke(tickTime)); + enterScheduledCallback(false); } //------------------------------------- @@ -246,7 +285,10 @@ export class Transport extends ToneWithContext implements Emit * console.log("measure 16!"); * }, "16:0:0"); */ - schedule(callback: TransportCallback, time: TransportTime | TransportTimeClass): number { + schedule( + callback: TransportCallback, + time: TransportTime | TransportTimeClass + ): number { const event = new TransportEvent(this, { callback, time: new TransportTimeClass(this.context, time).toTicks(), @@ -274,7 +316,7 @@ export class Transport extends ToneWithContext implements Emit callback: TransportCallback, interval: Time | TimeClass, startTime?: TransportTime | TransportTimeClass, - duration: Time = Infinity, + duration: Time = Infinity ): number { const event = new TransportRepeatEvent(this, { callback, @@ -293,7 +335,10 @@ export class Transport extends ToneWithContext implements Emit * @param time The time the callback should be invoked. * @returns The ID of the scheduled event. */ - scheduleOnce(callback: TransportCallback, time: TransportTime | TransportTimeClass): number { + scheduleOnce( + callback: TransportCallback, + time: TransportTime | TransportTimeClass + ): number { const event = new TransportEvent(this, { callback, once: true, @@ -338,8 +383,12 @@ export class Transport extends ToneWithContext implements Emit */ cancel(after: TransportTime = 0): this { const computedAfter = this.toTicks(after); - this._timeline.forEachFrom(computedAfter, event => this.clear(event.id)); - this._repeatedEvents.forEachFrom(computedAfter, event => this.clear(event.id)); + this._timeline.forEachFrom(computedAfter, (event) => + this.clear(event.id) + ); + this._repeatedEvents.forEachFrom(computedAfter, (event) => + this.clear(event.id) + ); return this; } @@ -381,6 +430,8 @@ export class Transport extends ToneWithContext implements Emit * Tone.Transport.start("+1", "4:0:0"); */ start(time?: Time, offset?: TransportTime): this { + // start the context + this.context.resume(); let offsetTicks; if (isDefined(offset)) { offsetTicks = this.toTicks(offset); @@ -486,7 +537,10 @@ export class Transport extends ToneWithContext implements Emit * Tone.Transport.setLoopPoints(0, "1m"); * Tone.Transport.loop = true; */ - setLoopPoints(startPosition: TransportTime, endPosition: TransportTime): this { + setLoopPoints( + startPosition: TransportTime, + endPosition: TransportTime + ): this { this.loopStart = startPosition; this.loopEnd = endPosition; return this; @@ -530,7 +584,7 @@ export class Transport extends ToneWithContext implements Emit } /** - * The Transport's position in seconds + * The Transport's position in seconds. * Setting the value will jump to that position right away. */ get seconds(): Seconds { @@ -544,20 +598,22 @@ export class Transport extends ToneWithContext implements Emit /** * The Transport's loop position as a normalized value. Always - * returns 0 if the transport if loop is not true. + * returns 0 if the Transport.loop = false. */ get progress(): NormalRange { if (this.loop) { const now = this.now(); const ticks = this._clock.getTicksAtTime(now); - return (ticks - this._loopStart) / (this._loopEnd - this._loopStart); + return ( + (ticks - this._loopStart) / (this._loopEnd - this._loopStart) + ); } else { return 0; } } /** - * The transports current tick position. + * The Transport's current tick position. */ get ticks(): Ticks { return this._clock.ticks; @@ -576,6 +632,7 @@ export class Transport extends ToneWithContext implements Emit // restart it with the new time this.emit("start", time, this._clock.getSecondsAtTime(time)); } else { + this.emit("ticks", now); this._clock.setTicksAtTime(t, now); } } @@ -587,7 +644,7 @@ export class Transport extends ToneWithContext implements Emit * @return The tick value at the given time. */ getTicksAtTime(time?: Time): Ticks { - return Math.round(this._clock.getTicksAtTime(time)); + return this._clock.getTicksAtTime(time); } /** @@ -625,7 +682,7 @@ export class Transport extends ToneWithContext implements Emit * @return The context time of the next subdivision. * @example * // the transport must be started, otherwise returns 0 - * Tone.Transport.start(); + * Tone.Transport.start(); * Tone.Transport.nextSubdivision("4n"); */ nextSubdivision(subdivision?: Time): Seconds { @@ -637,7 +694,7 @@ export class Transport extends ToneWithContext implements Emit const now = this.now(); // the remainder of the current ticks and the subdivision const transportPos = this.getTicksAtTime(now); - const remainingTicks = subdivision - transportPos % subdivision; + const remainingTicks = subdivision - (transportPos % subdivision); return this._clock.nextTickTime(remainingTicks, now); } } @@ -652,25 +709,45 @@ export class Transport extends ToneWithContext implements Emit * Otherwise it will be computed based on their current values. */ syncSignal(signal: Signal, ratio?: number): this { + const now = this.now(); + let source : TickParam<"bpm"> | ToneAudioNode = this.bpm; + let sourceValue = 1 / (60 / source.getValueAtTime(now) / this.PPQ); + let nodes : ToneAudioNode[] = []; + // If the signal is in the time domain, sync it to the reciprocal of + // the tempo instead of the tempo. + if (signal.units === "time") { + // The input to Pow should be in the range [1 / 4096, 1], where + // where 4096 is half of the buffer size of Pow's waveshaper. + // Pick a scaling factor based on the initial tempo that ensures + // that the initial input is in this range, while leaving room for + // tempo changes. + const scaleFactor = 1 / 64 / sourceValue; + const scaleBefore = new Gain(scaleFactor); + const reciprocal = new Pow(-1); + const scaleAfter = new Gain(scaleFactor); + // @ts-ignore + source.chain(scaleBefore, reciprocal, scaleAfter); + source = scaleAfter; + sourceValue = 1 / sourceValue; + nodes = [scaleBefore, reciprocal, scaleAfter]; + } if (!ratio) { // get the sync ratio - const now = this.now(); if (signal.getValueAtTime(now) !== 0) { - const bpm = this.bpm.getValueAtTime(now); - const computedFreq = 1 / (60 / bpm / this.PPQ); - ratio = signal.getValueAtTime(now) / computedFreq; + ratio = signal.getValueAtTime(now) / sourceValue; } else { ratio = 0; } } const ratioSignal = new Gain(ratio); // @ts-ignore - this.bpm.connect(ratioSignal); + source.connect(ratioSignal); // @ts-ignore ratioSignal.connect(signal._param); + nodes.push(ratioSignal); this._syncedSignals.push({ initial: signal.value, - ratio: ratioSignal, + nodes: nodes, signal, }); signal.value = 0; @@ -685,7 +762,7 @@ export class Transport extends ToneWithContext implements Emit for (let i = this._syncedSignals.length - 1; i >= 0; i--) { const syncedSignal = this._syncedSignals[i]; if (syncedSignal.signal === signal) { - syncedSignal.ratio.dispose(); + syncedSignal.nodes.forEach((node) => node.dispose()); syncedSignal.signal.value = syncedSignal.initial; this._syncedSignals.splice(i, 1); } @@ -709,9 +786,18 @@ export class Transport extends ToneWithContext implements Emit // EMITTER MIXIN TO SATISFY COMPILER //------------------------------------- - on!: (event: TransportEventNames, callback: (...args: any[]) => void) => this; - once!: (event: TransportEventNames, callback: (...args: any[]) => void) => this; - off!: (event: TransportEventNames, callback?: ((...args: any[]) => void) | undefined) => this; + on!: ( + event: TransportEventNames, + callback: (...args: any[]) => void + ) => this; + once!: ( + event: TransportEventNames, + callback: (...args: any[]) => void + ) => this; + off!: ( + event: TransportEventNames, + callback?: ((...args: any[]) => void) | undefined + ) => this; emit!: (event: any, ...args: any[]) => this; } @@ -721,10 +807,10 @@ Emitter.mixin(Transport); // INITIALIZATION //------------------------------------- -onContextInit(context => { +onContextInit((context) => { context.transport = new Transport({ context }); }); -onContextClose(context => { +onContextClose((context) => { context.transport.dispose(); }); diff --git a/Tone/core/clock/TransportEvent.ts b/Tone/core/clock/TransportEvent.ts index c0999c07d..acd019e87 100644 --- a/Tone/core/clock/TransportEvent.ts +++ b/Tone/core/clock/TransportEvent.ts @@ -41,6 +41,12 @@ export class TransportEvent { */ private _once: boolean; + /** + * The remaining value between the passed in time, and Math.floor(time). + * This value is later added back when scheduling to get sub-tick precision. + */ + protected _remainderTime = 0; + /** * @param transport The transport object which the event belongs to */ @@ -51,7 +57,8 @@ export class TransportEvent { this.transport = transport; this.callback = options.callback; this._once = options.once; - this.time = options.time; + this.time = Math.floor(options.time); + this._remainderTime = options.time - this.time; } static getDefaults(): TransportEventOptions { @@ -67,13 +74,21 @@ export class TransportEvent { */ private static _eventId = 0; + /** + * Get the time and remainder time. + */ + protected get floatTime(): number { + return this.time + this._remainderTime; + } + /** * Invoke the event callback. * @param time The AudioContext time in seconds of the event */ invoke(time: Seconds): void { if (this.callback) { - this.callback(time); + const tickDuration = this.transport.bpm.getDurationOfTicks(1, time); + this.callback(time + this._remainderTime * tickDuration); if (this._once) { this.transport.clear(this.id); } diff --git a/Tone/core/clock/TransportRepeatEvent.ts b/Tone/core/clock/TransportRepeatEvent.ts index d549c9ca7..b680d52c0 100644 --- a/Tone/core/clock/TransportRepeatEvent.ts +++ b/Tone/core/clock/TransportRepeatEvent.ts @@ -2,6 +2,7 @@ import { BaseContext } from "../context/BaseContext"; import { TicksClass } from "../type/Ticks"; import { Seconds, Ticks, Time } from "../type/Units"; import { TransportEvent, TransportEventOptions } from "./TransportEvent"; +import { GT, LT } from "../util/Math"; type Transport = import("../clock/Transport").Transport; @@ -60,11 +61,12 @@ export class TransportRepeatEvent extends TransportEvent { const options = Object.assign(TransportRepeatEvent.getDefaults(), opts); - this.duration = new TicksClass(transport.context, options.duration).valueOf(); - this._interval = new TicksClass(transport.context, options.interval).valueOf(); + this.duration = options.duration; + this._interval = options.interval; this._nextTick = options.time; this.transport.on("start", this._boundRestart); this.transport.on("loopStart", this._boundRestart); + this.transport.on("ticks", this._boundRestart); this.context = this.transport.context; this._restart(); } @@ -89,13 +91,25 @@ export class TransportRepeatEvent extends TransportEvent { super.invoke(time); } + /** + * Create an event on the transport on the nextTick + */ + private _createEvent(): number { + if (LT(this._nextTick, this.floatTime + this.duration)) { + return this.transport.scheduleOnce(this.invoke.bind(this), + new TicksClass(this.context, this._nextTick).toSeconds()); + } + return -1; + } + /** * Push more events onto the timeline to keep up with the position of the timeline */ private _createEvents(time: Seconds): void { // schedule the next event - const ticks = this.transport.getTicksAtTime(time); - if (ticks >= this.time && ticks >= this._nextTick && this._nextTick + this._interval < this.time + this.duration) { + // const ticks = this.transport.getTicksAtTime(time); + // if the next tick is within the bounds set by "duration" + if (LT(this._nextTick + this._interval, this.floatTime + this.duration)) { this._nextTick += this._interval; this._currentId = this._nextId; this._nextId = this.transport.scheduleOnce(this.invoke.bind(this), @@ -104,21 +118,21 @@ export class TransportRepeatEvent extends TransportEvent { } /** - * Push more events onto the timeline to keep up with the position of the timeline + * Re-compute the events when the transport time has changed from a start/ticks/loopStart event */ private _restart(time?: Time): void { this.transport.clear(this._currentId); this.transport.clear(this._nextId); - this._nextTick = this.time; + // start at the first event + this._nextTick = this.floatTime; const ticks = this.transport.getTicksAtTime(time); - if (ticks > this.time) { - this._nextTick = this.time + Math.ceil((ticks - this.time) / this._interval) * this._interval; + if (GT(ticks, this.time)) { + // the event is not being scheduled from the beginning and should be offset + this._nextTick = this.floatTime + Math.ceil((ticks - this.floatTime) / this._interval) * this._interval; } - this._currentId = this.transport.scheduleOnce(this.invoke.bind(this), - new TicksClass(this.context, this._nextTick).toSeconds()); + this._currentId = this._createEvent(); this._nextTick += this._interval; - this._nextId = this.transport.scheduleOnce(this.invoke.bind(this), - new TicksClass(this.context, this._nextTick).toSeconds()); + this._nextId = this._createEvent(); } /** @@ -130,6 +144,7 @@ export class TransportRepeatEvent extends TransportEvent { this.transport.clear(this._nextId); this.transport.off("start", this._boundRestart); this.transport.off("loopStart", this._boundRestart); + this.transport.off("ticks", this._boundRestart); return this; } } diff --git a/Tone/core/context/BaseContext.ts b/Tone/core/context/BaseContext.ts index 7587bb7c2..ba831424f 100644 --- a/Tone/core/context/BaseContext.ts +++ b/Tone/core/context/BaseContext.ts @@ -104,9 +104,8 @@ export abstract class BaseContext abstract get rawContext(): AnyAudioContext; - abstract async addAudioWorkletModule( - _url: string, - _name: string + abstract addAudioWorkletModule( + _url: string ): Promise; abstract lookAhead: number; diff --git a/Tone/core/context/Context.test.ts b/Tone/core/context/Context.test.ts index 4481cba7e..bf61d2189 100644 --- a/Tone/core/context/Context.test.ts +++ b/Tone/core/context/Context.test.ts @@ -75,8 +75,10 @@ describe("Context", () => { clockSource: "timeout", latencyHint: "playback", lookAhead: 0.2, + updateInterval: 0.1 }); expect(ctx.lookAhead).to.equal(0.2); + expect(ctx.updateInterval).to.equal(0.1); expect(ctx.latencyHint).to.equal("playback"); expect(ctx.clockSource).to.equal("timeout"); ctx.dispose(); @@ -116,10 +118,9 @@ describe("Context", () => { } }); await context.resume(); - await new Promise((done) => setTimeout(() => done(), 10)); + await new Promise((done) => setTimeout(() => done(), 10)); expect(triggerChange).to.equal(true); - context.dispose(); - return ac.close(); + return context.dispose(); }); }); diff --git a/Tone/core/context/Context.ts b/Tone/core/context/Context.ts index adbbfd518..dc3ecd017 100644 --- a/Tone/core/context/Context.ts +++ b/Tone/core/context/Context.ts @@ -3,7 +3,7 @@ import { Seconds } from "../type/Units"; import { isAudioContext } from "../util/AdvancedTypeCheck"; import { optionsFromArguments } from "../util/Defaults"; import { Timeline } from "../util/Timeline"; -import { isDefined, isString } from "../util/TypeCheck"; +import { isDefined } from "../util/TypeCheck"; import { AnyAudioContext, createAudioContext, @@ -39,13 +39,6 @@ export interface ContextTimeoutEvent { export class Context extends BaseContext { readonly name: string = "Context"; - /** - * The amount of time into the future events are scheduled. Giving Web Audio - * a short amount of time into the future to schedule events can reduce clicks and - * improve performance. This value can be set to 0 to get the lowest latency. - */ - lookAhead: Seconds; - /** * private reference to the BaseAudioContext */ @@ -101,6 +94,11 @@ export class Context extends BaseContext { */ private _initialized = false; + /** + * Private indicator if a close() has been called on the context, since close is async + */ + private _closeStarted = false; + /** * Indicates if the context is an OfflineAudioContext or an AudioContext */ @@ -116,16 +114,20 @@ export class Context extends BaseContext { if (options.context) { this._context = options.context; + // custom context provided, latencyHint unknown (unless explicitly provided in options) + this._latencyHint = arguments[0]?.latencyHint || ""; } else { this._context = createAudioContext({ latencyHint: options.latencyHint, }); + this._latencyHint = options.latencyHint; } this._ticker = new Ticker( this.emit.bind(this, "tick"), options.clockSource, - options.updateInterval + options.updateInterval, + this._context.sampleRate ); this.on("tick", this._timeoutLoop.bind(this)); @@ -133,9 +135,9 @@ export class Context extends BaseContext { this._context.onstatechange = () => { this.emit("statechange", this.state); }; - - this._setLatencyHint(options.latencyHint); - this.lookAhead = options.lookAhead; + + // if no custom updateInterval provided, updateInterval will be derived by lookAhead setter + this[arguments[0]?.hasOwnProperty("updateInterval") ? "_lookAhead" : "lookAhead"] = options.lookAhead; } static getDefaults(): ContextOptions { @@ -343,7 +345,7 @@ export class Context extends BaseContext { /** * Maps a module name to promise of the addModule method */ - private _workletModules: Map> = new Map(); + private _workletPromise: null | Promise = null; /** * Create an audio worklet node from a name and options. The module @@ -359,29 +361,23 @@ export class Context extends BaseContext { /** * Add an AudioWorkletProcessor module * @param url The url of the module - * @param name The name of the module */ - async addAudioWorkletModule(url: string, name: string): Promise { + async addAudioWorkletModule(url: string): Promise { assert( isDefined(this.rawContext.audioWorklet), "AudioWorkletNode is only available in a secure context (https or localhost)" ); - if (!this._workletModules.has(name)) { - this._workletModules.set( - name, - this.rawContext.audioWorklet.addModule(url) - ); + if (!this._workletPromise) { + this._workletPromise = this.rawContext.audioWorklet.addModule(url); } - await this._workletModules.get(name); + await this._workletPromise; } /** * Returns a promise which resolves when all of the worklets have been loaded on this context */ protected async workletsAreReady(): Promise { - const promises: Promise[] = []; - this._workletModules.forEach((promise) => promises.push(promise)); - await Promise.all(promises); + await this._workletPromise ? this._workletPromise : Promise.resolve(); } //--------------------------- @@ -391,8 +387,9 @@ export class Context extends BaseContext { /** * How often the interval callback is invoked. * This number corresponds to how responsive the scheduling - * can be. context.updateInterval + context.lookAhead gives you the - * total latency between scheduling an event and hearing it. + * can be. Setting to 0 will result in the lowest practial interval + * based on context properties. context.updateInterval + context.lookAhead + * gives you the total latency between scheduling an event and hearing it. */ get updateInterval(): Seconds { return this._ticker.updateInterval; @@ -412,6 +409,22 @@ export class Context extends BaseContext { this._ticker.type = type; } + /** + * The amount of time into the future events are scheduled. Giving Web Audio + * a short amount of time into the future to schedule events can reduce clicks and + * improve performance. This value can be set to 0 to get the lowest latency. + * Adjusting this value also affects the [[updateInterval]]. + */ + get lookAhead(): Seconds { + return this._lookAhead; + } + set lookAhead(time: Seconds) { + this._lookAhead = time; + // if lookAhead is 0, default to .01 updateInterval + this.updateInterval = time ? (time / 2) : .01; + } + private _lookAhead!: Seconds; + /** * The type of playback, which affects tradeoffs between audio * output latency and responsiveness. @@ -431,29 +444,6 @@ export class Context extends BaseContext { return this._latencyHint; } - /** - * Update the lookAhead and updateInterval based on the latencyHint - */ - private _setLatencyHint(hint: ContextLatencyHint | Seconds): void { - let lookAheadValue = 0; - this._latencyHint = hint; - if (isString(hint)) { - switch (hint) { - case "interactive": - lookAheadValue = 0.1; - break; - case "playback": - lookAheadValue = 0.5; - break; - case "balanced": - lookAheadValue = 0.25; - break; - } - } - this.lookAhead = lookAheadValue; - this.updateInterval = lookAheadValue / 2; - } - /** * The unwrapped AudioContext or OfflineAudioContext */ @@ -463,9 +453,13 @@ export class Context extends BaseContext { /** * The current audio context time plus a short [[lookAhead]]. + * @example + * setInterval(() => { + * console.log("now", Tone.now()); + * }, 100); */ now(): Seconds { - return this._context.currentTime + this.lookAhead; + return this._context.currentTime + this._lookAhead; } /** @@ -481,7 +475,7 @@ export class Context extends BaseContext { /** * Starts the audio context from a suspended state. This is required - * to initially start the AudioContext. See [[Tone.start]] + * to initially start the AudioContext. See [[start]] */ resume(): Promise { if (isAudioContext(this._context)) { @@ -496,7 +490,8 @@ export class Context extends BaseContext { * any AudioNodes created from the context will be silent. */ async close(): Promise { - if (isAudioContext(this._context)) { + if (isAudioContext(this._context) && (this.state !== "closed") && !this._closeStarted) { + this._closeStarted = true; await this._context.close(); } if (this._initialized) { @@ -541,6 +536,7 @@ export class Context extends BaseContext { Object.keys(this._constants).map((val) => this._constants[val].disconnect() ); + this.close(); return this; } diff --git a/Tone/core/context/ContextInitialization.ts b/Tone/core/context/ContextInitialization.ts index 435657a19..09cf4e255 100644 --- a/Tone/core/context/ContextInitialization.ts +++ b/Tone/core/context/ContextInitialization.ts @@ -25,7 +25,7 @@ export function initializeContext(ctx: Context): void { } /** - * Array of callbacks to invoke when a new context is created + * Array of callbacks to invoke when a new context is closed */ const notifyCloseContext: Array<(ctx: Context) => void> = []; @@ -37,6 +37,6 @@ export function onContextClose(cb: (ctx: Context) => void): void { } export function closeContext(ctx: Context): void { - // add any additional modules + // remove any additional modules notifyCloseContext.forEach(cb => cb(ctx)); } diff --git a/Tone/core/context/DummyContext.test.ts b/Tone/core/context/DummyContext.test.ts index a570d8289..75dc8e3c3 100644 --- a/Tone/core/context/DummyContext.test.ts +++ b/Tone/core/context/DummyContext.test.ts @@ -25,7 +25,7 @@ describe("DummyContext", () => { context.decodeAudioData(new Float32Array(100)); context.createAudioWorkletNode("test.js"); context.rawContext; - context.addAudioWorkletModule("test.js", "test"); + context.addAudioWorkletModule("test.js"); context.resume(); context.setTimeout(() => {}, 1); context.clearTimeout(1); diff --git a/Tone/core/context/DummyContext.ts b/Tone/core/context/DummyContext.ts index 9bf643e2b..3af541808 100644 --- a/Tone/core/context/DummyContext.ts +++ b/Tone/core/context/DummyContext.ts @@ -127,7 +127,7 @@ export class DummyContext extends BaseContext { return {} as AnyAudioContext; } - async addAudioWorkletModule(_url: string, _name: string): Promise { + async addAudioWorkletModule(_url: string): Promise { return Promise.resolve(); } diff --git a/Tone/core/context/ToneAudioBuffer.ts b/Tone/core/context/ToneAudioBuffer.ts index 237f4d005..a471167cb 100644 --- a/Tone/core/context/ToneAudioBuffer.ts +++ b/Tone/core/context/ToneAudioBuffer.ts @@ -1,7 +1,6 @@ import { getContext } from "../Global"; import { Tone } from "../Tone"; import { Samples, Seconds } from "../type/Units"; -import { isAudioBuffer } from "../util/AdvancedTypeCheck"; import { optionsFromArguments } from "../util/Defaults"; import { noOp } from "../util/Interface"; import { isArray, isNumber, isString } from "../util/TypeCheck"; @@ -25,7 +24,6 @@ interface ToneAudioBufferOptions { * @category Core */ export class ToneAudioBuffer extends Tone { - readonly name: string = "ToneAudioBuffer"; /** @@ -54,23 +52,26 @@ export class ToneAudioBuffer extends Tone { constructor( url?: string | ToneAudioBuffer | AudioBuffer, onload?: (buffer: ToneAudioBuffer) => void, - onerror?: (error: Error) => void, + onerror?: (error: Error) => void ); constructor(options?: Partial); constructor() { - super(); - const options = optionsFromArguments(ToneAudioBuffer.getDefaults(), arguments, ["url", "onload", "onerror"]); + const options = optionsFromArguments( + ToneAudioBuffer.getDefaults(), + arguments, + ["url", "onload", "onerror"] + ); this.reverse = options.reverse; this.onload = options.onload; - if (options.url && isAudioBuffer(options.url) || options.url instanceof ToneAudioBuffer) { - this.set(options.url); - } else if (isString(options.url)) { + if (isString(options.url)) { // initiate the download this.load(options.url).catch(options.onerror); + } else if (options.url) { + this.set(options.url); } } @@ -132,11 +133,13 @@ export class ToneAudioBuffer extends Tone { * @returns A Promise which resolves with this ToneAudioBuffer */ async load(url: string): Promise { - const doneLoading: Promise = ToneAudioBuffer.load(url).then(audioBuffer => { - this.set(audioBuffer); - // invoke the onload method - this.onload(this); - }); + const doneLoading: Promise = ToneAudioBuffer.load(url).then( + (audioBuffer) => { + this.set(audioBuffer); + // invoke the onload method + this.onload(this); + } + ); ToneAudioBuffer.downloads.push(doneLoading); try { await doneLoading; @@ -165,11 +168,15 @@ export class ToneAudioBuffer extends Tone { fromArray(array: Float32Array | Float32Array[]): this { const isMultidimensional = isArray(array) && array[0].length > 0; const channels = isMultidimensional ? array.length : 1; - const len = isMultidimensional ? (array[0] as Float32Array).length : array.length; + const len = isMultidimensional + ? (array[0] as Float32Array).length + : array.length; const context = getContext(); const buffer = context.createBuffer(channels, len, context.sampleRate); - const multiChannelArray: Float32Array[] = !isMultidimensional && channels === 1 ? - [array as Float32Array] : array as Float32Array[]; + const multiChannelArray: Float32Array[] = + !isMultidimensional && channels === 1 + ? [array as Float32Array] + : (array as Float32Array[]); for (let c = 0; c < channels; c++) { buffer.copyToChannel(multiChannelArray[c], c); @@ -195,7 +202,7 @@ export class ToneAudioBuffer extends Tone { } } // divide by the number of channels - outputArray = outputArray.map(sample => sample / numChannels); + outputArray = outputArray.map((sample) => sample / numChannels); this.fromArray(outputArray); } return this; @@ -240,13 +247,24 @@ export class ToneAudioBuffer extends Tone { * @param end The end time to slice. If none is given will default to the end of the buffer */ slice(start: Seconds, end: Seconds = this.duration): ToneAudioBuffer { + assert(this.loaded, "Buffer is not loaded"); const startSamples = Math.floor(start * this.sampleRate); const endSamples = Math.floor(end * this.sampleRate); - assert(startSamples < endSamples, "The start time must be less than the end time"); + assert( + startSamples < endSamples, + "The start time must be less than the end time" + ); const length = endSamples - startSamples; - const retBuffer = getContext().createBuffer(this.numberOfChannels, length, this.sampleRate); + const retBuffer = getContext().createBuffer( + this.numberOfChannels, + length, + this.sampleRate + ); for (let channel = 0; channel < this.numberOfChannels; channel++) { - retBuffer.copyToChannel(this.getChannelData(channel).subarray(startSamples, endSamples), channel); + retBuffer.copyToChannel( + this.getChannelData(channel).subarray(startSamples, endSamples), + channel + ); } return new ToneAudioBuffer(retBuffer); } @@ -332,7 +350,7 @@ export class ToneAudioBuffer extends Tone { * @return A ToneAudioBuffer created from the array */ static fromArray(array: Float32Array | Float32Array[]): ToneAudioBuffer { - return (new ToneAudioBuffer()).fromArray(array); + return new ToneAudioBuffer().fromArray(array); } /** @@ -354,7 +372,6 @@ export class ToneAudioBuffer extends Tone { * Loads a url using fetch and returns the AudioBuffer. */ static async load(url: string): Promise { - // test if the url contains multiple extensions const matches = url.match(/\[([^\]\[]+\|.+)\]$/); if (matches) { @@ -370,8 +387,21 @@ export class ToneAudioBuffer extends Tone { } // make sure there is a slash between the baseUrl and the url - const baseUrl = ToneAudioBuffer.baseUrl === "" || ToneAudioBuffer.baseUrl.endsWith("/") ? ToneAudioBuffer.baseUrl : ToneAudioBuffer.baseUrl + "/"; - const response = await fetch(baseUrl + url); + const baseUrl = + ToneAudioBuffer.baseUrl === "" || + ToneAudioBuffer.baseUrl.endsWith("/") + ? ToneAudioBuffer.baseUrl + : ToneAudioBuffer.baseUrl + "/"; + + // encode special characters in file path + const location = document.createElement("a"); + location.href = baseUrl + url; + location.pathname = (location.pathname + location.hash) + .split("/") + .map(encodeURIComponent) + .join("/"); + + const response = await fetch(location.href); if (!response.ok) { throw new Error(`could not load url: ${url}`); } @@ -394,7 +424,9 @@ export class ToneAudioBuffer extends Tone { static supportsType(url: string): boolean { const extensions = url.split("."); const extension = extensions[extensions.length - 1]; - const response = document.createElement("audio").canPlayType("audio/" + extension); + const response = document + .createElement("audio") + .canPlayType("audio/" + extension); return response !== ""; } diff --git a/Tone/core/context/ToneAudioBuffers.ts b/Tone/core/context/ToneAudioBuffers.ts index 44f86465a..dd44727ed 100644 --- a/Tone/core/context/ToneAudioBuffers.ts +++ b/Tone/core/context/ToneAudioBuffers.ts @@ -147,6 +147,10 @@ export class ToneAudioBuffers extends Tone { onerror: (e: Error) => void = noOp, ): this { if (isString(url)) { + // don't include the baseUrl if the url is a base64 encoded sound + if (this.baseUrl && url.trim().substring(0, 11).toLowerCase() === "data:audio/") { + this.baseUrl = ""; + } this._buffers.set(name.toString(), new ToneAudioBuffer(this.baseUrl + url, callback, onerror)); } else { this._buffers.set(name.toString(), new ToneAudioBuffer(url, callback, onerror)); diff --git a/Tone/core/context/ToneAudioNode.test.ts b/Tone/core/context/ToneAudioNode.test.ts index 02880753c..95cb66102 100644 --- a/Tone/core/context/ToneAudioNode.test.ts +++ b/Tone/core/context/ToneAudioNode.test.ts @@ -3,7 +3,7 @@ import { Merge } from "Tone/component"; import { Split } from "Tone/component/channel/Split"; import { Oscillator } from "Tone/source"; import { Gain } from "./Gain"; -import { connect, disconnect } from "./ToneAudioNode"; +import { connect, disconnect, fanIn } from "./ToneAudioNode"; import { PassAudio } from "test/helper/PassAudio"; import { Offline } from "test/helper/Offline"; @@ -218,6 +218,16 @@ describe("ToneAudioNode", () => { disconnect(input); }, false); }); + + it("can fan in multiple nodes to a destination", () => { + return PassAudio(input => { + const context = input.context; + const gain0 = context.createGain(); + const gain1 = context.createGain(); + const output = context.destination; + fanIn(gain0, gain1, input, output); + }); + }); it("can connect one channel to another", () => { return PassAudio(input => { @@ -320,6 +330,16 @@ describe("ToneAudioNode", () => { disconnect(gain, output); }); }); + + it("can fan in multiple nodes to a destination", async () => { + await Offline(() => { + const output = new Gain(); + const input0 = new Gain(); + const input1 = new Gain(); + const input2 = new Gain(); + fanIn(input0, input1, input2, output); + }); + }); }); }); diff --git a/Tone/core/context/ToneAudioNode.ts b/Tone/core/context/ToneAudioNode.ts index da7a1727e..6276dedd1 100644 --- a/Tone/core/context/ToneAudioNode.ts +++ b/Tone/core/context/ToneAudioNode.ts @@ -27,7 +27,7 @@ export abstract class ToneAudioNode connect(node, dstNode)); + } +} diff --git a/Tone/core/context/ToneWithContext.ts b/Tone/core/context/ToneWithContext.ts index 52ebc8ed0..c58e50f88 100644 --- a/Tone/core/context/ToneWithContext.ts +++ b/Tone/core/context/ToneWithContext.ts @@ -4,6 +4,7 @@ import { FrequencyClass } from "../type/Frequency"; import { TimeClass } from "../type/Time"; import { TransportTimeClass } from "../type/TransportTime"; import { Frequency, Hertz, Seconds, Ticks, Time } from "../type/Units"; +import { assertUsedScheduleTime } from "../util/Debug"; import { getDefaultsFromInstance, optionsFromArguments } from "../util/Defaults"; import { RecursivePartial } from "../util/Interface"; import { isArray, isBoolean, isDefined, isNumber, isString, isUndef } from "../util/TypeCheck"; @@ -96,7 +97,7 @@ export abstract class ToneWithContext ex /** * Convert the incoming time to seconds. - * This is calculated against the current [[Tone.Transport]] bpm + * This is calculated against the current [[Transport]] bpm * @example * const gain = new Tone.Gain(); * setInterval(() => console.log(gain.toSeconds("4n")), 100); @@ -104,6 +105,7 @@ export abstract class ToneWithContext ex * Tone.getTransport().bpm.rampTo(60, 30); */ toSeconds(time?: Time): Seconds { + assertUsedScheduleTime(time); return new TimeClass(this.context, time).toSeconds(); } diff --git a/Tone/core/type/Frequency.ts b/Tone/core/type/Frequency.ts index f29a0ec57..8d0fd85e3 100644 --- a/Tone/core/type/Frequency.ts +++ b/Tone/core/type/Frequency.ts @@ -1,3 +1,4 @@ +/* eslint-disable key-spacing */ import { getContext } from "../Global"; import { intervalToFrequencyRatio, mtof } from "./Conversions"; import { ftom, getA4, setA4 } from "./Conversions"; @@ -50,7 +51,7 @@ export class FrequencyClass extends TimeClass extends TimeClass extends } /** - * TransportTime is a the time along the Transport's - * timeline. It is similar to [[Time]], but instead of evaluating + * TransportTime is a time along the Transport's + * timeline. It is similar to Tone.Time, but instead of evaluating * against the AudioContext's clock, it is evaluated against * the Transport's position. See [TransportTime wiki](https://github.com/Tonejs/Tone.js/wiki/TransportTime). * @category Unit diff --git a/Tone/core/util/AdvancedTypeCheck.ts b/Tone/core/util/AdvancedTypeCheck.ts index cef9a9e3b..7f056e068 100644 --- a/Tone/core/util/AdvancedTypeCheck.ts +++ b/Tone/core/util/AdvancedTypeCheck.ts @@ -1,6 +1,7 @@ import { - isAnyAudioContext, isAnyAudioNode, - isAnyAudioParam, isAnyOfflineAudioContext, + AudioBuffer, isAnyAudioContext, + isAnyAudioNode, isAnyAudioParam, + isAnyOfflineAudioContext } from "standardized-audio-context"; /** diff --git a/Tone/core/util/Debug.ts b/Tone/core/util/Debug.ts index bc99a401c..31a37a0ef 100644 --- a/Tone/core/util/Debug.ts +++ b/Tone/core/util/Debug.ts @@ -1,9 +1,11 @@ +import { isUndef } from "./TypeCheck"; + /** * Assert that the statement is true, otherwise invoke the error. * @param statement * @param error The message which is passed into an Error */ -export function assert(statement: boolean, error: string): void { +export function assert(statement: boolean, error: string): asserts statement { if (!statement) { throw new Error(error); } @@ -14,17 +16,48 @@ export function assert(statement: boolean, error: string): void { */ export function assertRange(value: number, gte: number, lte = Infinity): void { if (!(gte <= value && value <= lte)) { - throw new RangeError(`Value must be within [${gte}, ${lte}], got: ${value}`); + throw new RangeError( + `Value must be within [${gte}, ${lte}], got: ${value}` + ); } } /** - * Make sure that the given value is within the range + * Warn if the context is not running. */ -export function assertContextRunning(context: import("../context/BaseContext").BaseContext): void { +export function assertContextRunning( + context: import("../context/BaseContext").BaseContext +): void { // add a warning if the context is not started if (!context.isOffline && context.state !== "running") { - warn("The AudioContext is \"suspended\". Invoke Tone.start() from a user action to start the audio."); + warn( + "The AudioContext is \"suspended\". Invoke Tone.start() from a user action to start the audio." + ); + } +} + +/** + * If it is currently inside a scheduled callback + */ +let isInsideScheduledCallback = false; +let printedScheduledWarning = false; + +/** + * Notify that the following block of code is occurring inside a Transport callback. + */ +export function enterScheduledCallback(insideCallback: boolean): void { + isInsideScheduledCallback = insideCallback; +} + +/** + * Make sure that a time was passed into + */ +export function assertUsedScheduleTime( + time?: import("../type/Units").Time +): void { + if (isUndef(time) && isInsideScheduledCallback && !printedScheduledWarning) { + printedScheduledWarning = true; + warn("Events scheduled inside of scheduled callbacks should use the passed in scheduling time. See https://github.com/Tonejs/Tone.js/wiki/Accurate-Timing"); } } diff --git a/Tone/core/util/Emitter.ts b/Tone/core/util/Emitter.ts index b28174770..69f089a77 100644 --- a/Tone/core/util/Emitter.ts +++ b/Tone/core/util/Emitter.ts @@ -69,11 +69,11 @@ export class Emitter extends Tone { if (isUndef(this._events)) { this._events = {}; } - if (this._events.hasOwnProperty(event)) { + if (this._events.hasOwnProperty(eventName)) { if (isUndef(callback)) { - this._events[event] = []; + this._events[eventName] = []; } else { - const eventList = this._events[event]; + const eventList = this._events[eventName]; for (let i = eventList.length - 1; i >= 0; i--) { if (eventList[i] === callback) { eventList.splice(i, 1); diff --git a/Tone/core/util/StateTimeline.ts b/Tone/core/util/StateTimeline.ts index 6cef05a14..21790cd0a 100644 --- a/Tone/core/util/StateTimeline.ts +++ b/Tone/core/util/StateTimeline.ts @@ -13,7 +13,7 @@ export interface StateTimelineEvent extends TimelineEvent { * A Timeline State. Provides the methods: `setStateAtTime("state", time)` and `getValueAtTime(time)` * @param initial The initial state of the StateTimeline. Defaults to `undefined` */ -export class StateTimeline extends Timeline { +export class StateTimeline = Record> extends Timeline { readonly name: string = "StateTimeline"; diff --git a/Tone/core/worklet/ToneAudioWorklet.ts b/Tone/core/worklet/ToneAudioWorklet.ts index 94a19bcc3..8e68f4ac9 100644 --- a/Tone/core/worklet/ToneAudioWorklet.ts +++ b/Tone/core/worklet/ToneAudioWorklet.ts @@ -53,7 +53,7 @@ export abstract class ToneAudioWorklet this._dummyParam = this._dummyGain.gain; // Register the processor - this.context.addAudioWorkletModule(blobUrl, name).then(() => { + this.context.addAudioWorkletModule(blobUrl).then(() => { // create the worklet when it's read if (!this.disposed) { this._worklet = this.context.createAudioWorkletNode(name, this.workletOptions); diff --git a/Tone/effect/AutoFilter.ts b/Tone/effect/AutoFilter.ts index d8a6f49ad..63765f757 100644 --- a/Tone/effect/AutoFilter.ts +++ b/Tone/effect/AutoFilter.ts @@ -64,7 +64,7 @@ export class AutoFilter extends LFOEffect { baseFrequency: 200, octaves: 2.6, filter: { - type: "lowpass" as "lowpass", + type: "lowpass" as const, rolloff: -12 as -12, Q: 1, } diff --git a/Tone/effect/BitCrusher.test.ts b/Tone/effect/BitCrusher.test.ts index 29789eae9..6e68b5051 100644 --- a/Tone/effect/BitCrusher.test.ts +++ b/Tone/effect/BitCrusher.test.ts @@ -1,4 +1,5 @@ import { BitCrusher } from "./BitCrusher"; +import { FeedbackCombFilter } from "Tone/component/filter/FeedbackCombFilter"; import { Oscillator } from "Tone/source/oscillator/Oscillator"; import { BasicTests } from "test/helper/Basic"; import { EffectTests } from "test/helper/EffectTests"; @@ -38,5 +39,20 @@ describe("BitCrusher", () => { crusher.dispose(); }); }); + + it("should be usable with the FeedbackCombFilter", (done) => { + new BitCrusher(4); + new FeedbackCombFilter(); + + const handle = setTimeout(() => { + window.onunhandledrejection = null; + done(); + }, 100); + + window.onunhandledrejection = (event) => { + done(event.reason); + clearTimeout(handle); + }; + }); }); diff --git a/Tone/effect/Chebyshev.test.ts b/Tone/effect/Chebyshev.test.ts index d6703c0e8..f3b1e7e45 100644 --- a/Tone/effect/Chebyshev.test.ts +++ b/Tone/effect/Chebyshev.test.ts @@ -36,6 +36,14 @@ describe("Chebyshev", () => { expect(cheby.get().order).to.equal(40); cheby.dispose(); }); + + it("throws an error if order is not an integer", () => { + const cheby = new Chebyshev(); + expect(() => { + cheby.order = 0.2; + }).to.throw(Error); + cheby.dispose(); + }); }); }); diff --git a/Tone/effect/Chebyshev.ts b/Tone/effect/Chebyshev.ts index 8bb693868..74b9a7f8a 100644 --- a/Tone/effect/Chebyshev.ts +++ b/Tone/effect/Chebyshev.ts @@ -2,6 +2,7 @@ import { Effect, EffectOptions } from "./Effect"; import { Positive } from "../core/type/Units"; import { optionsFromArguments } from "../core/util/Defaults"; import { WaveShaper } from "../signal/WaveShaper"; +import { assert } from "../core/util/Debug"; export interface ChebyshevOptions extends EffectOptions { order: Positive; @@ -60,7 +61,7 @@ export class Chebyshev extends Effect { static getDefaults(): ChebyshevOptions { return Object.assign(Effect.getDefaults(), { order: 1, - oversample: "none" as "none" + oversample: "none" as const }); } @@ -85,7 +86,7 @@ export class Chebyshev extends Effect { /** * The order of the Chebyshev polynomial which creates the equation which is applied to the incoming - * signal through a Tone.WaveShaper. The equations are in the form: + * signal through a Tone.WaveShaper. Must be an integer. The equations are in the form: * ``` * order 2: 2x^2 + 1 * order 3: 4x^3 + 3x @@ -97,6 +98,7 @@ export class Chebyshev extends Effect { return this._order; } set order(order) { + assert(Number.isInteger(order), "'order' must be an integer"); this._order = order; this._shaper.setMap((x => { return this._getCoefficient(x, order, new Map()); diff --git a/Tone/effect/Chorus.test.ts b/Tone/effect/Chorus.test.ts index 2d10aa321..dc3d649a8 100644 --- a/Tone/effect/Chorus.test.ts +++ b/Tone/effect/Chorus.test.ts @@ -7,25 +7,29 @@ import { Oscillator } from "Tone/source"; import { Offline } from "test/helper/Offline"; describe("Chorus", () => { - BasicTests(Chorus); EffectTests(Chorus); it("matches a file", () => { - return CompareToFile(() => { - const chorus = new Chorus().toDestination().start(); - const osc = new Oscillator(220, "sawtooth").connect(chorus).start(); - }, "chorus.wav", 0.1); + return CompareToFile( + () => { + const chorus = new Chorus().toDestination().start(); + const osc = new Oscillator(220, "sawtooth") + .connect(chorus) + .start(); + }, + "chorus.wav", + 0.25 + ); }); context("API", () => { - it("can pass in options in the constructor", () => { const chorus = new Chorus({ frequency: 2, delayTime: 1, depth: 0.4, - spread: 90 + spread: 90, }); expect(chorus.frequency.value).to.be.closeTo(2, 0.01); expect(chorus.delayTime).to.be.closeTo(1, 0.01); @@ -84,4 +88,3 @@ describe("Chorus", () => { }); }); }); - diff --git a/Tone/effect/Chorus.ts b/Tone/effect/Chorus.ts index a0a287a7f..d4d3e089b 100644 --- a/Tone/effect/Chorus.ts +++ b/Tone/effect/Chorus.ts @@ -17,15 +17,15 @@ export interface ChorusOptions extends StereoFeedbackEffectOptions { /** * Chorus is a stereo chorus effect composed of a left and right delay with an [[LFO]] applied to the delayTime of each channel. - * When [[feedback]] is set to a value larger than 0, you also get Flanger-type effects. + * When [[feedback]] is set to a value larger than 0, you also get Flanger-type effects. * Inspiration from [Tuna.js](https://github.com/Dinahmoe/tuna/blob/master/tuna.js). - * Read more on the chorus effect on [SoundOnSound](http://www.soundonsound.com/sos/jun04/articles/synthsecrets.htm). + * Read more on the chorus effect on [Sound On Sound](http://www.soundonsound.com/sos/jun04/articles/synthsecrets.htm). * * @example * const chorus = new Tone.Chorus(4, 2.5, 0.5).toDestination().start(); * const synth = new Tone.PolySynth().connect(chorus); * synth.triggerAttackRelease(["C3", "E3", "G3"], "8n"); - * + * * @category Effect */ export class Chorus extends StereoFeedbackEffect { @@ -118,7 +118,7 @@ export class Chorus extends StereoFeedbackEffect { frequency: 1.5, delayTime: 3.5, depth: 0.7, - type: "sine" as "sine", + type: "sine" as const, spread: 180, feedback: 0, wet: 0.5, diff --git a/Tone/effect/Distortion.test.ts b/Tone/effect/Distortion.test.ts index e6b89e61e..423169eee 100644 --- a/Tone/effect/Distortion.test.ts +++ b/Tone/effect/Distortion.test.ts @@ -16,7 +16,7 @@ describe("Distortion", () => { const osc = new Oscillator().connect(dist); osc.type = "square"; osc.start(0).stop(0.4); - }, "distortion.wav", 0.01); + }, "distortion.wav", 0.02); }); context("API", () => { diff --git a/Tone/effect/Distortion.ts b/Tone/effect/Distortion.ts index 210486886..3753fbf4d 100644 --- a/Tone/effect/Distortion.ts +++ b/Tone/effect/Distortion.ts @@ -10,7 +10,7 @@ export interface DistortionOptions extends EffectOptions { /** * A simple distortion effect using Tone.WaveShaper. * Algorithm from [this stackoverflow answer](http://stackoverflow.com/a/22313408). - * + * Read more about distortion on [Wikipedia] (https://en.wikipedia.org/wiki/Distortion_(music)). * @example * const dist = new Tone.Distortion(0.8).toDestination(); * const fm = new Tone.FMSynth().connect(dist); diff --git a/Tone/effect/MidSideEffect.ts b/Tone/effect/MidSideEffect.ts index 350f69e4f..776933631 100644 --- a/Tone/effect/MidSideEffect.ts +++ b/Tone/effect/MidSideEffect.ts @@ -68,14 +68,14 @@ export abstract class MidSideEffect extend /** * Connect the mid chain of the effect */ - protected connectEffectMid(...nodes: OutputNode[]): void{ + protected connectEffectMid(...nodes: OutputNode[]): void { this._midSend.chain(...nodes, this._midReturn); } /** * Connect the side chain of the effect */ - protected connectEffectSide(...nodes: OutputNode[]): void{ + protected connectEffectSide(...nodes: OutputNode[]): void { this._sideSend.chain(...nodes, this._sideReturn); } diff --git a/Tone/effect/StereoEffect.ts b/Tone/effect/StereoEffect.ts index 5f5de6f8a..91492246a 100644 --- a/Tone/effect/StereoEffect.ts +++ b/Tone/effect/StereoEffect.ts @@ -68,7 +68,7 @@ export class StereoEffect extends ToneAudio /** * Connect the left part of the effect */ - protected connectEffectLeft(...nodes: OutputNode[]): void{ + protected connectEffectLeft(...nodes: OutputNode[]): void { this._split.connect(nodes[0], 0, 0); connectSeries(...nodes); connect(nodes[nodes.length-1], this._merge, 0, 0); @@ -77,7 +77,7 @@ export class StereoEffect extends ToneAudio /** * Connect the right part of the effect */ - protected connectEffectRight(...nodes: OutputNode[]): void{ + protected connectEffectRight(...nodes: OutputNode[]): void { this._split.connect(nodes[0], 1, 0); connectSeries(...nodes); connect(nodes[nodes.length-1], this._merge, 0, 1); diff --git a/Tone/effect/Tremolo.ts b/Tone/effect/Tremolo.ts index d9315981f..ba750f75b 100644 --- a/Tone/effect/Tremolo.ts +++ b/Tone/effect/Tremolo.ts @@ -111,7 +111,7 @@ export class Tremolo extends StereoEffect { static getDefaults(): TremoloOptions { return Object.assign(StereoEffect.getDefaults(), { frequency: 10, - type: "sine" as "sine", + type: "sine" as const, depth: 0.5, spread: 180, }); diff --git a/Tone/effect/Vibrato.ts b/Tone/effect/Vibrato.ts index 13df1754f..2cb35bfc4 100644 --- a/Tone/effect/Vibrato.ts +++ b/Tone/effect/Vibrato.ts @@ -79,7 +79,7 @@ export class Vibrato extends Effect { maxDelay: 0.005, frequency: 5, depth: 0.1, - type: "sine" as "sine" + type: "sine" as const }); } diff --git a/Tone/event/Loop.test.ts b/Tone/event/Loop.test.ts index b63e8e2d0..525d56876 100644 --- a/Tone/event/Loop.test.ts +++ b/Tone/event/Loop.test.ts @@ -235,7 +235,7 @@ describe("Loop", () => { } }).start(0); transport.start(); - }, 0.8).then(() => { + }, 0.81).then(() => { expect(callCount).to.equal(9); }); }); @@ -326,7 +326,7 @@ describe("Loop", () => { loop.playbackRate = 1.5; expect(loop.playbackRate).to.equal(1.5); transport.start(); - }, 0.8).then(() => { + }, 0.81).then(() => { expect(callCount).to.equal(13); }); }); diff --git a/Tone/event/Loop.ts b/Tone/event/Loop.ts index fa31948c4..fb18d69cc 100644 --- a/Tone/event/Loop.ts +++ b/Tone/event/Loop.ts @@ -58,7 +58,8 @@ export class Loop extends ToneWithCon loop: true, loopEnd: options.interval, playbackRate: options.playbackRate, - probability: options.probability + probability: options.probability, + humanize: options.humanize, }); this.callback = options.callback; diff --git a/Tone/event/Part.test.ts b/Tone/event/Part.test.ts index edc465f8b..de0fd6015 100644 --- a/Tone/event/Part.test.ts +++ b/Tone/event/Part.test.ts @@ -575,6 +575,25 @@ describe("Part", () => { }); }); + it("can loop a specific number of times (different set order)", () => { + let callCount = 0; + const times = [0.1, 0.2, 0.4, 0.5]; + return Offline(({ transport }) => { + const part = new Part({ + events: [0, 0.1], + callback(time): void { + expect(times[callCount]).to.be.closeTo(time, 0.01); + callCount++; + }, + }).start(0.1); + part.loop = 2; + part.loopEnd = 0.3; + transport.start(); + }, 0.8).then(() => { + expect(callCount).to.equal(4); + }); + }); + it("plays once when loop is 1", () => { let callCount = 0; return Offline(({ transport }) => { diff --git a/Tone/event/Part.ts b/Tone/event/Part.ts index 20d4d0851..0033fe0eb 100644 --- a/Tone/event/Part.ts +++ b/Tone/event/Part.ts @@ -27,7 +27,7 @@ interface PartOptions extends Omit>, "value" * // the notes given as the second element in the array * // will be passed in as the second argument * synth.triggerAttackRelease(note, "8n", time); - * }), [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]]); + * }), [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]]).start(0); * Tone.Transport.start(); * @example * const synth = new Tone.Synth().toDestination(); diff --git a/Tone/event/Pattern.test.ts b/Tone/event/Pattern.test.ts index 2fd77115c..9375fa26e 100644 --- a/Tone/event/Pattern.test.ts +++ b/Tone/event/Pattern.test.ts @@ -89,19 +89,21 @@ describe("Pattern", () => { it("is invoked after it's started", () => { let invoked = false; return Offline(({ transport }) => { + const values = ["a", "b", "c"]; let index = 0; const pattern = new Pattern((() => { invoked = true; - expect(pattern.value).to.equal(index); + expect(pattern.value).to.equal(values[index]); + expect(pattern.index).to.equal(index); index++; - }), [0, 1, 2]).start(0); + }), values).start(0); transport.start(); }, 0.2).then(() => { expect(invoked).to.be.true; }); }); - it("passes in the scheduled time and pattern index to the callback", () => { + it("passes in the scheduled time and pattern note to the callback", () => { let invoked = false; return Offline(({ transport }) => { const startTime = 0.05; @@ -109,6 +111,8 @@ describe("Pattern", () => { expect(time).to.be.a("number"); expect(time - startTime).to.be.closeTo(0.3, 0.01); expect(note).to.be.equal("a"); + expect(pattern.value).to.equal("a"); + expect(pattern.index).to.be.equal(0); invoked = true; }), ["a"], "up"); transport.start(startTime); @@ -121,16 +125,38 @@ describe("Pattern", () => { it("passes in the next note of the pattern", () => { let counter = 0; return Offline(({ transport }) => { + const values = ["a", "b", "c"]; const pattern = new Pattern(((time, note) => { - expect(note).to.equal(counter % 3); + expect(note).to.equal(values[counter % 3]); + expect(pattern.value).to.equal(values[counter % 3]); + expect(pattern.index).to.be.equal(counter % 3); counter++; - }), [0, 1, 2], "up").start(0); + }), values, "up").start(0); pattern.interval = "16n"; transport.start(0); }, 0.7).then(() => { expect(counter).to.equal(6); }); }); + + it("can modify the pattern type and values", () => { + let counter = 0; + return Offline(({ transport }) => { + const values = ["a", "b", "c"]; + const pattern = new Pattern(((time, note) => { + expect(note).to.equal(values[counter % 3]); + expect(pattern.value).to.equal(values[counter % 3]); + expect(pattern.index).to.be.equal(counter % 3); + counter++; + }), ["a"], "down").start(0); + pattern.interval = "16n"; + pattern.pattern = "up"; + pattern.values = values; + transport.start(0); + }, 0.7).then(() => { + expect(counter).to.equal(6); + }); + }); }); }); diff --git a/Tone/event/Pattern.ts b/Tone/event/Pattern.ts index fbb61ab30..0f2e0f1cf 100644 --- a/Tone/event/Pattern.ts +++ b/Tone/event/Pattern.ts @@ -27,12 +27,17 @@ export class Pattern extends Loop> { /** * The pattern generator function */ - private _pattern: Iterator; + private _pattern: Iterator; + + /** + * The current index + */ + private _index?: number; /** * The current value */ - private _value?: ValueType; + private _value?: ValueType; /** * Hold the pattern type @@ -67,13 +72,13 @@ export class Pattern extends Loop> { this.callback = options.callback; this._values = options.values; - this._pattern = PatternGenerator(options.values, options.pattern); + this._pattern = PatternGenerator(options.values.length, options.pattern); this._type = options.pattern; } static getDefaults(): PatternOptions { return Object.assign(Loop.getDefaults(), { - pattern: "up" as "up", + pattern: "up" as const, values: [], callback: noOp, }); @@ -83,8 +88,9 @@ export class Pattern extends Loop> { * Internal function called when the notes should be called */ protected _tick(time: Seconds): void { - const value = this._pattern.next() as IteratorResult; - this._value = value.value; + const index = this._pattern.next() as IteratorResult; + this._index = index.value; + this._value = this._values[index.value]; this.callback(time, this._value); } @@ -107,6 +113,13 @@ export class Pattern extends Loop> { return this._value; } + /** + * The current index of the pattern. + */ + get index(): number | undefined { + return this._index; + } + /** * The pattern type. See Tone.CtrlPattern for the full list of patterns. */ @@ -115,7 +128,7 @@ export class Pattern extends Loop> { } set pattern(pattern) { this._type = pattern; - this._pattern = PatternGenerator(this._values, this._type); + this._pattern = PatternGenerator(this._values.length, this._type); } } diff --git a/Tone/event/PatternGenerator.test.ts b/Tone/event/PatternGenerator.test.ts index 841778e78..81b2acf17 100644 --- a/Tone/event/PatternGenerator.test.ts +++ b/Tone/event/PatternGenerator.test.ts @@ -13,22 +13,14 @@ describe("PatternGenerator", () => { context("API", () => { - it("can be constructed with an array and type", () => { - const pattern = PatternGenerator([0, 1, 2, 3], "down"); + it("can be constructed with an number and type", () => { + const pattern = PatternGenerator(4, "down"); expect(getArrayValues(pattern, 10)).to.deep.equal([3, 2, 1, 0, 3, 2, 1, 0, 3, 2]); }); - it("can be resized smaller when the index is after the previous length", () => { - const values = [0, 1, 2, 3, 4]; - const pattern = PatternGenerator(values); - expect(pattern.next().value).to.equal(0); - values.shift(); - expect(pattern.next().value).to.equal(2); - }); - - it("throws an error with an empty array", () => { + it("throws an error with a number less than 1", () => { expect(() => { - const pattern = PatternGenerator([]); + const pattern = PatternGenerator(0); pattern.next(); }).to.throw(Error); }); @@ -37,54 +29,54 @@ describe("PatternGenerator", () => { context("Patterns", () => { it("does the up pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3], "up"); + const pattern = PatternGenerator(4, "up"); expect(getArrayValues(pattern, 6)).to.deep.equal([0, 1, 2, 3, 0, 1]); }); it("does the down pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3], "down"); + const pattern = PatternGenerator(4, "down"); expect(getArrayValues(pattern, 6)).to.deep.equal([3, 2, 1, 0, 3, 2]); }); it("does the upDown pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3], "upDown"); + const pattern = PatternGenerator(4, "upDown"); expect(getArrayValues(pattern, 10)).to.deep.equal([0, 1, 2, 3, 2, 1, 0, 1, 2, 3]); }); it("does the downUp pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3], "downUp"); + const pattern = PatternGenerator(4, "downUp"); expect(getArrayValues(pattern, 10)).to.deep.equal([3, 2, 1, 0, 1, 2, 3, 2, 1, 0]); }); it("does the alternateUp pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3, 4], "alternateUp"); + const pattern = PatternGenerator(5, "alternateUp"); expect(getArrayValues(pattern, 10)).to.deep.equal([0, 2, 1, 3, 2, 4, 3, 0, 2, 1]); }); it("does the alternateDown pattern", () => { - const pattern = PatternGenerator([0, 1, 2, 3, 4], "alternateDown"); + const pattern = PatternGenerator(5, "alternateDown"); expect(getArrayValues(pattern, 10)).to.deep.equal([4, 2, 3, 1, 2, 0, 1, 4, 2, 3]); }); it("outputs random elements from the values", () => { - const values = [0, 1, 2, 3, 4]; - const pattern = PatternGenerator(values, "random"); + const numValues = 5; + const pattern = PatternGenerator(numValues, "random"); for (let i = 0; i < 10; i++) { - expect(values.indexOf(pattern.next().value)).to.not.equal(-1); + expect(pattern.next().value).to.be.at.least(0).and.at.most(numValues - 1); } }); it("does randomOnce pattern", () => { - const pattern = PatternGenerator([4, 5, 6, 7, 8], "randomOnce"); - expect(getArrayValues(pattern, 10).sort()).to.deep.equal([4, 4, 5, 5, 6, 6, 7, 7, 8, 8]); + const pattern = PatternGenerator(5, "randomOnce"); + expect(getArrayValues(pattern, 10).sort()).to.deep.equal([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]); }); it("randomly walks up or down 1 step without repeating", () => { const values = [0, 1, 2, 3, 4]; - const pattern = PatternGenerator(values, "randomWalk"); - let currentIndex = values.indexOf(pattern.next().value); + const pattern = PatternGenerator(5, "randomWalk"); + let currentIndex = pattern.next().value; for (let i = 0; i < 10; i++) { - const nextIndex = values.indexOf(pattern.next().value); + const nextIndex = pattern.next().value; expect(currentIndex).to.not.equal(nextIndex); // change always equals 1 expect(Math.abs(currentIndex - nextIndex)).to.equal(1); diff --git a/Tone/event/PatternGenerator.ts b/Tone/event/PatternGenerator.ts index 8fcc3547f..9aecb92ca 100644 --- a/Tone/event/PatternGenerator.ts +++ b/Tone/event/PatternGenerator.ts @@ -9,11 +9,11 @@ export type PatternName = "up" | "down" | "upDown" | "downUp" | "alternateUp" | /** * Start at the first value and go up to the last */ -function* upPatternGen(values: T[]): IterableIterator { +function* upPatternGen(numValues: number): IterableIterator { let index = 0; - while (index < values.length) { - index = clampToArraySize(index, values); - yield values[index]; + while (index < numValues) { + index = clamp(index, 0, numValues - 1); + yield index; index++; } } @@ -21,11 +21,11 @@ function* upPatternGen(values: T[]): IterableIterator { /** * Start at the last value and go down to 0 */ -function* downPatternGen(values: T[]): IterableIterator { - let index = values.length - 1; +function* downPatternGen(numValues: number): IterableIterator { + let index = numValues - 1; while (index >= 0) { - index = clampToArraySize(index, values); - yield values[index]; + index = clamp(index, 0, numValues - 1); + yield index; index--; } } @@ -33,30 +33,23 @@ function* downPatternGen(values: T[]): IterableIterator { /** * Infinitely yield the generator */ -function* infiniteGen(values: T[], gen: typeof upPatternGen): IterableIterator { +function* infiniteGen(numValues: number, gen: typeof upPatternGen): IterableIterator { while (true) { - yield* gen(values); + yield* gen(numValues); } } -/** - * Make sure that the index is in the given range - */ -function clampToArraySize(index: number, values: any[]): number { - return clamp(index, 0, values.length - 1); -} - /** * Alternate between two generators */ -function* alternatingGenerator(values: T[], directionUp: boolean): IterableIterator { - let index = directionUp ? 0 : values.length - 1; +function* alternatingGenerator(numValues: number, directionUp: boolean): IterableIterator { + let index = directionUp ? 0 : numValues - 1; while (true) { - index = clampToArraySize(index, values); - yield values[index]; + index = clamp(index, 0, numValues - 1); + yield index; if (directionUp) { index++; - if (index >= values.length - 1) { + if (index >= numValues - 1) { directionUp = false; } } else { @@ -71,12 +64,12 @@ function* alternatingGenerator(values: T[], directionUp: boolean): IterableIt /** * Starting from the bottom move up 2, down 1 */ -function* jumpUp(values: T[]): IterableIterator { +function* jumpUp(numValues: number): IterableIterator { let index = 0; let stepIndex = 0; - while (index < values.length) { - index = clampToArraySize(index, values); - yield values[index]; + while (index < numValues) { + index = clamp(index, 0, numValues - 1); + yield index; stepIndex++; index += (stepIndex % 2 ? 2 : -1); } @@ -85,12 +78,12 @@ function* jumpUp(values: T[]): IterableIterator { /** * Starting from the top move down 2, up 1 */ -function* jumpDown(values: T[]): IterableIterator { - let index = values.length - 1; +function* jumpDown(numValues: number): IterableIterator { + let index = numValues - 1; let stepIndex = 0; while (index >= 0) { - index = clampToArraySize(index, values); - yield values[index]; + index = clamp(index, 0, numValues - 1); + yield index; stepIndex++; index += (stepIndex % 2 ? -2 : 1); } @@ -99,78 +92,78 @@ function* jumpDown(values: T[]): IterableIterator { /** * Choose a random index each time */ -function* randomGen(values: T[]): IterableIterator { +function* randomGen(numValues: number): IterableIterator { while (true) { - const randomIndex = Math.floor(Math.random() * values.length); - yield values[randomIndex]; + const randomIndex = Math.floor(Math.random() * numValues); + yield randomIndex; } } /** * Randomly go through all of the values once before choosing a new random order */ -function* randomOnce(values: T[]): IterableIterator { +function* randomOnce(numValues: number): IterableIterator { // create an array of indices const copy: number[] = []; - for (let i = 0; i < values.length; i++) { + for (let i = 0; i < numValues; i++) { copy.push(i); } while (copy.length > 0) { // random choose an index, and then remove it so it's not chosen again const randVal = copy.splice(Math.floor(copy.length * Math.random()), 1); - const index = clampToArraySize(randVal[0], values); - yield values[index]; + const index = clamp(randVal[0], 0, numValues - 1); + yield index; } } /** - * Randomly choose to walk up or down 1 index in the values array + * Randomly choose to walk up or down 1 index */ -function* randomWalk(values: T[]): IterableIterator { - // randomly choose a starting index in the values array - let index = Math.floor(Math.random() * values.length); +function* randomWalk(numValues: number): IterableIterator { + // randomly choose a starting index + let index = Math.floor(Math.random() * numValues); while (true) { if (index === 0) { - index++; // at bottom of array, so force upward step - } else if (index === values.length - 1) { - index--; // at top of array, so force downward step + index++; // at bottom, so force upward step + } else if (index === numValues - 1) { + index--; // at top, so force downward step } else if (Math.random() < 0.5) { // else choose random downward or upward step index--; } else { index++; } - yield values[index]; + yield index; } } /** - * PatternGenerator returns a generator which will iterate over the given array - * of values and yield the items according to the passed in pattern - * @param values An array of values to iterate over + * PatternGenerator returns a generator which will yield numbers between 0 and numValues + * according to the passed in pattern that can be used as indexes into an array of size numValues. + * @param numValues The size of the array to emit indexes for * @param pattern The name of the pattern use when iterating over * @param index Where to start in the offset of the values array */ -export function* PatternGenerator(values: T[], pattern: PatternName = "up", index = 0): Iterator { +export function* PatternGenerator(numValues: number, pattern: PatternName = "up", index = 0): Iterator { // safeguards - assert(values.length > 0, "The array must have more than one value in it"); + assert(numValues >= 1, "The number of values must be at least one"); switch (pattern) { case "up" : - yield* infiniteGen(values, upPatternGen); + yield* infiniteGen(numValues, upPatternGen); case "down" : - yield* infiniteGen(values, downPatternGen); + yield* infiniteGen(numValues, downPatternGen); case "upDown" : - yield* alternatingGenerator(values, true); + yield* alternatingGenerator(numValues, true); case "downUp" : - yield* alternatingGenerator(values, false); + yield* alternatingGenerator(numValues, false); case "alternateUp": - yield* infiniteGen(values, jumpUp); + yield* infiniteGen(numValues, jumpUp); case "alternateDown": - yield* infiniteGen(values, jumpDown); + yield* infiniteGen(numValues, jumpDown); case "random": - yield* randomGen(values); + yield* randomGen(numValues); case "randomOnce": - yield* infiniteGen(values, randomOnce); + yield* infiniteGen(numValues, randomOnce); case "randomWalk": - yield* randomWalk(values); + yield* randomWalk(numValues); } } diff --git a/Tone/event/Sequence.test.ts b/Tone/event/Sequence.test.ts index b3195149a..21d8e0707 100644 --- a/Tone/event/Sequence.test.ts +++ b/Tone/event/Sequence.test.ts @@ -299,6 +299,17 @@ describe("Sequence", () => { }); }); + it("can mute the callback", () => { + return Offline(({ transport }) => { + const seq = new Sequence(() => { + throw new Error("shouldn't call this callback"); + }, [0, 0.1, 0.2, 0.3]).start(); + seq.mute = true; + expect(seq.mute).to.be.true; + transport.start(); + }, 0.5); + }); + }); context("Looping", () => { diff --git a/Tone/event/Sequence.ts b/Tone/event/Sequence.ts index a06c77a40..011244a93 100644 --- a/Tone/event/Sequence.ts +++ b/Tone/event/Sequence.ts @@ -20,7 +20,7 @@ interface SequenceOptions extends Omit, "value"> { * in an array of events which will be spaced at the * given subdivision. Sub-arrays will subdivide that beat * by the number of items are in the array. - * Sequence notation inspiration from [Tidal](http://yaxu.org/tidal/) + * Sequence notation inspiration from [Tidal Cycles](http://tidalcycles.org/) * @example * const synth = new Tone.Synth().toDestination(); * const seq = new Tone.Sequence((time, note) => { @@ -102,7 +102,7 @@ export class Sequence extends ToneEvent { * The internal callback for when an event is invoked */ private _seqCallback(time: Seconds, value: any): void { - if (value !== null) { + if (value !== null && !this.mute) { this.callback(time, value); } } diff --git a/Tone/event/ToneEvent.test.ts b/Tone/event/ToneEvent.test.ts index 607d0d70c..573f9fd62 100644 --- a/Tone/event/ToneEvent.test.ts +++ b/Tone/event/ToneEvent.test.ts @@ -355,11 +355,11 @@ describe("ToneEvent", () => { it("can be started and stopped multiple times", () => { return Offline(({ transport }) => { - const eventTimes = [0.3, 0.4, 0.9, 1.0, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]; + const eventTimes = [0.3, 0.39, 0.9, 0.99, 1.3, 1.39, 1.48, 1.57, 1.66, 1.75, 1.84]; let eventTimeIndex = 0; new ToneEvent({ loop: true, - loopEnd: 0.1, + loopEnd: 0.09, callback(time): void { expect(eventTimes.length).to.be.gt(eventTimeIndex); expect(eventTimes[eventTimeIndex]).to.be.closeTo(time, 0.05); diff --git a/Tone/event/ToneEvent.ts b/Tone/event/ToneEvent.ts index f78ac5947..4c299c0ee 100644 --- a/Tone/event/ToneEvent.ts +++ b/Tone/event/ToneEvent.ts @@ -167,8 +167,6 @@ export class ToneEvent extends ToneWithContext extends ToneWithContext extends ToneWithContext extends ToneWithContext new type(context, ...args); + return (...args: unknown[]) => new type(context, ...args); } /** * Return an object with all of the classes bound to the passed in context * @param context The context to bind all of the nodes to */ -export function fromContext(context: Context): Tone { +export function fromContext(context: Context): ToneObject { const classesWithContext: Partial = {}; Object.keys(omitFromObject(Classes, ["Transport", "Destination", "Draw"])).map(key => { @@ -56,7 +56,7 @@ export function fromContext(context: Context): Tone { } }); - const toneFromContext: Tone = { + const toneFromContext: ToneObject = { ...(classesWithContext as ClassesWithoutSingletons), now: context.now.bind(context), immediate: context.immediate.bind(context), diff --git a/Tone/instrument/DuoSynth.ts b/Tone/instrument/DuoSynth.ts index f0ec39d94..09284b515 100644 --- a/Tone/instrument/DuoSynth.ts +++ b/Tone/instrument/DuoSynth.ts @@ -18,7 +18,7 @@ export interface DuoSynthOptions extends MonophonicOptions { } /** - * DuoSynth is a monophonic synth composed of two [[MonoSynths]] run in parallel with control over the + * DuoSynth is a monophonic synth composed of two [[MonoSynth]]s run in parallel with control over the * frequency ratio between the two voices and vibrato effect. * @example * const duoSynth = new Tone.DuoSynth().toDestination(); @@ -177,7 +177,7 @@ export class DuoSynth extends Monophonic { release: 0.5 } }), - }) as DuoSynthOptions; + }) as unknown as DuoSynthOptions; } /** * Trigger the attack portion of the note diff --git a/Tone/instrument/Instrument.ts b/Tone/instrument/Instrument.ts index a7e20f1ae..def4a98f5 100644 --- a/Tone/instrument/Instrument.ts +++ b/Tone/instrument/Instrument.ts @@ -83,6 +83,10 @@ export abstract class Instrument extends Tone if (this._syncState()) { this._syncMethod("triggerAttack", 1); this._syncMethod("triggerRelease", 0); + + this.context.transport.on("stop", this._syncedRelease); + this.context.transport.on("pause", this._syncedRelease); + this.context.transport.on("loopEnd", this._syncedRelease); } return this; } @@ -126,6 +130,10 @@ export abstract class Instrument extends Tone this._synced = false; this.triggerAttack = this._original_triggerAttack; this.triggerRelease = this._original_triggerRelease; + + this.context.transport.off("stop", this._syncedRelease); + this.context.transport.off("pause", this._syncedRelease); + this.context.transport.off("loopEnd", this._syncedRelease); } return this; } @@ -153,8 +161,8 @@ export abstract class Instrument extends Tone /** * Start the instrument's note. * @param note the note to trigger - * @param time the time to trigger the ntoe - * @param velocity the velocity to trigger the note (betwee 0-1) + * @param time the time to trigger the note + * @param velocity the velocity to trigger the note (between 0-1) */ abstract triggerAttack(note: Frequency, time?: Time, velocity?: NormalRange): this; private _original_triggerAttack = this.triggerAttack; @@ -166,6 +174,11 @@ export abstract class Instrument extends Tone abstract triggerRelease(...args: any[]): this; private _original_triggerRelease = this.triggerRelease; + /** + * The release which is scheduled to the timeline. + */ + protected _syncedRelease = (time: number) => this._original_triggerRelease(time); + /** * clean up * @returns {Instrument} this diff --git a/Tone/instrument/MetalSynth.ts b/Tone/instrument/MetalSynth.ts index 4538cf5c7..f15974b3c 100644 --- a/Tone/instrument/MetalSynth.ts +++ b/Tone/instrument/MetalSynth.ts @@ -1,9 +1,22 @@ import { Envelope, EnvelopeOptions } from "../component/envelope/Envelope"; import { Filter } from "../component/filter/Filter"; import { Gain } from "../core/context/Gain"; -import { ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode"; -import { Frequency, NormalRange, Positive, Seconds, Time } from "../core/type/Units"; -import { deepMerge, omitFromObject, optionsFromArguments } from "../core/util/Defaults"; +import { + ToneAudioNode, + ToneAudioNodeOptions, +} from "../core/context/ToneAudioNode"; +import { + Frequency, + NormalRange, + Positive, + Seconds, + Time, +} from "../core/type/Units"; +import { + deepMerge, + omitFromObject, + optionsFromArguments, +} from "../core/util/Defaults"; import { noOp, RecursivePartial } from "../core/util/Interface"; import { Multiply } from "../signal/Multiply"; import { Scale } from "../signal/Scale"; @@ -23,17 +36,15 @@ export interface MetalSynthOptions extends MonophonicOptions { * Inharmonic ratio of frequencies based on the Roland TR-808 * Taken from https://ccrma.stanford.edu/papers/tr-808-cymbal-physically-informed-circuit-bendable-digital-model */ -const inharmRatios: number[] = [1.0, 1.483, 1.932, 2.546, 2.630, 3.897]; +const inharmRatios: number[] = [1.0, 1.483, 1.932, 2.546, 2.63, 3.897]; /** * A highly inharmonic and spectrally complex source with a highpass filter * and amplitude envelope which is good for making metallophone sounds. * Based on CymbalSynth by [@polyrhythmatic](https://github.com/polyrhythmatic). - * Inspiration from [Sound on Sound](https://shorturl.at/rSZ12). * @category Instrument */ export class MetalSynth extends Monophonic { - readonly name: string = "MetalSynth"; /** @@ -84,10 +95,13 @@ export class MetalSynth extends Monophonic { */ readonly envelope: Envelope; - constructor(options?: RecursivePartial) + constructor(options?: RecursivePartial); constructor() { super(optionsFromArguments(MetalSynth.getDefaults(), arguments)); - const options = optionsFromArguments(MetalSynth.getDefaults(), arguments); + const options = optionsFromArguments( + MetalSynth.getDefaults(), + arguments + ); this.detune = new Signal({ context: this.context, @@ -158,12 +172,15 @@ export class MetalSynth extends Monophonic { static getDefaults(): MetalSynthOptions { return deepMerge(Monophonic.getDefaults(), { envelope: Object.assign( - omitFromObject(Envelope.getDefaults(), Object.keys(ToneAudioNode.getDefaults())), + omitFromObject( + Envelope.getDefaults(), + Object.keys(ToneAudioNode.getDefaults()) + ), { attack: 0.001, decay: 1.4, release: 0.2, - }, + } ), harmonicity: 5.1, modulationIndex: 32, @@ -177,24 +194,33 @@ export class MetalSynth extends Monophonic { * @param time When the attack should be triggered. * @param velocity The velocity that the envelope should be triggered at. */ - protected _triggerEnvelopeAttack(time: Seconds, velocity: NormalRange = 1): this { + protected _triggerEnvelopeAttack( + time: Seconds, + velocity: NormalRange = 1 + ): this { this.envelope.triggerAttack(time, velocity); - this._oscillators.forEach(osc => osc.start(time)); + this._oscillators.forEach((osc) => osc.start(time)); if (this.envelope.sustain === 0) { - this._oscillators.forEach(osc => { - osc.stop(time + this.toSeconds(this.envelope.attack) + this.toSeconds(this.envelope.decay)); + this._oscillators.forEach((osc) => { + osc.stop( + time + + this.toSeconds(this.envelope.attack) + + this.toSeconds(this.envelope.decay) + ); }); } return this; } - + /** * Trigger the release of the envelope. * @param time When the release should be triggered. */ protected _triggerEnvelopeRelease(time: Seconds): this { this.envelope.triggerRelease(time); - this._oscillators.forEach(osc => osc.stop(time + this.toSeconds(this.envelope.release))); + this._oscillators.forEach((osc) => + osc.stop(time + this.toSeconds(this.envelope.release)) + ); return this; } @@ -213,7 +239,7 @@ export class MetalSynth extends Monophonic { return this._oscillators[0].modulationIndex.value; } set modulationIndex(val) { - this._oscillators.forEach(osc => (osc.modulationIndex.value = val)); + this._oscillators.forEach((osc) => (osc.modulationIndex.value = val)); } /** @@ -226,7 +252,7 @@ export class MetalSynth extends Monophonic { return this._oscillators[0].harmonicity.value; } set harmonicity(val) { - this._oscillators.forEach(osc => (osc.harmonicity.value = val)); + this._oscillators.forEach((osc) => (osc.harmonicity.value = val)); } /** @@ -254,13 +280,14 @@ export class MetalSynth extends Monophonic { } set octaves(val) { this._octaves = val; - this._filterFreqScaler.max = this._filterFreqScaler.min * Math.pow(2, val); + this._filterFreqScaler.max = + this._filterFreqScaler.min * Math.pow(2, val); } dispose(): this { super.dispose(); - this._oscillators.forEach(osc => osc.dispose()); - this._freqMultipliers.forEach(freqMult => freqMult.dispose()); + this._oscillators.forEach((osc) => osc.dispose()); + this._freqMultipliers.forEach((freqMult) => freqMult.dispose()); this.frequency.dispose(); this.detune.dispose(); this._filterFreqScaler.dispose(); diff --git a/Tone/instrument/Monophonic.ts b/Tone/instrument/Monophonic.ts index fddb4057a..9ac574090 100644 --- a/Tone/instrument/Monophonic.ts +++ b/Tone/instrument/Monophonic.ts @@ -62,7 +62,7 @@ export abstract class Monophonic extends Inst * Trigger the attack of the note optionally with a given velocity. * @param note The note to trigger. * @param time When the note should start. - * @param velocity The velocity scaler determines how "loud" the note will be triggered. + * @param velocity The velocity determines how "loud" the note will be. * @example * const synth = new Tone.Synth().toDestination(); * // trigger the note a half second from now at half velocity @@ -77,8 +77,8 @@ export abstract class Monophonic extends Inst } /** - * Trigger the release portion of the envelope - * @param time If no time is given, the release happens immediatly + * Trigger the release portion of the envelope. + * @param time If no time is given, the release happens immediately. * @example * const synth = new Tone.Synth().toDestination(); * synth.triggerAttack("C4"); diff --git a/Tone/instrument/NoiseSynth.ts b/Tone/instrument/NoiseSynth.ts index 38781e059..785241cb8 100644 --- a/Tone/instrument/NoiseSynth.ts +++ b/Tone/instrument/NoiseSynth.ts @@ -4,7 +4,10 @@ import { omitFromObject, optionsFromArguments } from "../core/util/Defaults"; import { RecursivePartial } from "../core/util/Interface"; import { Noise, NoiseOptions } from "../source/Noise"; import { Instrument, InstrumentOptions } from "./Instrument"; -import { ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode"; +import { + ToneAudioNode, + ToneAudioNodeOptions, +} from "../core/context/ToneAudioNode"; import { Envelope, EnvelopeOptions } from "../component/envelope/Envelope"; import { Source } from "../source/Source"; @@ -14,7 +17,7 @@ export interface NoiseSynthOptions extends InstrumentOptions { } /** - * Tone.NoiseSynth is composed of [[Noise]] through an [[AmplitudeEnvelope]]. + * Tone.NoiseSynth is composed of [[Noise]] through an [[AmplitudeEnvelope]]. * ``` * +-------+ +-------------------+ * | Noise +>--> AmplitudeEnvelope +>--> Output @@ -26,7 +29,6 @@ export interface NoiseSynthOptions extends InstrumentOptions { * @category Instrument */ export class NoiseSynth extends Instrument { - readonly name = "NoiseSynth"; /** @@ -39,17 +41,30 @@ export class NoiseSynth extends Instrument { */ readonly envelope: AmplitudeEnvelope; - constructor(options?: RecursivePartial) + constructor(options?: RecursivePartial); constructor() { super(optionsFromArguments(NoiseSynth.getDefaults(), arguments)); - const options = optionsFromArguments(NoiseSynth.getDefaults(), arguments); - this.noise = new Noise(Object.assign({ - context: this.context, - }, options.noise)); + const options = optionsFromArguments( + NoiseSynth.getDefaults(), + arguments + ); + this.noise = new Noise( + Object.assign( + { + context: this.context, + }, + options.noise + ) + ); - this.envelope = new AmplitudeEnvelope(Object.assign({ - context: this.context, - }, options.envelope)); + this.envelope = new AmplitudeEnvelope( + Object.assign( + { + context: this.context, + }, + options.envelope + ) + ); // connect the noise to the output this.noise.chain(this.envelope, this.output); @@ -58,17 +73,23 @@ export class NoiseSynth extends Instrument { static getDefaults(): NoiseSynthOptions { return Object.assign(Instrument.getDefaults(), { envelope: Object.assign( - omitFromObject(Envelope.getDefaults(), Object.keys(ToneAudioNode.getDefaults())), + omitFromObject( + Envelope.getDefaults(), + Object.keys(ToneAudioNode.getDefaults()) + ), { decay: 0.1, sustain: 0.0, - }, + } ), noise: Object.assign( - omitFromObject(Noise.getDefaults(), Object.keys(Source.getDefaults())), + omitFromObject( + Noise.getDefaults(), + Object.keys(Source.getDefaults()) + ), { type: "white", - }, + } ), }); } @@ -87,7 +108,11 @@ export class NoiseSynth extends Instrument { // start the noise this.noise.start(time); if (this.envelope.sustain === 0) { - this.noise.stop(time + this.toSeconds(this.envelope.attack) + this.toSeconds(this.envelope.decay)); + this.noise.stop( + time + + this.toSeconds(this.envelope.attack) + + this.toSeconds(this.envelope.decay) + ); } return this; } @@ -110,7 +135,21 @@ export class NoiseSynth extends Instrument { return this; } - triggerAttackRelease(duration: Time, time?: Time, velocity: NormalRange = 1): this { + /** + * Trigger the attack and then the release after the duration. + * @param duration The amount of time to hold the note for + * @param time The time the note should start + * @param velocity The volume of the note (0-1) + * @example + * const noiseSynth = new Tone.NoiseSynth().toDestination(); + * // hold the note for 0.5 seconds + * noiseSynth.triggerAttackRelease(0.5); + */ + triggerAttackRelease( + duration: Time, + time?: Time, + velocity: NormalRange = 1 + ): this { time = this.toSeconds(time); duration = this.toSeconds(duration); this.triggerAttack(time, velocity); diff --git a/Tone/instrument/PluckSynth.ts b/Tone/instrument/PluckSynth.ts index 71b01f449..1ab13f416 100644 --- a/Tone/instrument/PluckSynth.ts +++ b/Tone/instrument/PluckSynth.ts @@ -14,7 +14,7 @@ export interface PluckSynthOptions extends InstrumentOptions { } /** - * Karplus-String string synthesis. + * Karplus-Strong string synthesis. * @example * const plucky = new Tone.PluckSynth().toDestination(); * plucky.triggerAttack("C4", "+0.5"); diff --git a/Tone/instrument/PolySynth.test.ts b/Tone/instrument/PolySynth.test.ts index f4861c147..d3d022ecd 100644 --- a/Tone/instrument/PolySynth.test.ts +++ b/Tone/instrument/PolySynth.test.ts @@ -121,20 +121,17 @@ describe("PolySynth", () => { }); }); - it("can be synced to the transport", () => { - return Offline(({ transport }) => { - const polySynth = new PolySynth(Synth, { - envelope: { - release: 0.1, - }, - }).sync(); + it("can stop all sounds scheduled to start in the future when disposed", () => { + return Offline(() => { + const polySynth = new PolySynth(); + polySynth.set({ envelope: { release: 0.1 } }); polySynth.toDestination(); - polySynth.triggerAttackRelease("C4", 0.1, 0.1); - polySynth.triggerAttackRelease("E4", 0.1, 0.3); - transport.start(0.1); - }, 0.8).then((buffer) => { - expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.2, 0.01); - expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.6, 0.01); + polySynth.triggerAttackRelease(["C4", "E4", "G4", "B4"], 0.2); + return atTime(0.1, () => { + polySynth.dispose(); + }); + }, 0.3).then((buffer) => { + expect(buffer.isSilent()).to.be.true; }); }); @@ -252,6 +249,86 @@ describe("PolySynth", () => { }); + context("Transport sync", () => { + it("can be synced to the transport", () => { + return Offline(({ transport }) => { + const polySynth = new PolySynth(Synth, { + envelope: { + release: 0.1, + }, + }).sync(); + polySynth.toDestination(); + polySynth.triggerAttackRelease("C4", 0.1, 0.1); + polySynth.triggerAttackRelease("E4", 0.1, 0.3); + transport.start(0.1); + }, 0.8).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.2, 0.01); + expect(buffer.getTimeOfLastSound()).to.be.closeTo(0.6, 0.01); + }); + }); + + it("is silent until the transport is started", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5); + }, 1).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).is.closeTo(0.5, 0.1); + }); + }); + + it("stops when the transport is stopped", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5).stop(1); + }, 1.5).then((buffer) => { + expect(buffer.getTimeOfLastSound()).is.closeTo(1, 0.1); + }); + }); + + it("goes silent at the loop boundary", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.8, 0.5); + transport.loopEnd = 1; + transport.loop = true; + transport.start(); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.2, 0.05); + expect(buffer.getRmsAtTime(1.1)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0.2, 0.05); + }); + }); + + it("can unsync", () => { + return Offline(({ transport }) => { + const synth = new PolySynth(Synth, { + envelope: { + sustain: 1, + release: 0 + } + }).sync().toDestination().unsync(); + synth.triggerAttackRelease("C4", 1, 0.5); + transport.start().stop(1); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.4)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0, 0.05); + }); + }); + }); + context("API", () => { it("can be constructed with an options object", () => { @@ -264,6 +341,13 @@ describe("PolySynth", () => { polySynth.dispose(); }); + it("throws an error when used without a monophonic synth", () => { + expect(() => { + // @ts-ignore + new PolySynth(PluckSynth); + }).throws(Error); + }); + it("can pass in the volume", () => { const polySynth = new PolySynth({ volume: -12, diff --git a/Tone/instrument/PolySynth.ts b/Tone/instrument/PolySynth.ts index 8daa23b7d..07cdfa58b 100644 --- a/Tone/instrument/PolySynth.ts +++ b/Tone/instrument/PolySynth.ts @@ -26,7 +26,8 @@ type VoiceOptions = T extends MonoSynth ? MonoSynthOptions : T extends AMSynth ? AMSynthOptions : T extends Synth ? SynthOptions : - never; + T extends Monophonic ? U : + never; /** * The settable synth options. excludes monophonic options. @@ -41,7 +42,7 @@ export interface PolySynthOptions extends InstrumentOptions { /** * PolySynth handles voice creation and allocation for any - * instruments passed in as the second paramter. PolySynth is + * instruments passed in as the second parameter. PolySynth is * not a synthesizer by itself, it merely manages voices of * one of the other types of synths, allowing any of the * monophonic synthesizers to be polyphonic. @@ -175,6 +176,7 @@ export class PolySynth = Synth> extends Instrument context: this.context, onsilence: this._makeVoiceAvailable.bind(this), })); + assert(voice instanceof Monophonic, "Voice must extend Monophonic class"); voice.connect(this.output); this._voices.push(voice); return voice; @@ -250,7 +252,9 @@ export class PolySynth = Synth> extends Instrument } else { // schedule it to start in the future this.context.setTimeout(() => { - this._scheduleEvent(type, notes, time, velocity); + if (!this.disposed) { + this._scheduleEvent(type, notes, time, velocity); + } }, time - this.now()); } } @@ -281,10 +285,9 @@ export class PolySynth = Synth> extends Instrument * @param notes The notes to play. Accepts a single Frequency or an array of frequencies. * @param time When the release will be triggered. * @example - * @example * const poly = new Tone.PolySynth(Tone.AMSynth).toDestination(); * poly.triggerAttack(["Ab3", "C4", "F5"]); - * // trigger the release of the given notes. + * // trigger the release of the given notes. * poly.triggerRelease(["Ab3", "C4"], "+1"); * poly.triggerRelease("F5", "+3"); */ @@ -337,10 +340,20 @@ export class PolySynth = Synth> extends Instrument if (this._syncState()) { this._syncMethod("triggerAttack", 1); this._syncMethod("triggerRelease", 1); + + // make sure that the sound doesn't play after its been stopped + this.context.transport.on("stop", this._syncedRelease); + this.context.transport.on("pause", this._syncedRelease); + this.context.transport.on("loopEnd", this._syncedRelease); } return this; } + /** + * The release which is scheduled to the timeline. + */ + protected _syncedRelease = (time: number) => this.releaseAll(time); + /** * Set a member/attribute of the voices * @example @@ -378,7 +391,7 @@ export class PolySynth = Synth> extends Instrument }); return this; } - + dispose(): this { super.dispose(); this._dummyVoice.dispose(); diff --git a/Tone/instrument/Sampler.test.ts b/Tone/instrument/Sampler.test.ts index 2e4d39a07..99b2a9dc6 100644 --- a/Tone/instrument/Sampler.test.ts +++ b/Tone/instrument/Sampler.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ import { expect } from "chai"; import { BasicTests } from "test/helper/Basic"; import { CompareToFile } from "test/helper/CompareToFile"; diff --git a/Tone/instrument/Sampler.ts b/Tone/instrument/Sampler.ts index c7ba3951d..9efb1f480 100644 --- a/Tone/instrument/Sampler.ts +++ b/Tone/instrument/Sampler.ts @@ -138,7 +138,7 @@ export class Sampler extends Instrument { return Object.assign(Instrument.getDefaults(), { attack: 0, baseUrl: "", - curve: "exponential" as "exponential", + curve: "exponential" as const, onload: noOp, onerror: noOp, release: 0.1, diff --git a/Tone/instrument/Synth.test.ts b/Tone/instrument/Synth.test.ts index d907fa933..436107116 100644 --- a/Tone/instrument/Synth.test.ts +++ b/Tone/instrument/Synth.test.ts @@ -106,6 +106,69 @@ describe("Synth", () => { }); }); + context("Transport sync", () => { + it("is silent until the transport is started", () => { + return Offline(({ transport }) => { + const synth = new Synth().sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5); + }, 1).then((buffer) => { + expect(buffer.getTimeOfFirstSound()).is.closeTo(0.5, 0.1); + }); + }); + + it("stops when the transport is stopped", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.5); + transport.start(0.5).stop(1); + }, 1.5).then((buffer) => { + expect(buffer.getTimeOfLastSound()).is.closeTo(1, 0.1); + }); + }); + + it("goes silent at the loop boundary", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + release: 0 + } + }).sync().toDestination(); + synth.triggerAttackRelease("C4", 0.8, 0.5); + transport.loopEnd = 1; + transport.loop = true; + transport.start(); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.2, 0.05); + expect(buffer.getRmsAtTime(1.1)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0.2, 0.05); + }); + }); + + it("can unsync", () => { + return Offline(({ transport }) => { + const synth = new Synth({ + envelope: { + sustain: 1, + release: 0 + } + }).sync().toDestination().unsync(); + synth.triggerAttackRelease("C4", 1, 0.5); + transport.start().stop(1); + }, 2).then((buffer) => { + expect(buffer.getRmsAtTime(0)).to.be.closeTo(0, 0.05); + expect(buffer.getRmsAtTime(0.6)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.4)).to.be.closeTo(0.6, 0.05); + expect(buffer.getRmsAtTime(1.6)).to.be.closeTo(0, 0.05); + }); + }); + }); + context("Portamento", () => { it("can play notes with a portamento", () => { return Offline(() => { diff --git a/Tone/signal/Signal.test.ts b/Tone/signal/Signal.test.ts index 3e263efe0..cb1345375 100644 --- a/Tone/signal/Signal.test.ts +++ b/Tone/signal/Signal.test.ts @@ -480,6 +480,15 @@ describe("Signal", () => { }, 10); }); + it("keeps the ratio of a time signal when the bpm changes", () => { + return ConstantOutput(({ transport }) => { + transport.bpm.value = 120; + const sig = new Signal("4n", "time").toDestination(); + transport.syncSignal(sig); + transport.bpm.value = 240; + }, 0.25); + }); + it("outputs 0 when the signal is 0", () => { return ConstantOutput(({ transport }) => { transport.bpm.value = 120; diff --git a/Tone/signal/SyncedSignal.test.ts b/Tone/signal/SyncedSignal.test.ts index f0875b52f..38eb55bfb 100644 --- a/Tone/signal/SyncedSignal.test.ts +++ b/Tone/signal/SyncedSignal.test.ts @@ -156,9 +156,11 @@ describe("SyncedSignal", () => { sched.exponentialRampTo(3, 1, 1); transport.start(0); }, 3).then((buffer) => { - buffer.forEach((sample, time) => { - expect(sample).to.be.closeTo(sched.getValueAtTime(time), 0.02); - }); + expect(buffer.getValueAtTime(0)).to.closeTo(1, 0.1); + expect(buffer.getValueAtTime(0.5)).to.closeTo(1, 0.1); + expect(buffer.getValueAtTime(1)).to.closeTo(1, 0.1); + expect(buffer.getValueAtTime(1.5)).to.closeTo(1.75, 0.1); + expect(buffer.getValueAtTime(2)).to.closeTo(3, 0.1); }); }); diff --git a/Tone/signal/ToneConstantSource.test.ts b/Tone/signal/ToneConstantSource.test.ts index 1ae9bea51..f8c7d868c 100644 --- a/Tone/signal/ToneConstantSource.test.ts +++ b/Tone/signal/ToneConstantSource.test.ts @@ -152,7 +152,7 @@ describe("ToneConstantSource", () => { expect(source.getStateAtTime(0)).to.equal("stopped"); expect(source.getStateAtTime(currentTime)).to.equal("started"); setTimeout(() => { - currentTime = source.context.currentTime; + currentTime = source.now(); source.stop(0); expect(source.getStateAtTime(currentTime + 0.01)).to.equal("stopped"); source.dispose(); diff --git a/Tone/source/OneShotSource.ts b/Tone/source/OneShotSource.ts index 87a6b3a5f..c87590d33 100644 --- a/Tone/source/OneShotSource.ts +++ b/Tone/source/OneShotSource.ts @@ -168,7 +168,7 @@ export abstract class OneShotSource< // schedule the stop callback this._stopTime = this.toSeconds(time) + fadeOutTime; - this._stopTime = Math.max(this._stopTime, this.context.currentTime); + this._stopTime = Math.max(this._stopTime, this.now()); if (fadeOutTime > 0) { // start the fade out curve at the given time if (this._curve === "linear") { @@ -254,7 +254,8 @@ export abstract class OneShotSource< dispose(): this { super.dispose(); - this._gainNode.disconnect(); + this._gainNode.dispose(); + this.onended = noOp; return this; } } diff --git a/Tone/source/Source.test.ts b/Tone/source/Source.test.ts index a5d9661f4..10df2675c 100644 --- a/Tone/source/Source.test.ts +++ b/Tone/source/Source.test.ts @@ -368,7 +368,7 @@ describe("Source", () => { }, 0.7).then(output => { expect(output.getValueAtTime(0.01)).to.be.closeTo(0.2, 0.01); expect(output.getValueAtTime(0.1)).to.be.closeTo(0.3, 0.01); - expect(output.getValueAtTime(0.2)).to.be.closeTo(0.4, 0.01); + expect(output.getValueAtTime(0.199)).to.be.closeTo(0.4, 0.01); expect(output.getValueAtTime(0.31)).to.be.equal(0); }); }); diff --git a/Tone/source/Source.ts b/Tone/source/Source.ts index 9c3e07b5d..d2d7bbac0 100644 --- a/Tone/source/Source.ts +++ b/Tone/source/Source.ts @@ -2,11 +2,19 @@ import { Volume } from "../component/channel/Volume"; import "../core/context/Destination"; import "../core/clock/Transport"; import { Param } from "../core/context/Param"; -import { OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode"; +import { + OutputNode, + ToneAudioNode, + ToneAudioNodeOptions, +} from "../core/context/ToneAudioNode"; import { Decibels, Seconds, Time } from "../core/type/Units"; import { defaultArg } from "../core/util/Defaults"; import { noOp, readOnly } from "../core/util/Interface"; -import { BasicPlaybackState, StateTimeline, StateTimelineEvent } from "../core/util/StateTimeline"; +import { + BasicPlaybackState, + StateTimeline, + StateTimelineEvent, +} from "../core/util/StateTimeline"; import { isDefined, isUndef } from "../core/util/TypeCheck"; import { assert, assertContextRunning } from "../core/util/Debug"; import { GT } from "../core/util/Math"; @@ -20,9 +28,9 @@ export interface SourceOptions extends ToneAudioNodeOptions { } /** - * Base class for sources. + * Base class for sources. * start/stop of this.context.transport. - * + * * ``` * // Multiple state change events can be chained together, * // but must be set in the correct order and with ascending times @@ -36,15 +44,16 @@ export interface SourceOptions extends ToneAudioNodeOptions { * state.start("+0.3").stop("+0.2"); * ``` */ -export abstract class Source extends ToneAudioNode { - +export abstract class Source< + Options extends SourceOptions +> extends ToneAudioNode { /** * The output volume node */ private _volume: Volume; /** - * The output note + * The output node */ output: OutputNode; @@ -129,7 +138,9 @@ export abstract class Source extends ToneAudioNod get state(): BasicPlaybackState { if (this._synced) { if (this.context.transport.state === "started") { - return this._state.getValueAtTime(this.context.transport.seconds) as BasicPlaybackState; + return this._state.getValueAtTime( + this.context.transport.seconds + ) as BasicPlaybackState; } else { return "stopped"; } @@ -155,7 +166,11 @@ export abstract class Source extends ToneAudioNod // overwrite these functions protected abstract _start(time: Time, offset?: Time, duration?: Time): void; protected abstract _stop(time: Time): void; - protected abstract _restart(time: Seconds, offset?: Time, duration?: Time): void; + protected abstract _restart( + time: Seconds, + offset?: Time, + duration?: Time + ): void; /** * Ensure that the scheduled time is not before the current time. @@ -178,12 +193,24 @@ export abstract class Source extends ToneAudioNod * source.start("+0.5"); // starts the source 0.5 seconds from now */ start(time?: Time, offset?: Time, duration?: Time): this { - let computedTime = isUndef(time) && this._synced ? this.context.transport.seconds : this.toSeconds(time); + let computedTime = + isUndef(time) && this._synced + ? this.context.transport.seconds + : this.toSeconds(time); computedTime = this._clampToCurrentTime(computedTime); // if it's started, stop it and restart it - if (!this._synced && this._state.getValueAtTime(computedTime) === "started") { + if ( + !this._synced && + this._state.getValueAtTime(computedTime) === "started" + ) { // time should be strictly greater than the previous start time - assert(GT(computedTime, (this._state.get(computedTime) as StateTimelineEvent).time), "Start time must be strictly greater than previous start time"); + assert( + GT( + computedTime, + (this._state.get(computedTime) as StateTimelineEvent).time + ), + "Start time must be strictly greater than previous start time" + ); this._state.cancel(computedTime); this._state.setStateAtTime("started", computedTime); this.log("restart", computedTime); @@ -196,18 +223,26 @@ export abstract class Source extends ToneAudioNod const event = this._state.get(computedTime); if (event) { event.offset = this.toSeconds(defaultArg(offset, 0)); - event.duration = duration ? this.toSeconds(duration) : undefined; + event.duration = duration + ? this.toSeconds(duration) + : undefined; } - const sched = this.context.transport.schedule(t => { + const sched = this.context.transport.schedule((t) => { this._start(t, offset, duration); }, computedTime); this._scheduled.push(sched); // if the transport is already started // and the time is greater than where the transport is - if (this.context.transport.state === "started" && - this.context.transport.getSecondsAtTime(this.immediate()) > computedTime) { - this._syncedStart(this.now(), this.context.transport.seconds); + if ( + this.context.transport.state === "started" && + this.context.transport.getSecondsAtTime(this.immediate()) > + computedTime + ) { + this._syncedStart( + this.now(), + this.context.transport.seconds + ); } } else { assertContextRunning(this.context); @@ -227,14 +262,23 @@ export abstract class Source extends ToneAudioNod * source.stop("+0.5"); // stops the source 0.5 seconds from now */ stop(time?: Time): this { - let computedTime = isUndef(time) && this._synced ? this.context.transport.seconds : this.toSeconds(time); + let computedTime = + isUndef(time) && this._synced + ? this.context.transport.seconds + : this.toSeconds(time); computedTime = this._clampToCurrentTime(computedTime); - if (this._state.getValueAtTime(computedTime) === "started" || isDefined(this._state.getNextState("started", computedTime))) { + if ( + this._state.getValueAtTime(computedTime) === "started" || + isDefined(this._state.getNextState("started", computedTime)) + ) { this.log("stop", computedTime); if (!this._synced) { this._stop(computedTime); } else { - const sched = this.context.transport.schedule(this._stop.bind(this), computedTime); + const sched = this.context.transport.schedule( + this._stop.bind(this), + computedTime + ); this._scheduled.push(sched); } this._state.cancel(computedTime); @@ -274,23 +318,36 @@ export abstract class Source extends ToneAudioNod if (!this._synced) { this._synced = true; this._syncedStart = (time, offset) => { - if (offset > 0) { + if (GT(offset, 0)) { // get the playback state at that time const stateEvent = this._state.get(offset); // listen for start events which may occur in the middle of the sync'ed time - if (stateEvent && stateEvent.state === "started" && stateEvent.time !== offset) { + if ( + stateEvent && + stateEvent.state === "started" && + stateEvent.time !== offset + ) { // get the offset - const startOffset = offset - this.toSeconds(stateEvent.time); + const startOffset = + offset - this.toSeconds(stateEvent.time); let duration: number | undefined; if (stateEvent.duration) { - duration = this.toSeconds(stateEvent.duration) - startOffset; + duration = + this.toSeconds(stateEvent.duration) - + startOffset; } - this._start(time, this.toSeconds(stateEvent.offset) + startOffset, duration); + this._start( + time, + this.toSeconds(stateEvent.offset) + startOffset, + duration + ); } } }; - this._syncedStop = time => { - const seconds = this.context.transport.getSecondsAtTime(Math.max(time - this.sampleTime, 0)); + this._syncedStop = (time) => { + const seconds = this.context.transport.getSecondsAtTime( + Math.max(time - this.sampleTime, 0) + ); if (this._state.getValueAtTime(seconds) === "started") { this._stop(time); } @@ -317,7 +374,7 @@ export abstract class Source extends ToneAudioNod } this._synced = false; // clear all of the scheduled ids - this._scheduled.forEach(id => this.context.transport.clear(id)); + this._scheduled.forEach((id) => this.context.transport.clear(id)); this._scheduled = []; this._state.cancel(0); // stop it also diff --git a/Tone/source/buffer/Player.test.ts b/Tone/source/buffer/Player.test.ts index 9d9b6dceb..ee167cde9 100644 --- a/Tone/source/buffer/Player.test.ts +++ b/Tone/source/buffer/Player.test.ts @@ -8,7 +8,6 @@ import { getContext } from "Tone/core/Global"; import { Player } from "./Player"; describe("Player", () => { - const buffer = new ToneAudioBuffer(); beforeEach(() => { @@ -20,15 +19,18 @@ describe("Player", () => { SourceTests(Player, buffer); it("matches a file", () => { - return CompareToFile(() => { - const player = new Player(buffer).toDestination(); - player.start(0.1).stop(0.2); - player.playbackRate = 2; - }, "player.wav", 0.005); + return CompareToFile( + () => { + const player = new Player(buffer).toDestination(); + player.start(0.1).stop(0.2); + player.playbackRate = 2; + }, + "player.wav", + 0.005 + ); }); context("Constructor", () => { - it("can be constructed with a Tone.Buffer", () => { const player = new Player(buffer); expect(player.buffer.get()).to.equal(buffer.get()); @@ -60,7 +62,6 @@ describe("Player", () => { }); context("onstop", () => { - it("invokes the onstop method when the player is explicitly stopped", () => { let wasInvoked = false; return Offline(() => { @@ -105,7 +106,6 @@ describe("Player", () => { }); context("Loading", () => { - it("loads a url which was passed in", (done) => { const player = new Player("./audio/sine.wav", () => { expect(player.loaded).to.be.true; @@ -131,12 +131,12 @@ describe("Player", () => { it("invokes onerror if no url", (done) => { const source = new Player({ - url: "./nosuchfile.wav", + url: "./nosuchfile.wav", onerror(e) { expect(e).to.be.instanceOf(Error); source.dispose(); done(); - } + }, }); }); @@ -152,11 +152,9 @@ describe("Player", () => { url: "./audio/sine.wav", }); }); - }); context("Reverse", () => { - it("can get/set reverse", () => { const player = new Player(); player.reverse = true; @@ -166,7 +164,9 @@ describe("Player", () => { it("can be played in reverse", () => { const shorterBuffer = buffer.slice(0, buffer.duration / 2); - const audioBuffer = (shorterBuffer.get() as AudioBuffer).getChannelData(0); + const audioBuffer = ( + shorterBuffer.get() as AudioBuffer + ).getChannelData(0); const lastSample = audioBuffer[audioBuffer.length - 1]; expect(lastSample).to.not.equal(0); return Offline(() => { @@ -180,11 +180,9 @@ describe("Player", () => { expect(firstSample).to.equal(lastSample); }); }); - }); context("Looping", () => { - beforeEach(() => { return buffer.load("./audio/short_sine.wav"); }); @@ -243,7 +241,7 @@ describe("Player", () => { player.toDestination(); player.start(0); player.loop = true; - }, buffer.duration * 1.5).then(buff => { + }, buffer.duration * 1.5).then((buff) => { expect(buff.getRmsAtTime(0)).to.be.above(0); expect(buff.getRmsAtTime(buffer.duration * 0.5)).to.be.above(0); expect(buff.getRmsAtTime(buffer.duration)).to.be.above(0); @@ -252,7 +250,8 @@ describe("Player", () => { }); it("offset is the loopStart when set to loop", () => { - const testSample = buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; + const testSample = + buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; return Offline(() => { const player = new Player(buffer); player.loopStart = 0.1; @@ -271,10 +270,10 @@ describe("Player", () => { player.loop = true; player.toDestination(); player.start(0, 0, playDur); - }, buffer.duration * 2).then(buff => { + }, buffer.duration * 2).then((buff) => { for (let time = 0; time < buffer.duration * 2; time += 0.1) { const val = buff.getRmsAtTime(time); - if (time < (playDur - 0.01)) { + if (time < playDur - 0.01) { expect(val).to.be.greaterThan(0); } else if (time > playDur) { expect(val).to.equal(0); @@ -286,9 +285,11 @@ describe("Player", () => { it("correctly compensates if the offset is greater than the loopEnd", () => { return Offline(() => { // make a ramp between 0-1 - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length)) * 0.3; + ramp[i] = (i / ramp.length) * 0.3; } const buff = ToneAudioBuffer.fromArray(ramp); const player = new Player(buff).toDestination(); @@ -306,7 +307,6 @@ describe("Player", () => { }); }); }); - }); context("PlaybackRate", () => { @@ -344,7 +344,6 @@ describe("Player", () => { }); context("Get/Set", () => { - it("can be set with an options object", () => { const player = new Player(); expect(player.loop).to.be.false; @@ -399,13 +398,12 @@ describe("Player", () => { expect(player.playbackRate).to.equal(0.5); player.dispose(); }); - }); context("Start Scheduling", () => { - it("can be start with an offset", () => { - const testSample = buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; + const testSample = + buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)]; return Offline(() => { const player = new Player(buffer.get()); player.toDestination(); @@ -418,9 +416,11 @@ describe("Player", () => { it("is stopped and restarted when start is called twice", () => { return Offline(() => { // make a ramp between 0-1 - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length - 1)); + ramp[i] = i / (ramp.length - 1); } const buff = new ToneAudioBuffer().fromArray(ramp); const player = new Player(buff).toDestination(); @@ -442,9 +442,11 @@ describe("Player", () => { it("can seek to a position at the given time", () => { return Offline(() => { - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length)) * 0.3; + ramp[i] = (i / ramp.length) * 0.3; } const buff = new ToneAudioBuffer().fromArray(ramp); const player = new Player(buff).toDestination(); @@ -466,7 +468,7 @@ describe("Player", () => { const player = new Player(buffer); player.toDestination(); player.start(0).stop(0.1); - return time => { + return (time) => { whenBetween(time, 0.1, Infinity, () => { expect(player.state).to.equal("stopped"); }); @@ -475,9 +477,13 @@ describe("Player", () => { }); }; }, 0.3).then((buff) => { - buff.forEachBetween((sample) => { - expect(sample).to.equal(0); - }, 0.11, 0.15); + buff.forEachBetween( + (sample) => { + expect(sample).to.equal(0); + }, + 0.11, + 0.15 + ); }); }); @@ -495,7 +501,10 @@ describe("Player", () => { return Offline(() => { const player = new Player(buffer); player.toDestination(); - player.start(0, 0, 0.05).start(0.1, 0, 0.05).start(0.2, 0, 0.05); + player + .start(0, 0, 0.05) + .start(0.1, 0, 0.05) + .start(0.2, 0, 0.05); player.stop(0.1); }, 0.3).then((buff) => { expect(buff.getTimeOfLastSound()).to.be.closeTo(0.1, 0.02); @@ -518,7 +527,7 @@ describe("Player", () => { const player = new Player(buffer); player.toDestination(); player.start(0, 0, 0.1); - return time => { + return (time) => { whenBetween(time, 0.1, Infinity, () => { expect(player.state).to.equal("stopped"); }); @@ -549,17 +558,42 @@ describe("Player", () => { it("plays synced to the Transport", () => { return Offline(({ transport }) => { - const player = new Player(buffer).sync().start(0).toDestination(); + const player = new Player(buffer) + .sync() + .start(0) + .toDestination(); transport.start(0); }, 0.05).then((buff) => { expect(buff.isSilent()).to.be.false; }); }); + it("does not play twice when the offset is very small", () => { + // addresses #999 and #944 + return CompareToFile( + () => { + const player = new Player(buffer).toDestination(); + player.sync().start(0); + getContext().transport.bpm.value = 125; + getContext().transport.setLoopPoints(0, "1:0:0"); + getContext().transport.loop = true; + getContext().transport.start(0); + }, + "playerSyncLoop.wav", + 0.01 + ); + }); + it("offsets correctly when started by the Transport", () => { - const testSample = buffer.toArray(0)[Math.floor(0.13125 * getContext().sampleRate)]; + const testSample = + buffer.toArray(0)[ + Math.floor(0.13125 * getContext().sampleRate) + ]; return Offline(({ transport }) => { - const player = new Player(buffer).sync().start(0, 0.1).toDestination(); + const player = new Player(buffer) + .sync() + .start(0, 0.1) + .toDestination(); transport.start(0, 0.03125); }, 0.05).then((buff) => { expect(buff.toArray()[0][0]).to.equal(testSample); @@ -569,9 +603,11 @@ describe("Player", () => { it("starts at the correct position when Transport is offset and playbackRate is not 1", () => { return Offline(({ transport }) => { // make a ramp between 0-1 - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length)); + ramp[i] = i / ramp.length; } const buff = ToneAudioBuffer.fromArray(ramp); const player = new Player(buff).toDestination(); @@ -586,9 +622,11 @@ describe("Player", () => { it("starts with an offset when synced and started after Transport is running", () => { return Offline(({ transport }) => { - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length)) * 0.3; + ramp[i] = (i / ramp.length) * 0.3; } const buff = new ToneAudioBuffer().fromArray(ramp); const player = new Player(buff).toDestination(); @@ -606,9 +644,11 @@ describe("Player", () => { it("can pass in an offset when synced and started after Transport is running", () => { return Offline(({ transport }) => { - const ramp = new Float32Array(Math.floor(getContext().sampleRate * 0.3)); + const ramp = new Float32Array( + Math.floor(getContext().sampleRate * 0.3) + ); for (let i = 0; i < ramp.length; i++) { - ramp[i] = (i / (ramp.length)) * 0.3; + ramp[i] = (i / ramp.length) * 0.3; } const buff = new ToneAudioBuffer().fromArray(ramp); const player = new Player(buff).toDestination(); @@ -630,12 +670,18 @@ describe("Player", () => { it("fades in and out correctly", () => { let duration = 0.5; return Offline(() => { - const onesArray = new Float32Array(getContext().sampleRate * duration); + const onesArray = new Float32Array( + getContext().sampleRate * duration + ); onesArray.forEach((sample, index) => { onesArray[index] = 1; }); const onesBuffer = ToneAudioBuffer.fromArray(onesArray); - const player = new Player({ url: onesBuffer, fadeOut: 0.1, fadeIn: 0.1 }).toDestination(); + const player = new Player({ + url: onesBuffer, + fadeOut: 0.1, + fadeIn: 0.1, + }).toDestination(); player.start(0); }, 0.6).then((buff) => { expect(buff.getRmsAtTime(0)).to.be.closeTo(0, 0.1); @@ -643,9 +689,40 @@ describe("Player", () => { expect(buff.getRmsAtTime(0.1)).to.be.closeTo(1, 0.1); duration -= 0.1; expect(buff.getRmsAtTime(duration)).to.be.closeTo(1, 0.1); - expect(buff.getRmsAtTime(duration + 0.05)).to.be.closeTo(0.5, 0.1); + expect(buff.getRmsAtTime(duration + 0.05)).to.be.closeTo( + 0.5, + 0.1 + ); expect(buff.getRmsAtTime(duration + 0.1)).to.be.closeTo(0, 0.1); }); }); + + it("stops only last activeSource when restarting at intervals < latencyHint", (done) => { + const originalLookAhead = getContext().lookAhead; + getContext().lookAhead = .3; + const player = new Player({ + onload(): void { + player.start(undefined, undefined, 1); + setTimeout(() => player.restart(undefined, undefined, 1), 50); + setTimeout(() => player.restart(undefined, undefined, 1), 100); + setTimeout(() => player.restart(undefined, undefined, 1), 150); + setTimeout(() => { + player.restart(undefined, undefined, 1); + const checkStopTimes = new Set(); + // @ts-ignore + player._activeSources.forEach(source => { + // @ts-ignore + checkStopTimes.add(source._stopTime); + }); + getContext().lookAhead = originalLookAhead; + // ensure each source has a different stopTime + // @ts-ignore + expect(checkStopTimes.size).to.equal(player._activeSources.size); + done(); + }, 250); + }, + url: "./audio/sine.wav", + }); + }); }); }); diff --git a/Tone/source/buffer/Player.ts b/Tone/source/buffer/Player.ts index aca158b28..fbb88ab5e 100644 --- a/Tone/source/buffer/Player.ts +++ b/Tone/source/buffer/Player.ts @@ -31,7 +31,6 @@ export interface PlayerOptions extends SourceOptions { * @category Source */ export class Player extends Source { - readonly name: string = "Player"; /** @@ -86,12 +85,22 @@ export class Player extends Source { * @param url Either the AudioBuffer or the url from which to load the AudioBuffer * @param onload The function to invoke when the buffer is loaded. */ - constructor(url?: string | AudioBuffer | ToneAudioBuffer, onload?: () => void); + constructor( + url?: string | AudioBuffer | ToneAudioBuffer, + onload?: () => void + ); constructor(options?: Partial); constructor() { - - super(optionsFromArguments(Player.getDefaults(), arguments, ["url", "onload"])); - const options = optionsFromArguments(Player.getDefaults(), arguments, ["url", "onload"]); + super( + optionsFromArguments(Player.getDefaults(), arguments, [ + "url", + "onload", + ]) + ); + const options = optionsFromArguments(Player.getDefaults(), arguments, [ + "url", + "onload", + ]); this._buffer = new ToneAudioBuffer({ onload: this._onload.bind(this, options.onload), @@ -157,8 +166,11 @@ export class Player extends Source { // delete the source from the active sources this._activeSources.delete(source); - if (this._activeSources.size === 0 && !this._synced && - this._state.getValueAtTime(this.now()) === "started") { + if ( + this._activeSources.size === 0 && + !this._synced && + this._state.getValueAtTime(this.now()) === "started" + ) { // remove the 'implicitEnd' event and replace with an explicit end this._state.cancel(this.now()); this._state.setStateAtTime("stopped", this.now()); @@ -196,7 +208,10 @@ export class Player extends Source { // compute the duration which is either the passed in duration of the buffer.duration - offset const origDuration = duration; - duration = defaultArg(duration, Math.max(this._buffer.duration - computedOffset, 0)); + duration = defaultArg( + duration, + Math.max(this._buffer.duration - computedOffset, 0) + ); let computedDuration = this.toSeconds(duration); // scale it by the playback rate @@ -223,9 +238,13 @@ export class Player extends Source { // cancel the previous stop this._state.cancel(startTime + computedDuration); // if it's not looping, set the state change at the end of the sample - this._state.setStateAtTime("stopped", startTime + computedDuration, { - implicitEnd: true, - }); + this._state.setStateAtTime( + "stopped", + startTime + computedDuration, + { + implicitEnd: true, + } + ); } // add it to the array of active sources @@ -236,7 +255,11 @@ export class Player extends Source { source.start(startTime, computedOffset); } else { // subtract the fade out time - source.start(startTime, computedOffset, computedDuration - this.toSeconds(this.fadeOut)); + source.start( + startTime, + computedOffset, + computedDuration - this.toSeconds(this.fadeOut) + ); } } @@ -245,14 +268,14 @@ export class Player extends Source { */ protected _stop(time?: Time): void { const computedTime = this.toSeconds(time); - this._activeSources.forEach(source => source.stop(computedTime)); + this._activeSources.forEach((source) => source.stop(computedTime)); } /** * Stop and then restart the player from the beginning (or offset) * @param time When the player should start. * @param offset The offset from the beginning of the sample to start at. - * @param duration How long the sample should play. If no duration is given, + * @param duration How long the sample should play. If no duration is given, * it will default to the full length of the sample (minus any offset) */ restart(time?: Seconds, offset?: Time, duration?: Time): this { @@ -261,7 +284,7 @@ export class Player extends Source { } protected _restart(time?: Seconds, offset?: Time, duration?: Time): void { - this._stop(time); + [...this._activeSources].pop()?.stop(time); // explicitly stop only the most recently created source, to avoid edge case when > 1 source exists and _stop() erroneously sets all stop times past original end offset this._start(time, offset, duration); } @@ -318,7 +341,7 @@ export class Player extends Source { assertRange(this.toSeconds(loopStart), 0, this.buffer.duration); } // get the current source - this._activeSources.forEach(source => { + this._activeSources.forEach((source) => { source.loopStart = loopStart; }); } @@ -335,7 +358,7 @@ export class Player extends Source { assertRange(this.toSeconds(loopEnd), 0, this.buffer.duration); } // get the current source - this._activeSources.forEach(source => { + this._activeSources.forEach((source) => { source.loopEnd = loopEnd; }); } @@ -367,7 +390,7 @@ export class Player extends Source { } this._loop = loop; // set the loop of all of the sources - this._activeSources.forEach(source => { + this._activeSources.forEach((source) => { source.loop = loop; }); if (loop) { @@ -399,17 +422,18 @@ export class Player extends Source { const stopEvent = this._state.getNextState("stopped", now); if (stopEvent && stopEvent.implicitEnd) { this._state.cancel(stopEvent.time); - this._activeSources.forEach(source => source.cancelStop()); + this._activeSources.forEach((source) => source.cancelStop()); } // set all the sources - this._activeSources.forEach(source => { + this._activeSources.forEach((source) => { source.playbackRate.setValueAtTime(rate, now); }); } /** - * If the buffer should be reversed + * If the buffer should be reversed. Note that this sets the underlying [[ToneAudioBuffer.reverse]], so + * if multiple players are pointing at the same ToneAudioBuffer, they will all be reversed. * @example * const player = new Tone.Player("https://tonejs.github.io/audio/berklee/chime_1.mp3").toDestination(); * player.autostart = true; @@ -432,7 +456,7 @@ export class Player extends Source { dispose(): this { super.dispose(); // disconnect all of the players - this._activeSources.forEach(source => source.dispose()); + this._activeSources.forEach((source) => source.dispose()); this._activeSources.clear(); this._buffer.dispose(); return this; diff --git a/Tone/source/buffer/Players.ts b/Tone/source/buffer/Players.ts index ddfc71c4c..c75b45a9e 100644 --- a/Tone/source/buffer/Players.ts +++ b/Tone/source/buffer/Players.ts @@ -202,6 +202,12 @@ export class Players extends ToneAudioNode { * @param name A unique name to give the player * @param url Either the url of the bufer or a buffer which will be added with the given name. * @param callback The callback to invoke when the url is loaded. + * @example + * const players = new Tone.Players(); + * players.add("gong", "https://tonejs.github.io/audio/berklee/gong_1.mp3", () => { + * console.log("gong loaded"); + * players.player("gong").start(); + * }); */ add(name: string, url: string | ToneAudioBuffer | AudioBuffer, callback?: () => void): this { assert(!this._buffers.has(name), "A buffer with that name already exists on this object"); diff --git a/Tone/source/oscillator/OmniOscillator.ts b/Tone/source/oscillator/OmniOscillator.ts index a21f6f9f0..562d2ee2a 100644 --- a/Tone/source/oscillator/OmniOscillator.ts +++ b/Tone/source/oscillator/OmniOscillator.ts @@ -315,7 +315,7 @@ export class OmniOscillator /** * The width of the oscillator when sourceType === "pulse". - * See [[PWMOscillator.width]] + * See [[PWMOscillator]] */ get width(): IsPulseOscillator> { if (this._getOscType(this._oscillator, "pulse")) { diff --git a/Tone/source/oscillator/PWMOscillator.ts b/Tone/source/oscillator/PWMOscillator.ts index 724393f82..099051212 100644 --- a/Tone/source/oscillator/PWMOscillator.ts +++ b/Tone/source/oscillator/PWMOscillator.ts @@ -106,7 +106,7 @@ export class PWMOscillator extends Source implements ToneO frequency: 440, modulationFrequency: 0.4, phase: 0, - type: "pwm" as "pwm", + type: "pwm" as const, }); } /** diff --git a/Tone/source/oscillator/PulseOscillator.ts b/Tone/source/oscillator/PulseOscillator.ts index 6d98be3c1..14209dcb7 100644 --- a/Tone/source/oscillator/PulseOscillator.ts +++ b/Tone/source/oscillator/PulseOscillator.ts @@ -128,7 +128,7 @@ export class PulseOscillator extends Source implements T detune: 0, frequency: 440, phase: 0, - type: "pulse" as "pulse", + type: "pulse" as const, width: 0.2, }); } diff --git a/Tone/source/oscillator/ToneOscillatorNode.test.ts b/Tone/source/oscillator/ToneOscillatorNode.test.ts index 03c695336..79f27351d 100644 --- a/Tone/source/oscillator/ToneOscillatorNode.test.ts +++ b/Tone/source/oscillator/ToneOscillatorNode.test.ts @@ -191,7 +191,7 @@ describe("ToneOscillatorNode", () => { expect(osc.getStateAtTime(0)).to.equal("stopped"); expect(osc.getStateAtTime(currentTime)).to.equal("started"); setTimeout(() => { - currentTime = osc.context.currentTime; + currentTime = osc.now(); osc.stop(0); expect(osc.getStateAtTime(currentTime + 0.01)).to.equal("stopped"); osc.dispose(); diff --git a/examples/meter.html b/examples/meter.html index f890359d4..b3a11c53a 100644 --- a/examples/meter.html +++ b/examples/meter.html @@ -33,7 +33,7 @@ }).toDestination(); const toneMeter = new Tone.Meter({ - channels: 2, + channelCount: 2, }); player.connect(toneMeter); diff --git a/examples/mixer.html b/examples/mixer.html index 6de258bfc..8c2a7325c 100644 --- a/examples/mixer.html +++ b/examples/mixer.html @@ -55,7 +55,7 @@ } // create a meter on the destination node - const toneMeter = new Tone.Meter({ channels: 2 }); + const toneMeter = new Tone.Meter({ channelCount: 2 }); Tone.Destination.chain(toneMeter); meter({ tone: toneMeter, diff --git a/examples/pingPongDelay.html b/examples/pingPongDelay.html index 9d8aba9be..a68133cd0 100644 --- a/examples/pingPongDelay.html +++ b/examples/pingPongDelay.html @@ -39,7 +39,7 @@ // play a snare sound through it const player = new Tone.Player("https://tonejs.github.io/audio/drum-samples/CR78/snare.mp3").connect(feedbackDelay); - const toneMeter = new Tone.Meter({ channels: 2 }); + const toneMeter = new Tone.Meter({ channelCount: 2 }); feedbackDelay.connect(toneMeter); meter({ diff --git a/examples/simpleHtml.html b/examples/simpleHtml.html index bc9b61ec7..389e84297 100644 --- a/examples/simpleHtml.html +++ b/examples/simpleHtml.html @@ -5,7 +5,7 @@ Simple HTML - +