Repository: Tonejs/Tone.js Branch: dev Commit: 03569e2a54c2 Files: 432 Total size: 2.4 MB Directory structure: gitextract_3dx9qg9q/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── codecov.yml │ └── workflows/ │ ├── stale.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── Tone/ │ ├── classes.ts │ ├── component/ │ │ ├── analysis/ │ │ │ ├── Analyser.test.ts │ │ │ ├── Analyser.ts │ │ │ ├── DCMeter.test.ts │ │ │ ├── DCMeter.ts │ │ │ ├── FFT.test.ts │ │ │ ├── FFT.ts │ │ │ ├── Follower.test.ts │ │ │ ├── Follower.ts │ │ │ ├── Meter.test.ts │ │ │ ├── Meter.ts │ │ │ ├── MeterBase.ts │ │ │ ├── Waveform.test.ts │ │ │ └── Waveform.ts │ │ ├── channel/ │ │ │ ├── Channel.test.ts │ │ │ ├── Channel.ts │ │ │ ├── CrossFade.test.ts │ │ │ ├── CrossFade.ts │ │ │ ├── Merge.test.ts │ │ │ ├── Merge.ts │ │ │ ├── MidSideMerge.test.ts │ │ │ ├── MidSideMerge.ts │ │ │ ├── MidSideSplit.test.ts │ │ │ ├── MidSideSplit.ts │ │ │ ├── Mono.test.ts │ │ │ ├── Mono.ts │ │ │ ├── MultibandSplit.test.ts │ │ │ ├── MultibandSplit.ts │ │ │ ├── PanVol.test.ts │ │ │ ├── PanVol.ts │ │ │ ├── Panner.test.ts │ │ │ ├── Panner.ts │ │ │ ├── Panner3D.test.ts │ │ │ ├── Panner3D.ts │ │ │ ├── Recorder.test.ts │ │ │ ├── Recorder.ts │ │ │ ├── Solo.test.ts │ │ │ ├── Solo.ts │ │ │ ├── Split.test.ts │ │ │ ├── Split.ts │ │ │ ├── Volume.test.ts │ │ │ └── Volume.ts │ │ ├── dynamics/ │ │ │ ├── Compressor.test.ts │ │ │ ├── Compressor.ts │ │ │ ├── Gate.test.ts │ │ │ ├── Gate.ts │ │ │ ├── Limiter.test.ts │ │ │ ├── Limiter.ts │ │ │ ├── MidSideCompressor.test.ts │ │ │ ├── MidSideCompressor.ts │ │ │ ├── MultibandCompressor.test.ts │ │ │ └── MultibandCompressor.ts │ │ ├── envelope/ │ │ │ ├── AmplitudeEnvelope.test.ts │ │ │ ├── AmplitudeEnvelope.ts │ │ │ ├── Envelope.test.ts │ │ │ ├── Envelope.ts │ │ │ ├── FrequencyEnvelope.test.ts │ │ │ └── FrequencyEnvelope.ts │ │ ├── filter/ │ │ │ ├── BiquadFilter.test.ts │ │ │ ├── BiquadFilter.ts │ │ │ ├── Convolver.test.ts │ │ │ ├── Convolver.ts │ │ │ ├── EQ3.test.ts │ │ │ ├── EQ3.ts │ │ │ ├── FeedbackCombFilter.test.ts │ │ │ ├── FeedbackCombFilter.ts │ │ │ ├── FeedbackCombFilter.worklet.ts │ │ │ ├── Filter.test.ts │ │ │ ├── Filter.ts │ │ │ ├── LowpassCombFilter.test.ts │ │ │ ├── LowpassCombFilter.ts │ │ │ ├── OnePoleFilter.test.ts │ │ │ ├── OnePoleFilter.ts │ │ │ ├── PhaseShiftAllpass.test.ts │ │ │ └── PhaseShiftAllpass.ts │ │ └── index.ts │ ├── core/ │ │ ├── Global.ts │ │ ├── Tone.ts │ │ ├── clock/ │ │ │ ├── Clock.test.ts │ │ │ ├── Clock.ts │ │ │ ├── TickParam.test.ts │ │ │ ├── TickParam.ts │ │ │ ├── TickSignal.test.ts │ │ │ ├── TickSignal.ts │ │ │ ├── TickSource.test.ts │ │ │ ├── TickSource.ts │ │ │ ├── Ticker.test.ts │ │ │ ├── Ticker.ts │ │ │ ├── Transport.test.ts │ │ │ ├── Transport.ts │ │ │ ├── TransportEvent.test.ts │ │ │ ├── TransportEvent.ts │ │ │ ├── TransportRepeatEvent.test.ts │ │ │ └── TransportRepeatEvent.ts │ │ ├── context/ │ │ │ ├── AbstractParam.ts │ │ │ ├── AudioContext.ts │ │ │ ├── BaseContext.ts │ │ │ ├── Context.test.ts │ │ │ ├── Context.ts │ │ │ ├── ContextInitialization.ts │ │ │ ├── Delay.test.ts │ │ │ ├── Delay.ts │ │ │ ├── Destination.test.ts │ │ │ ├── Destination.ts │ │ │ ├── DummyContext.test.ts │ │ │ ├── DummyContext.ts │ │ │ ├── Gain.test.ts │ │ │ ├── Gain.ts │ │ │ ├── Listener.test.ts │ │ │ ├── Listener.ts │ │ │ ├── Offline.test.ts │ │ │ ├── Offline.ts │ │ │ ├── OfflineContext.test.ts │ │ │ ├── OfflineContext.ts │ │ │ ├── OnRunning.test.ts │ │ │ ├── OnRunning.ts │ │ │ ├── Param.test.ts │ │ │ ├── Param.ts │ │ │ ├── ToneAudioBuffer.test.ts │ │ │ ├── ToneAudioBuffer.ts │ │ │ ├── ToneAudioBuffers.test.ts │ │ │ ├── ToneAudioBuffers.ts │ │ │ ├── ToneAudioNode.test.ts │ │ │ ├── ToneAudioNode.ts │ │ │ ├── ToneWithContext.test.ts │ │ │ └── ToneWithContext.ts │ │ ├── index.ts │ │ ├── type/ │ │ │ ├── Conversions.test.ts │ │ │ ├── Conversions.ts │ │ │ ├── Frequency.test.ts │ │ │ ├── Frequency.ts │ │ │ ├── Midi.test.ts │ │ │ ├── Midi.ts │ │ │ ├── NoteUnits.ts │ │ │ ├── Ticks.test.ts │ │ │ ├── Ticks.ts │ │ │ ├── Time.test.ts │ │ │ ├── Time.ts │ │ │ ├── TimeBase.ts │ │ │ ├── TransportTime.test.ts │ │ │ ├── TransportTime.ts │ │ │ └── Units.ts │ │ ├── util/ │ │ │ ├── AdvancedTypeCheck.ts │ │ │ ├── Debug.test.ts │ │ │ ├── Debug.ts │ │ │ ├── Decorator.ts │ │ │ ├── Defaults.ts │ │ │ ├── Draw.test.ts │ │ │ ├── Draw.ts │ │ │ ├── Emitter.test.ts │ │ │ ├── Emitter.ts │ │ │ ├── Interface.ts │ │ │ ├── IntervalTimeline.test.ts │ │ │ ├── IntervalTimeline.ts │ │ │ ├── Math.ts │ │ │ ├── StateTimeline.test.ts │ │ │ ├── StateTimeline.ts │ │ │ ├── Timeline.test.ts │ │ │ ├── Timeline.ts │ │ │ ├── TimelineValue.test.ts │ │ │ ├── TimelineValue.ts │ │ │ ├── TypeCheck.ts │ │ │ └── global.d.ts │ │ └── worklet/ │ │ ├── DelayLine.worklet.ts │ │ ├── SingleIOProcessor.worklet.ts │ │ ├── ToneAudioWorklet.ts │ │ ├── ToneAudioWorkletProcessor.worklet.ts │ │ └── WorkletGlobalScope.ts │ ├── effect/ │ │ ├── AutoFilter.test.ts │ │ ├── AutoFilter.ts │ │ ├── AutoPanner.test.ts │ │ ├── AutoPanner.ts │ │ ├── AutoWah.test.ts │ │ ├── AutoWah.ts │ │ ├── BitCrusher.test.ts │ │ ├── BitCrusher.ts │ │ ├── BitCrusher.worklet.ts │ │ ├── Chebyshev.test.ts │ │ ├── Chebyshev.ts │ │ ├── Chorus.test.ts │ │ ├── Chorus.ts │ │ ├── Distortion.test.ts │ │ ├── Distortion.ts │ │ ├── Effect.ts │ │ ├── FeedbackDelay.test.ts │ │ ├── FeedbackDelay.ts │ │ ├── FeedbackEffect.ts │ │ ├── Freeverb.test.ts │ │ ├── Freeverb.ts │ │ ├── FrequencyShifter.test.ts │ │ ├── FrequencyShifter.ts │ │ ├── JCReverb.test.ts │ │ ├── JCReverb.ts │ │ ├── LFOEffect.ts │ │ ├── LFOStereoEffect.test.ts │ │ ├── LFOStereoEffect.ts │ │ ├── MidSideEffect.ts │ │ ├── Phaser.test.ts │ │ ├── Phaser.ts │ │ ├── PingPongDelay.test.ts │ │ ├── PingPongDelay.ts │ │ ├── PitchShift.test.ts │ │ ├── PitchShift.ts │ │ ├── Reverb.test.ts │ │ ├── Reverb.ts │ │ ├── ReverseDelay.test.ts │ │ ├── ReverseDelay.ts │ │ ├── ReverseDelay.worklet.ts │ │ ├── StereoEffect.ts │ │ ├── StereoFeedbackEffect.ts │ │ ├── StereoWidener.test.ts │ │ ├── StereoWidener.ts │ │ ├── StereoXFeedbackEffect.ts │ │ ├── Tremolo.test.ts │ │ ├── Tremolo.ts │ │ ├── Vibrato.test.ts │ │ ├── Vibrato.ts │ │ └── index.ts │ ├── event/ │ │ ├── Loop.test.ts │ │ ├── Loop.ts │ │ ├── Part.test.ts │ │ ├── Part.ts │ │ ├── Pattern.test.ts │ │ ├── Pattern.ts │ │ ├── PatternGenerator.test.ts │ │ ├── PatternGenerator.ts │ │ ├── Sequence.test.ts │ │ ├── Sequence.ts │ │ ├── ToneEvent.test.ts │ │ ├── ToneEvent.ts │ │ └── index.ts │ ├── fromContext.test.ts │ ├── fromContext.ts │ ├── index.test.ts │ ├── index.ts │ ├── instrument/ │ │ ├── AMSynth.test.ts │ │ ├── AMSynth.ts │ │ ├── DuoSynth.test.ts │ │ ├── DuoSynth.ts │ │ ├── FMSynth.test.ts │ │ ├── FMSynth.ts │ │ ├── Instrument.ts │ │ ├── MembraneSynth.test.ts │ │ ├── MembraneSynth.ts │ │ ├── MetalSynth.test.ts │ │ ├── MetalSynth.ts │ │ ├── ModulationSynth.ts │ │ ├── MonoSynth.test.ts │ │ ├── MonoSynth.ts │ │ ├── Monophonic.ts │ │ ├── NoiseSynth.test.ts │ │ ├── NoiseSynth.ts │ │ ├── PluckSynth.test.ts │ │ ├── PluckSynth.ts │ │ ├── PolySynth.test.ts │ │ ├── PolySynth.ts │ │ ├── Sampler.test.ts │ │ ├── Sampler.ts │ │ ├── Synth.test.ts │ │ ├── Synth.ts │ │ └── index.ts │ ├── signal/ │ │ ├── Abs.test.ts │ │ ├── Abs.ts │ │ ├── Add.test.ts │ │ ├── Add.ts │ │ ├── AudioToGain.test.ts │ │ ├── AudioToGain.ts │ │ ├── GainToAudio.test.ts │ │ ├── GainToAudio.ts │ │ ├── GreaterThan.test.ts │ │ ├── GreaterThan.ts │ │ ├── GreaterThanZero.test.ts │ │ ├── GreaterThanZero.ts │ │ ├── Multiply.test.ts │ │ ├── Multiply.ts │ │ ├── Negate.test.ts │ │ ├── Negate.ts │ │ ├── Pow.test.ts │ │ ├── Pow.ts │ │ ├── Scale.test.ts │ │ ├── Scale.ts │ │ ├── ScaleExp.test.ts │ │ ├── ScaleExp.ts │ │ ├── Signal.test.ts │ │ ├── Signal.ts │ │ ├── SignalOperator.test.ts │ │ ├── SignalOperator.ts │ │ ├── Subtract.test.ts │ │ ├── Subtract.ts │ │ ├── SyncedSignal.test.ts │ │ ├── SyncedSignal.ts │ │ ├── ToneConstantSource.test.ts │ │ ├── ToneConstantSource.ts │ │ ├── WaveShaper.test.ts │ │ ├── WaveShaper.ts │ │ ├── Zero.test.ts │ │ ├── Zero.ts │ │ └── index.ts │ └── source/ │ ├── Noise.test.ts │ ├── Noise.ts │ ├── OneShotSource.ts │ ├── Source.test.ts │ ├── Source.ts │ ├── UserMedia.test.ts │ ├── UserMedia.ts │ ├── buffer/ │ │ ├── GrainPlayer.test.ts │ │ ├── GrainPlayer.ts │ │ ├── Player.test.ts │ │ ├── Player.ts │ │ ├── Players.test.ts │ │ ├── Players.ts │ │ ├── ToneBufferSource.test.ts │ │ └── ToneBufferSource.ts │ ├── index.ts │ └── oscillator/ │ ├── AMOscillator.test.ts │ ├── AMOscillator.ts │ ├── FMOscillator.test.ts │ ├── FMOscillator.ts │ ├── FatOscillator.test.ts │ ├── FatOscillator.ts │ ├── LFO.test.ts │ ├── LFO.ts │ ├── OmniOscillator.test.ts │ ├── OmniOscillator.ts │ ├── Oscillator.test.ts │ ├── Oscillator.ts │ ├── OscillatorInterface.ts │ ├── PWMOscillator.test.ts │ ├── PWMOscillator.ts │ ├── PulseOscillator.test.ts │ ├── PulseOscillator.ts │ ├── ToneOscillatorNode.test.ts │ └── ToneOscillatorNode.ts ├── eslint.config.mjs ├── examples/ │ ├── README.md │ ├── amSynth.html │ ├── analysis.html │ ├── animationSync.html │ ├── bembe.html │ ├── buses.html │ ├── daw.html │ ├── envelope.html │ ├── events.html │ ├── fmSynth.html │ ├── funkyShape.html │ ├── grainPlayer.html │ ├── index.html │ ├── js/ │ │ ├── ExampleList.json │ │ ├── components.js │ │ └── tone-ui.js │ ├── jump.html │ ├── lfoEffects.html │ ├── meter.html │ ├── mic.html │ ├── mixer.html │ ├── monoSynth.html │ ├── noises.html │ ├── offline.html │ ├── oscillator.html │ ├── pianoPhase.html │ ├── pingPongDelay.html │ ├── pitchShift.html │ ├── player.html │ ├── polySynth.html │ ├── quantization.html │ ├── rampTo.html │ ├── reverb.html │ ├── reverseDelay.html │ ├── sampler.html │ ├── shiny.html │ ├── signal.html │ ├── simpleHtml.html │ ├── simpleSynth.html │ ├── spatialPanner.html │ └── stepSequencer.html ├── package.json ├── scripts/ │ ├── cspell.json │ ├── increment_version.cjs │ ├── tsconfig.build.json │ ├── typedoc.json │ └── webpack.config.cjs ├── test/ │ ├── README.md │ ├── helper/ │ │ ├── Basic.ts │ │ ├── CompareToFile.ts │ │ ├── Connect.ts │ │ ├── ConstantOutput.ts │ │ ├── Dispose.ts │ │ ├── EffectTests.ts │ │ ├── InstrumentTests.ts │ │ ├── MonophonicTests.ts │ │ ├── Offline.ts │ │ ├── OscillatorTests.ts │ │ ├── OutputAudio.ts │ │ ├── PassAudio.ts │ │ ├── SignalTests.ts │ │ ├── SourceTests.ts │ │ ├── StereoSignal.ts │ │ └── compare/ │ │ ├── Compare.ts │ │ ├── OfflineRender.ts │ │ ├── Plot.ts │ │ ├── Spectrum.ts │ │ ├── TestAudioBuffer.ts │ │ └── index.ts │ ├── integration/ │ │ ├── node/ │ │ │ ├── package.json │ │ │ └── test.mjs │ │ ├── typescript/ │ │ │ ├── package.json │ │ │ └── test.ts │ │ ├── unpkg/ │ │ │ ├── package.json │ │ │ └── test.mjs │ │ ├── vite/ │ │ │ ├── index.html │ │ │ ├── index.ts │ │ │ └── package.json │ │ └── webpack/ │ │ ├── package.json │ │ └── test.js │ ├── scripts/ │ │ ├── test_examples.mjs │ │ ├── test_html.mjs │ │ ├── test_integrations.mjs │ │ ├── test_readme.mjs │ │ └── utils.mjs │ └── web-test-runner.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ Bug fixes and contributions are welcome! If you're looking for ideas for where to get started, consider jumping in on any of these areas: ### Examples To contribute examples, please follow the current style of the examples. Add your example's title and file name to `examples/js/ExampleList.json` for it to appear in the examples list on the index page. ### Docs There is always more work that can be done on documentation. Especially adding good examples to methods and members to make the docs more informative and useful for people coming from diverse musical and technical backgrounds. All of the docs are written in [TypeDoc](https://typedoc.org/)-style comments in the source code. If you catch a mistake, please send a pull request. Along with this, it'd be great to integrate more visuals and references in the docs to help illustrate concepts. ### Forum If you are someone who is familiar with Tone.js, consider jumping in on [the forum](https://groups.google.com/forum/#!forum/tonejs) to answer some questions. ### Tutorials I'd love to see more tutorials for newcomers to Tone.js to help them get up and running or solve a particular issue. If you make a tutorial, please send me a link. ### Tests Tone.js has an extensive [test suite](https://github.com/Tonejs/Tone.js/wiki/Testing) using Mocha and Chai. Along with more unit tests, it'd also be great to have more tests which run in the online context and generate music to help find bugs by ear that the automated tests might not illuminate. [Audiokit](http://audiokit.io/tests/), for example, has a great suite of aural tests. You can also take a look at Tone.js' [code coverage](https://coveralls.io/github/Tonejs/Tone.js) to get an idea of where more tests might be helpful. ### Synths/Effects If you'd like to contribute a cool and expressive synth or effect, I'd love to hear it. Please send me an example that I can play with along with the PR. ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- 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. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- 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. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ 1. Please make all pull requests on the `dev` branch. 2. Don't commit build files 2. Try to get all [tests](https://github.com/Tonejs/Tone.js/wiki/Testing) to pass. ================================================ FILE: .github/codecov.yml ================================================ coverage: status: project: default: target: auto threshold: 1% ================================================ FILE: .github/workflows/stale.yml ================================================ name: "Close stale issues and PRs" on: schedule: - cron: "30 1 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: any-of-labels: "cant reproduce" stale-issue-message: "Please provide a way to reproduce the issue. Issues without a minimal (Tone.js-only) repro will be closed. A Tone.js-only repro helps ensure that the issue is not caused by another library." days-before-close: 7 days-before-stale: 14 - uses: actions/stale@v9 with: any-of-labels: "feature request" stale-issue-message: "Unfortunately with limited development time, not all feature requests can be tackled. If you are interested in contributing this feature, please open a PR." days-before-close: 30 days-before-stale: 90 ================================================ FILE: .github/workflows/test.yml ================================================ name: Tests on: pull_request: types: [opened, reopened, synchronize] branches: - dev - main push: branches: - dev - main jobs: test-conventional-commit: name: Validate PR title runs-on: ubuntu-latest permissions: pull-requests: read steps: - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # only on PRs if: github.event_name == 'pull_request' run-tests: name: All tests permissions: contents: read id-token: write runs-on: ubuntu-latest needs: [test-conventional-commit] env: BROWSER: chrome CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.12.0 cache: "npm" - name: Install dependencies run: npm install - name: All tests run: npm run test - name: Upload coverage uses: codecov/codecov-action@v5 with: fail_ci_if_error: false codecov_yml_path: ./.github/codecov.yml token: ${{ secrets.CODECOV_TOKEN }} test-code-examples: name: Check typedocs permissions: contents: read id-token: write runs-on: ubuntu-latest needs: [test-conventional-commit] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.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 needs: [test-conventional-commit] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.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 needs: [test-conventional-commit] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.12.0 cache: "npm" - name: Install dependencies run: npm install - name: Linting run: npm run lint - name: Spell check run: npm run spellcheck test-readme: name: Ensure that examples in the README compile permissions: contents: read id-token: write runs-on: ubuntu-latest needs: [test-conventional-commit] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.12.0 cache: "npm" - name: Install dependencies run: npm install - name: Build run: npm run build - name: Test run: npm run test:readme test-integrations: name: Test integrations permissions: contents: read id-token: write runs-on: ubuntu-latest needs: [test-conventional-commit] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Setup Nodejs uses: actions/setup-node@v4 with: node-version: 22.12.0 cache: "npm" - name: Install dependencies run: npm install - name: Build run: npm run build - name: Test run: npm run test:integrations publish: runs-on: ubuntu-latest permissions: contents: read id-token: write # make sure all the tests pass first needs: [ run-tests, test-code-examples, test-html-examples, test-lint, test-readme, test-integrations, ] # not on PRs if: github.event_name != 'pull_request' env: GITHUB_CI: true steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: 24 registry-url: "https://registry.npmjs.org" # Need to unset this in order to use OICD - name: Unset NODE_AUTH_TOKEN run: unset NODE_AUTH_TOKEN - name: Install dependencies run: npm install - name: Build run: npm run build - name: Increment version run: npm run increment - name: Publish @next # dev branch gets published with @next tag run: npm publish --tag next --provenance --access public if: ${{ github.ref == 'refs/heads/dev' }} - name: Publish @latest # main branch gets published with @latest tag run: npm publish --provenance --access public if: ${{ github.ref == 'refs/heads/main' }} ================================================ FILE: .gitignore ================================================ .DS_Store *.asd *.scssc *.sublime-workspace *.sublime-project node_modules TODO.txt wiki examples/scratch.html examples/deps/FileSaver.js examples/oscilloscope.html examples/graph.html test/performance test/mainTest.js test/Main.js test/supports.html coverage/ build/* **/dist/* examples/scratch.js examples/scratch.ts Tone/version.js Tone/version.ts Tone/index.js test/test.js docs .vscode tone.d.ts examples/scratch.ts test/integration/*/package-lock.json ================================================ FILE: CHANGELOG.md ================================================ # 15.0.x - `set` in constructor even if AudioBuffer is not from std-audio-context ([0399687](https://github.com/Tonejs/Tone.js/commit/0399687)), closes [#991](https://github.com/Tonejs/Tone.js/issues/991) - ability to mute a sequence ([ac856bc](https://github.com/Tonejs/Tone.js/commit/ac856bc)), closes [#823](https://github.com/Tonejs/Tone.js/issues/823) - Add `"type": "module"` to `package.json` ([69055ba](https://github.com/Tonejs/Tone.js/commit/69055ba)) - Add back `main` as ESM build ([8cc6e8a](https://github.com/Tonejs/Tone.js/commit/8cc6e8a)) - Add Pattern.index ([205c438](https://github.com/Tonejs/Tone.js/commit/205c438)) - Add test for duplicate events ([d5c8a25](https://github.com/Tonejs/Tone.js/commit/d5c8a25)) - adding @category definitions for docs, fixing some typos/mistakes along the way ([0e2b5b9](https://github.com/Tonejs/Tone.js/commit/0e2b5b9)) - adding createMediaElementSource ([301f8cd](https://github.com/Tonejs/Tone.js/commit/301f8cd)), closes [#756](https://github.com/Tonejs/Tone.js/issues/756) - Allow instrument and PolySynth to be scheduled to the transport stop/loop events ([954a4fc](https://github.com/Tonejs/Tone.js/commit/954a4fc)), closes [#924](https://github.com/Tonejs/Tone.js/issues/924) - allow worklet-based effects to be used with native contexts (#1131) ([f06ff17](https://github.com/Tonejs/Tone.js/commit/f06ff17)), closes [#1131](https://github.com/Tonejs/Tone.js/issues/1131) - Analyser constructor smoothing option bug fix ([afb5284](https://github.com/Tonejs/Tone.js/commit/afb5284)) - bumping standardized-audio-context version ([cbdb596](https://github.com/Tonejs/Tone.js/commit/cbdb596)) - Chebyshev order must be an integer ([4f9aece](https://github.com/Tonejs/Tone.js/commit/4f9aece)), closes [#844](https://github.com/Tonejs/Tone.js/issues/844) - correctly offset phase for each oscillator ([0ac1da5](https://github.com/Tonejs/Tone.js/commit/0ac1da5)), closes [#733](https://github.com/Tonejs/Tone.js/issues/733) - custom decay curve #1107 ([d463286](https://github.com/Tonejs/Tone.js/commit/d463286)), closes [#1107](https://github.com/Tonejs/Tone.js/issues/1107) - Don't reschedule source when offset is very small ([444d617](https://github.com/Tonejs/Tone.js/commit/444d617)), closes [#999](https://github.com/Tonejs/Tone.js/issues/999) [#944](https://github.com/Tonejs/Tone.js/issues/944) - Export remaining effect option interfaces (#1293) ([2b8039b](https://github.com/Tonejs/Tone.js/commit/2b8039b)), closes [#1293](https://github.com/Tonejs/Tone.js/issues/1293) - exporting Mono ([8b996bc](https://github.com/Tonejs/Tone.js/commit/8b996bc)), closes [#765](https://github.com/Tonejs/Tone.js/issues/765) - fix the wrong variable name 'event' in Emitter.ts ([46bd717](https://github.com/Tonejs/Tone.js/commit/46bd717)) - Garbage collect nodes used for Transport syncing ([a392067](https://github.com/Tonejs/Tone.js/commit/a392067)) - increasing attack/release above 0 to avoid distortion ([b39883e](https://github.com/Tonejs/Tone.js/commit/b39883e)), closes [#770](https://github.com/Tonejs/Tone.js/issues/770) - latest std-audio-context ([8915e1b](https://github.com/Tonejs/Tone.js/commit/8915e1b)), closes [#720](https://github.com/Tonejs/Tone.js/issues/720) - Less verbose unit types (#1181) ([9353d33](https://github.com/Tonejs/Tone.js/commit/9353d33)), closes [#1181](https://github.com/Tonejs/Tone.js/issues/1181) - Load only a single AudioWorklet ([75a802c](https://github.com/Tonejs/Tone.js/commit/75a802c)) - Memoize getTicksAtTime and getSecondsAtTime ([da73385](https://github.com/Tonejs/Tone.js/commit/da73385)) - Parse note strings with three sharps or flats. ([5cd3560](https://github.com/Tonejs/Tone.js/commit/5cd3560)) - pass in partials to LFO ([fdc7eb4](https://github.com/Tonejs/Tone.js/commit/fdc7eb4)), closes [#814](https://github.com/Tonejs/Tone.js/issues/814) - polysynth does not reschedule event if disposed ([e3a611f](https://github.com/Tonejs/Tone.js/commit/e3a611f)) - Recorder resumes if start() is called in paused state (#1266) ([ead4f21](https://github.com/Tonejs/Tone.js/commit/ead4f21)), closes [#1266](https://github.com/Tonejs/Tone.js/issues/1266) - remove implicit "stop" scheduling ([0b7a352](https://github.com/Tonejs/Tone.js/commit/0b7a352)), closes [#778](https://github.com/Tonejs/Tone.js/issues/778) - Reverb input string time (#1313) ([bf3ee91](https://github.com/Tonejs/Tone.js/commit/bf3ee91)), closes [#1313](https://github.com/Tonejs/Tone.js/issues/1313) [#1253](https://github.com/Tonejs/Tone.js/issues/1253) - Reverse Emitter off callback loop for correct removal of duplicate events ([b5f582e](https://github.com/Tonejs/Tone.js/commit/b5f582e)) - Revert "fix for AudioBufferSourceNode stop time miscalculation" ([e1c6631](https://github.com/Tonejs/Tone.js/commit/e1c6631)) - Smooth RMS values per channel in Meter ([45d2009](https://github.com/Tonejs/Tone.js/commit/45d2009)), closes [#882](https://github.com/Tonejs/Tone.js/issues/882) - throws error when polysynth is used with a non monophonic class ([4f5353e](https://github.com/Tonejs/Tone.js/commit/4f5353e)), closes [#939](https://github.com/Tonejs/Tone.js/issues/939) - use now() instead of currentTime ([9e9b3d2](https://github.com/Tonejs/Tone.js/commit/9e9b3d2)) - Use reciprocal of tempo when syncing time signals to Transport ([64c8a29](https://github.com/Tonejs/Tone.js/commit/64c8a29)), closes [#879](https://github.com/Tonejs/Tone.js/issues/879) - Using web-test-runner for tests, updating import paths (#1242) ([aaf880c](https://github.com/Tonejs/Tone.js/commit/aaf880c)), closes [#1242](https://github.com/Tonejs/Tone.js/issues/1242) - warn if event is scheduled without using the scheduled time. ([6dd22e7](https://github.com/Tonejs/Tone.js/commit/6dd22e7)), closes [#959](https://github.com/Tonejs/Tone.js/issues/959) - fix: load base64 encoded sounds when baseUrl is not empty ([a771811](https://github.com/Tonejs/Tone.js/commit/a771811)), closes [#898](https://github.com/Tonejs/Tone.js/issues/898) - fix: loading non relative URLs ([f7bdff0](https://github.com/Tonejs/Tone.js/commit/f7bdff0)) - feat: sub-tick scheduling ([33e14d0](https://github.com/Tonejs/Tone.js/commit/33e14d0)) ### BREAKING CHANGES - Deprecating singleton variables, use singleton getter instead (#1233) ([3d42017](https://github.com/Tonejs/Tone.js/commit/3d42017)), closes [#1233](https://github.com/Tonejs/Tone.js/issues/1233) - Removing double-encoding of urls (#1254) ([de086f5](https://github.com/Tonejs/Tone.js/commit/de086f5)), closes [#1254](https://github.com/Tonejs/Tone.js/issues/1254) # 14.7.x ### Features - **Converted to typescript!!!** - adding AudioWorkletNode constructors to Context ([f7bdd75](https://github.com/Tonejs/Tone.js/commit/f7bdd75)) - adding ability to get the frequency at the FFT index ([22cecdc](https://github.com/Tonejs/Tone.js/commit/22cecdc281c8076c054affaef9dc422665acda2e)) - adding AudioWorkletNode constructors to Context ([f7bdd75](https://github.com/Tonejs/Tone.js/commit/f7bdd7528fa9549740dc514df6308303c060e091)) - adding BiquadFilter ([75617d3](https://github.com/Tonejs/Tone.js/commit/75617d341fe44ca5d332ea4e547f07c266a54753)), closes [#686](https://github.com/Tonejs/Tone.js/issues/686) - adding linting to jsdocs ([10ef513](https://github.com/Tonejs/Tone.js/commit/10ef513)) - adding send/receive to Channel ([703f27a](https://github.com/Tonejs/Tone.js/commit/703f27a)) - Adding triggerRelease to PluckSynth ([04405af](https://github.com/Tonejs/Tone.js/commit/04405af)) - Can set the parameter after constructing Param ([23ca0f9](https://github.com/Tonejs/Tone.js/commit/23ca0f9)) - adding onerror to Sampler ([7236600](https://github.com/Tonejs/Tone.js/commit/7236600182d336d6598f86d7d7afe8761e733774)), closes [#605](https://github.com/Tonejs/Tone.js/issues/605) - Chorus extends StereoFeedbackEffect ([a28f1af](https://github.com/Tonejs/Tone.js/commit/a28f1af)), closes [#575](https://github.com/Tonejs/Tone.js/issues/575) - Convolver is just a wrapper around the ConvolverNode, no longer an effect ([1668dec](https://github.com/Tonejs/Tone.js/commit/1668dec)) - Get an oscillator wave as an array ([9ad519e](https://github.com/Tonejs/Tone.js/commit/9ad519e)) - OfflineContext returns a ToneAudioBuffer ([889dafa](https://github.com/Tonejs/Tone.js/commit/889dafa)) - OfflineContext yields thread every second of audio rendered ([1154470](https://github.com/Tonejs/Tone.js/commit/1154470)), closes [#436](https://github.com/Tonejs/Tone.js/issues/436) - Renaming TransportTimelineSignal to SyncedSignal ([86853fb](https://github.com/Tonejs/Tone.js/commit/86853fb)) - es6 output ([e5d28ba](https://github.com/Tonejs/Tone.js/commit/e5d28baa5f02c19a6f1c8c50c99e98bd1551d15b)) - Render a segment of the envelope as an array ([fc5b6f7](https://github.com/Tonejs/Tone.js/commit/fc5b6f7)) - testing examples in jsdocs ([e306319](https://github.com/Tonejs/Tone.js/commit/e306319)) - Wrapper around the AudioWorkletNode ([2ee8cb1](https://github.com/Tonejs/Tone.js/commit/2ee8cb1)) - Input/Outputs are no longer arrays. - simplifies connect/disconnect logic greatly. Simplifies API to just have clearly named inputs/outputs instead of overloading input/output connect numbers - Using "Destination" instead of "Master" for output - More consistent with Web Audio API - FrequencyShifter - thanks @Foaly - PolySynth does not require a polyphony value. - Voice allocation and disposing is done automatically based on demand. - MetalSynth and MembraneSynth extends Monophonic enabling them to be used in PolySynth - OnePoleFilter is a 6b-per-octave lowpass or highpass filter - Using OnePoleFilter in PluckSynth and LowpassCombFilter - latencyHint is now set in constructor ([ba8e82b](https://github.com/Tonejs/Tone.js/commit/ba8e82b1ca8a841a23d6e774641916019c37cc92)), closes [#658](https://github.com/Tonejs/Tone.js/issues/658) - meter output can be normalRange in addition to decibels ([2625a13](https://github.com/Tonejs/Tone.js/commit/2625a134b62af117c1c525a4e631e4e52b25ba90)) - option to pass in the number of input channels to Panner ([d966735](https://github.com/Tonejs/Tone.js/commit/d966735bd97bddc70039bce5a48f26413054eddc)), closes [#609](https://github.com/Tonejs/Tone.js/issues/609) ### BREAKING CHANGES - TransportTimelineSignal renamed SyncedSignal - Master renamed Destination - Buffer renamed ToneAudioBuffer - Buffer.on("loaded") is should now use: `Tone.loaded(): Promise` - Removing bower ([71c8b3b](https://github.com/Tonejs/Tone.js/commit/71c8b3bbb96e45cfc4aa2cce8a2d8c61a092c91e)), closes [#197](https://github.com/Tonejs/Tone.js/issues/197) - Removing Ctrl classes ([51d06bd](https://github.com/Tonejs/Tone.js/commit/51d06bd9873b2f1936a3169930f9696f1ccfb845)) - `Players.get(name: string)` is renamed to `Players.player(name: string)` # 13.8.25 - Moving to common.js-style code ### BREAKING CHANGES - AudioNode.prototype.connect is no longer overwritten. This means that you can no longer connect native nodes to Tone.js Nodes. - Tone.connect(srcNode, destNode, [ouputNum], [inputNum]) is the way to connect native Web Audio nodes with Tone.js nodes. # 13.4.9 - Updating semantic versioning to be more in line with other [semvers](https://semver.org/). Now version is 13.x.x - logging full version - Added Object notation for Tone.TimeBase and classes that extend it. - i.e. Tone.Time({'4n' : 1, '8t' : 2}) - Replacement for deprecated expression strings. - Tone.Meter uses RMS instead of peak (thanks [@Idicious](https://github.com/Idicious)) - Tone.Sampler supports polyphonic syntax (thanks [@zfan40](https://github.com/zfan40)) - Building files with [webpack](https://webpack.js.org/) - Follower/Gate uses a single "smoothing" value instead of separate attacks and releases - Changing references to `window` allowing it to not throw error in node context - Testing examples - Tone.Channel combines Tone.PanVol with Tone.Solo. - Removing require.html example. - adding `partialCount` and `baseType` to Oscillator classes, helps with getting/setting complex types. # r12 - Consolidating all shims into [shim folder](https://github.com/Tonejs/Tone.js/tree/dev/Tone/shim) - Using ConstantSourceNode in Signal when available - switching to eslint from jshint - Running [CI tests](https://travis-ci.org/Tonejs/Tone.js/) on Firefox, Chrome (latest and canary) and Safari (latest and version 9). - [Tone.Reverb](https://tonejs.github.io/docs/Reverb) is a convolution-based stereo reverb. [Example](https://tonejs.github.io/examples/#reverb). - Optimizing basic Oscillator types and many Signal use-case - Optimizing basic connection use-case of Tone.Signal where one signal is controlling another signal - Testing rendered output against an existing audio file for continuity and consistency - Optimizing triggerAttack/Release by starting/stopping oscillators when not playing - [TickSource](https://tonejs.github.io/docs/TickSource) (used in Clock and Player) tracks the elapsed ticks - Improved precision of tracking ticks in Transport and Clock - `Player.position` returns the playback position of the AudioBuffer accounting for any playbackRate changes - Removing `retrigger` option with Tone.Player. Tone.BufferSource should be used if retriggering is desired. **BREAKING CHANGES:** - Tone.TimeBase and all classes that extend it not longer support string expressions. RATIONALE : _ Since all classes implement `valueOf`, expressions can be composed in JS instead of as strings _ e.g. `Time('4n') * 2 + Time('3t')` instead of `Time('4n * 2 + 3t')` \* this change greatly simplifies the code and is more performant # r11 - [Code coverage](https://coveralls.io/github/Tonejs/Tone.js) analysis - [Dev build](https://tonejs.github.io/build/dev/Tone.js) with each successful commit - [Versioned docs](https://tonejs.github.io/docs/Tone) plus a [dev build of the docs](https://tonejs.github.io/docs/dev/Tone) on successful commits - [Tone.AudioNode](https://tonejs.github.io/docs/AudioNode) is base class for all classes which generate or process audio - [Tone.Sampler](https://tonejs.github.io/docs/Sampler) simplifies creating multisampled instruments - [Tone.Solo](https://tonejs.github.io/docs/Solo) makes it easier to mute/solo audio - [Mixer](https://tonejs.github.io/examples/#mixer) and [sampler](https://tonejs.github.io/examples/#sampler) examples - Making type-checking methods static - [Tone.TransportTimelineSignal](https://tonejs.github.io/docs/TransportTimelineSignal) is a signal which can be scheduled along the Transport - [Tone.FFT](https://tonejs.github.io/docs/FFT) and [Tone.Waveform](https://tonejs.github.io/docs/Waveform) abstract Tone.Analyser - [Tone.Meter](https://tonejs.github.io/docs/Meter) returns decibels - [Tone.Envelope](https://tonejs.github.io/docs/Envelope) uses exponential approach instead of exponential curve for decay and release curves - [Tone.BufferSource](https://tonejs.github.io/docs/BufferSource) fadeIn/Out can be either "linear" or "exponential" curve # r10 - Tone.Context wraps AudioContext - Tone.OfflineContext wraps OfflineAudioContext - Tone.Offline: method for rendering audio offline - Rewriting tests with Tone.Offline - Optimizing Tone.Draw to only loop when events are scheduled: [#194](https://github.com/Tonejs/Tone.js/issues/194) - Time.eval->valueOf which takes advantage of build-in primitive evaluation [#205](https://github.com/Tonejs/Tone.js/issues/205) - [Offline example](https://tonejs.github.io/examples/#offline) # r9 - Tone.Clock performance and lookAhead updates. - Tone.Transport.lookAhead = seconds|'playback'|'interactive'|'balanced' - Convolver.load and Player.load returns Promise - Tone.ExternalInput -> Tone.UserMedia, simplified API, open() returns Promise. - Tone.Draw for animation-frame synced drawing - Compressor Parameters are now Tone.Params - Bug fixes # r8 - Transport.seconds returns the progress in seconds. - Buffer.from/toArray, Float32Array <-> Buffer conversions - Buffer.slice(start, end) slices and returns a subsection of the Buffer - Source.sync now syncs all subsequent calls to `start` and `stop` to the TransportTime instead of the AudioContext time. - e.g. source.sync().start(0).stop(0.8); //plays source between 0 and 0.8 of the Transport - Transport.on("start" / "stop") callbacks are invoked just before the event. - Param can accept an LFO description in the constructor or .value - e.g. param.value = {min : 10, max : 20, frequency : 0.4} - Time.TimeBase has clone/copy methods. - Tone.Buffer.prototype.load returns Promise - Using Tone.Delay and Tone.Gain everywhere - Patch for Chrome 53+ issue of not correctly scheduling AudioParams with setValueAtTime - Panner3D and Tone.Listener wrap native PannerNode and AudioListener to give 3D panning ability. # r7 - MetalSynth creates metallic, cymbal sounds - DrumSynth -> MembraneSynth - FMOscillator, AMOscillator types - FatOscillator creates multiple oscillators and detunes them slightly - FM, AM, Fat Oscillators incorporated into OmniOscillator - Simplified FM and AM Synths and APIs - Panner.pan is between -1,1 like the StereoPannerNode - Pruned away unused (or little used) Signal classes. - All this functionality will be available when the AudioWorkerNode is introduced. - Clock uses Web Workers instead of requestAnimationFrame which allows it to run in the background. - Removed `startMobile`. Using [StartAudioContext](https://github.com/tambien/StartAudioContext) in examples. - Automated test runner using [Travis CI](https://travis-ci.org/Tonejs/Tone.js/) - Simplified NoiseSynth by removing filter and filter envelope. - Added new timing primitive types: Time, Frequency, TransportTime. - Switching parameter position of type and size in Tone.Analyser - Tone.Meter uses Tone.Analyser instead of ScriptProcessorNode. - Tone.Envelope has 5 new attack/release curves: "sine", "cosine", "bounce", "ripple", "step" - Renamed Tone.SimpleSynth -> Tone.Synth - Tone.Buffers combines multiple buffers - Tone.BufferSource a low-level wrapper, and Tone.MultiPlayer which is good for multisampled instruments. - Tone.GrainPlayer: granular synthesis buffer player. - Simplified Sampler DEPRECATED: - Removed SimpleFM and SimpleAM # r6 - Added PitchShift and Vibrato Effect. - Added Timeline/TimelineState/TimelineSignal which keeps track of all scheduled state changes. - Clock uses requestAnimationFrame instead of ScriptProcessorNode - Removed `onended` event from Tone.Source - Refactored tests into individual files. - Renamed some Signal methods: `exponentialRampToValueNow`->`exponentialRampToValue`, `setCurrentValueNow`->`setRampPoint` - LFO no longer starts at bottom of cycle. Starts at whatever phase it's set at. - Transport is an event emitter. triggers events on "start", "stop", "pause", and "loop". - Oscillator accepts a "partials" array. - Microphone inherits from ExternalInput which is generalized for different inputs. - New scheduling methods on Transport - `schedule`, `scheduleOnce`, and `scheduleRepeat`. - Tone.Gain and Tone.Delay classes wrap the native Web Audio nodes. - Moved [MidiToScore](https://github.com/Tonejs/MidiConvert) and [TypeScript](https://github.com/Tonejs/TypeScript) definitions to separate repos. - Tone.Param wraps the native AudioParam and allows for unit conversion. - Quantization with Transport.quantize and using "@" in any Time. [Read more](https://github.com/Tonejs/Tone.js/wiki/Time). - Control-rate generators for value interpolation, patterns, random numbers, and markov chains. - schedulable musical events: Tone.Event, Tone.Loop, Tone.Part, Tone.Pattern, Tone.Sequence. - Player's playbackRate is now a signal and Noise includes a playbackRate signal. - All filterEnvelopes use new Tone.FrequencyEnvelope with frequency units and `baseFrequency` and `octaves` instead of `min` and `max`. - Phaser uses "octaves" instead of "depth" to be more consistent across the whole Tone.js API. - Presets now have [their own repo](https://github.com/Tonejs/Presets) DEPRECATED: - `setTimeout`, `setInterval`, `setTimeline` in favor of new `schedule`, `scheduleOnce`, and `scheduleRepeat`. - Tone.Signal no longer takes an AudioParam in the first argument. Use Tone.Param instead. - Tone.Buffer.onload/onprogress/onerror is deprecated. Use `Tone.Buffer.on("load", callback)` instead. # r5 - reverse buffer for Player and Sampler. - Tone.Volume for simple volume control in Decibels. - Panner uses StereoPannerNode when available. - AutoFilter and Tremolo effects. - Made many attributes read-only. preventing this common type of error: `oscillator.frequency = 200` when it should be `oscillator.frequency.value = 200`. - Envelope supports "linear" and "exponential" attack curves. - Renamed Tone.EQ -> Tone.EQ3. - Tone.DrumSynth makes kick and tom sounds. - Tone.MidSideCompressor and Tone.MidSideSplit/Tone.MidSideMerge - Tone.Oscillator - can specify the number of partials in the type: i.e. "sine10", "triangle3", "square4", etc. - mute/unmute the master output: `Tone.Master.mute = true`. - 3 new simplified synths: SimpleSynth, SimpleAM and SimpleFM - `harmonicity` is a signal-rate value for all instruments. - expose Q in Phaser. - unit conversions using Tone.Type for signals and LFO. - [new docs](http://tonejs.org/docs) - [updated examples](http://tonejs.org/examples) # r4 - `toFrequency` accepts notes by name (i.e. `"C4"`) - Envelope no longer accepts exponential scaling, only Tone.ScaledEnvelope - Buffer progress and load events which tracks the progress of all downloads - Buffer only accepts a single url - Sampler accepts multiple samples as an object. - `setPitch` in sampler -> `setNote` - Deprecated MultiSampler - use Sampler with PolySynth instead - Added [cdn](http://cdn.tonejs.org/latest/Tone.min.js) - please don't use for production code - Renamed DryWet to CrossFade - Functions return `this` to allow for chaining. i.e. `player.toMaster().start(2)`. - Added `units` to Signal class which allows signals to be set in terms of Tone.Time, Tone.Frequency, Numbers, or Decibels. - Replaced set/get method with ES5 dot notation. i.e. `player.setVolume(-10)` is now `player.volume.value = -10`. To ramp the volume use either `player.volume.linearRampToValueNow(-10, "4n")`, or the new `rampTo` method which automatically selects the ramp (linear|exponential) based on the type of data. - set/get methods for all components - syncSignal and unsyncSignal moved from Signal to Transport - Add/Multiply/Subtract/Min/Max/GreaterThan/LessThan all extend Tone.Signal which allows them to be scheduled and automated just like Tone.Signal. - Deprecated Tone.Divide and Tone.Inverse. They were more complicated than they were useful. BREAKING CHANGES: The API has been changed consistently to use `.attribute` for getting and setting instead of `getAttribute` and `setAttribute` methods. The reasoning for this is twofold: firstly, Tone.Signal attributes were previously limited in their scheduling capabilities when set through a setter function. For exactly, it was not possible to do a setValueAtTime on the `bpm` of the Transport. Secondly, the new EcmaScript 5 getter/setter approach resembles the Web Audio API much more closely, which will make intermixing the two APIs even easier. If you're using Sublime Text, one way to transition from the old API to the new one is with a regex find/replace: find `Tone.Transport.setBpm\((\d+)\)` and replace it with `Tone.Transport.bpm.value = $1`. Or if setBpm was being invoked with a rampTime: find `Tone.Transport.setBpm\((\d+)\, (\d+)\)` and replace it with `Tone.Transport.bpm.rampTo($1, $2)`. # r3 Core Change: - Swing parameter on Transport - Player loop positions stay in tempo-relative terms even with tempo changes - Envelope ASDR stay in tempo-relative terms even with tempo changes - Modified build script to accommodate using requirejs with build and minified version Signal Processing: - Tone.Expr: signal processing expression parser for Tone.Signal math - All signal binary operators accept two signals as inputs - Deprecated Tone.Threshold - new class Tone.GreaterThanZero - NOT, OR, AND, and IfThenElse signal logic operators - Additional signal classes: Inverse, Divide, Pow, AudioToGain, Subtract - Scale no longer accepts input min/max. Assumes [0,1] range. - Normalize class if scaling needs to happen from other input ranges - WaveShaper function wraps the WaveShaperNode Effects: - Distortion and Chebyshev distortion effects - Compressor and MultibandCompressor - MidSide effect type and StereoWidener - Convolver effect and example Synths: - Setters on PluckSynth and PulseOscillator - new PWMOscillator - OmniOscillator which combines PWMOscillator, Oscillator, and PulseOscillator into one - NoiseSynth # r2 - PluckSynth - Karplus-Strong Plucked String modeling synth - Freeverb - John Chowning Reverb (JCReverb) - LowpassCombFilter and FeedbackCombFilter - Sampler with pitch control - Clock tick callback is out of the audio thread using setTimeout - Optimized Tone.Modulo - Tests run using OfflineRenderingContext - Fixed Transport bug where timeouts/intervals and timelines were on a different tick counter - AmplitudeEnvelope + triggerAttackDecay on Envelope - Instruments inherit from Tone.Instrument base-class - midi<-->note conversions # r1 - First! ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2014-2025 Yotam Mann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 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. - [API](https://tonejs.github.io/docs/) - [Examples](https://tonejs.github.io/examples/) # Installation 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 // Install the latest stable version npm install tone@next // Or, alternatively, use the 'next' version ``` Add Tone.js to a project using the JavaScript `import` syntax: ```js import * as Tone from "tone"; ``` 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. ```html ``` # Hello Tone ```javascript //create a synth and connect it to the main output (your speakers) const synth = new Tone.Synth().toDestination(); //play a middle 'C' for the duration of an 8th note synth.triggerAttackRelease("C4", "8n"); ``` ## Tone.Synth `Tone.Synth` is a basic synthesizer with a single oscillator and an ADSR envelope. ### triggerAttack / triggerRelease `triggerAttack` starts the note (the amplitude is rising), and `triggerRelease` is when the amplitude is going back to 0 (i.e. **note off**). ```javascript const synth = new Tone.Synth().toDestination(); const now = Tone.now(); // trigger the attack immediately synth.triggerAttack("C4", now); // wait one second before triggering the release synth.triggerRelease(now + 1); ``` ### triggerAttackRelease `triggerAttackRelease` is a combination of `triggerAttack` and `triggerRelease` The first argument to the note which can either be a frequency in hertz (like `440`) or as "pitch-octave" notation (like `"D#2"`). The second argument is the duration that the note is held. This value can either be in seconds, or as a [tempo-relative value](https://github.com/Tonejs/Tone.js/wiki/Time). The third (optional) argument of `triggerAttackRelease` is _when_ along the AudioContext time the note should play. It can be used to schedule events in the future. ```javascript const synth = new Tone.Synth().toDestination(); const now = Tone.now(); synth.triggerAttackRelease("C4", "8n", now); synth.triggerAttackRelease("E4", "8n", now + 0.5); synth.triggerAttackRelease("G4", "8n", now + 1); ``` ## Time Web Audio has advanced, sample accurate scheduling capabilities. The AudioContext time is what the Web Audio API uses to schedule events, starts at 0 when the page loads and counts up in **seconds**. `Tone.now()` gets the current time of the AudioContext. ```javascript setInterval(() => console.log(Tone.now()), 100); ``` Tone.js abstracts away the AudioContext time. Instead of defining all values in seconds, any method which takes time as an argument can accept a number or a string. For example `"4n"` is a quarter-note, `"8t"` is an eighth-note triplet, and `"1m"` is one measure. [Read about Time encodings](https://github.com/Tonejs/Tone.js/wiki/Time). # Starting Audio **IMPORTANT**: Browsers will not play _any_ audio until a user clicks something (like a play button). Run your Tone.js code only after calling `Tone.start()` from a event listener which is triggered by a user action such as "click" or "keydown". `Tone.start()` returns a promise, the audio will be ready only after that promise is resolved. Scheduling or playing audio before the AudioContext is running will result in silence or incorrect scheduling. ```javascript //attach a click listener to a play button document.querySelector("button")?.addEventListener("click", async () => { await Tone.start(); console.log("audio is ready"); }); ``` # Scheduling ## Transport `Tone.getTransport()` returns the main timekeeper. Unlike the AudioContext clock, it can be started, stopped, looped and adjusted on the fly. You can think of it like the arrangement view in a Digital Audio Workstation. Multiple events and parts can be arranged and synchronized along the Transport. `Tone.Loop` is a simple way to create a looped callback that can be scheduled to start and stop. ```javascript // create two monophonic synths const synthA = new Tone.FMSynth().toDestination(); const synthB = new Tone.AMSynth().toDestination(); //play a note every quarter-note const loopA = new Tone.Loop((time) => { synthA.triggerAttackRelease("C2", "8n", time); }, "4n").start(0); //play another note every off quarter-note, by starting it "8n" const loopB = new Tone.Loop((time) => { synthB.triggerAttackRelease("C4", "8n", time); }, "4n").start("8n"); // all loops start when the Transport is started Tone.getTransport().start(); // ramp up to 800 bpm over 10 seconds Tone.getTransport().bpm.rampTo(800, 10); ``` Since Javascript callbacks are **not precisely timed**, the sample-accurate time of the event is passed into the callback function. **Use this time value to schedule the events**. # Instruments There are numerous synths to choose from including `Tone.FMSynth`, `Tone.AMSynth` and `Tone.NoiseSynth`. All of these instruments are **monophonic** (single voice) which means that they can only play one note at a time. To create a **polyphonic** synthesizer, use `Tone.PolySynth`, which accepts a monophonic synth as its first parameter and automatically handles the note allocation so you can pass in multiple notes. The API is similar to the monophonic synths, except `triggerRelease` must be given a note or array of notes. ```javascript const synth = new Tone.PolySynth(Tone.Synth).toDestination(); const now = Tone.now(); synth.triggerAttack("D4", now); synth.triggerAttack("F4", now + 0.5); synth.triggerAttack("A4", now + 1); synth.triggerAttack("C5", now + 1.5); synth.triggerAttack("E5", now + 2); synth.triggerRelease(["D4", "F4", "A4", "C5", "E5"], now + 4); ``` # Samples Sound generation is not limited to synthesized sounds. You can also load a sample and play that back in a number of ways. `Tone.Player` is one way to load and play back an audio file. ```javascript const player = new Tone.Player( "https://tonejs.github.io/audio/berklee/gong_1.mp3" ).toDestination(); Tone.loaded().then(() => { player.start(); }); ``` `Tone.loaded()` returns a promise which resolves when _all_ audio files are loaded. It's a helpful shorthand instead of waiting on each individual audio buffer's `onload` event to resolve. ## Tone.Sampler Multiple samples can also be combined into an instrument. If you have audio files organized by note, `Tone.Sampler` will pitch shift the samples to fill in gaps between notes. So for example, if you only have every 3rd note on a piano sampled, you could turn that into a full piano sample. Unlike the other synths, Tone.Sampler is polyphonic so doesn't need to be passed into Tone.PolySynth ```javascript const sampler = new Tone.Sampler({ urls: { C4: "C4.mp3", "D#4": "Ds4.mp3", "F#4": "Fs4.mp3", A4: "A4.mp3", }, release: 1, baseUrl: "https://tonejs.github.io/audio/salamander/", }).toDestination(); Tone.loaded().then(() => { sampler.triggerAttackRelease(["Eb4", "G4", "Bb4"], 4); }); ``` # Effects In the above examples, the sources were always connected directly to the `Destination`, but the output of the synth could also be routed through one (or more) effects before going to the speakers. ```javascript const player = new Tone.Player({ url: "https://tonejs.github.io/audio/berklee/gurgling_theremin_1.mp3", loop: true, autostart: true, }); //create a distortion effect const distortion = new Tone.Distortion(0.4).toDestination(); //connect a player to the distortion player.connect(distortion); ``` The connection routing is flexible, connections can run serially or in parallel. ```javascript const player = new Tone.Player({ url: "https://tonejs.github.io/audio/drum-samples/loops/ominous.mp3", autostart: true, }); const filter = new Tone.Filter(400, "lowpass").toDestination(); const feedbackDelay = new Tone.FeedbackDelay(0.125, 0.5).toDestination(); // connect the player to the feedback delay and filter in parallel player.connect(filter); player.connect(feedbackDelay); ``` Multiple nodes can be connected to the same input enabling sources to share effects. `Tone.Gain` is useful utility node for creating complex routing. # Signals Like the underlying Web Audio API, Tone.js is built with audio-rate signal control over nearly everything. This is a powerful feature which allows for sample-accurate synchronization and scheduling of parameters. `Signal` properties have a few built in methods for creating automation curves. For example, the `frequency` parameter on `Oscillator` is a Signal so you can create a smooth ramp from one frequency to another. ```javascript const osc = new Tone.Oscillator().toDestination(); // start at "C4" osc.frequency.value = "C4"; // ramp to "C2" over 2 seconds osc.frequency.rampTo("C2", 2); // start the oscillator for 2 seconds osc.start().stop("+3"); ``` # AudioContext Tone.js creates an AudioContext when it loads and shims it for maximum browser compatibility using [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context). The AudioContext can be accessed at `Tone.getContext`. Or set your own AudioContext using `Tone.setContext(audioContext)`. # MIDI To use MIDI files, you'll first need to convert them into a JSON format which Tone.js can understand using [Midi](https://tonejs.github.io/Midi/). # Performance Tone.js makes extensive use of the native Web Audio Nodes such as the GainNode and WaveShaperNode for all signal processing, which enables Tone.js to work well on both desktop and mobile browsers. [This wiki](https://github.com/Tonejs/Tone.js/wiki/Performance) article has some suggestions related to performance for best practices. # Testing Tone.js runs an extensive test suite using [mocha](https://mochajs.org/) and [chai](http://chaijs.com/) with nearly 100% coverage. Passing builds on the 'dev' branch are published on npm as `tone@next`. # Contributing There are many ways to contribute to Tone.js. Check out [this wiki](https://github.com/Tonejs/Tone.js/wiki/Contributing) if you're interested. # References and Inspiration - [Many of Chris Wilson's Repositories](https://github.com/cwilso) - [Many of Mohayonao's Repositories](https://github.com/mohayonao) - [The Spec](http://webaudio.github.io/web-audio-api/) - [Sound on Sound - Synth Secrets](http://www.soundonsound.com/sos/may99/articles/synthsec.htm) - [Miller Puckette - Theory and Techniques of Electronic Music](http://msp.ucsd.edu/techniques.htm) - [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context) ================================================ FILE: Tone/classes.ts ================================================ export * from "./component/index.js"; export * from "./core/index.js"; export * from "./effect/index.js"; export * from "./event/index.js"; export * from "./instrument/index.js"; export * from "./signal/index.js"; export * from "./source/index.js"; ================================================ FILE: Tone/component/analysis/Analyser.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Noise } from "../../source/Noise.js"; import { Analyser } from "./Analyser.js"; describe("Analyser", () => { BasicTests(Analyser); it("can get and set properties", () => { const anl = new Analyser(); anl.set({ size: 32, smoothing: 0.2, }); const values = anl.get(); expect(values.size).to.equal(32); expect(values.smoothing).to.equal(0.2); anl.dispose(); }); it("can correctly set the size", () => { const anl = new Analyser("fft", 512); expect(anl.size).to.equal(512); anl.size = 1024; expect(anl.size).to.equal(1024); anl.dispose(); }); it("can run fft analysis", () => { const anl = new Analyser("fft", 512); const analysis = anl.getValue(); expect(analysis.length).to.equal(512); analysis.forEach((val) => { expect(val).is.lessThan(0); }); anl.dispose(); }); it("can run waveform analysis", (done) => { const noise = new Noise(); const anl = new Analyser("waveform", 256); noise.connect(anl); noise.start(); setTimeout(() => { const analysis = anl.getValue(); expect(analysis.length).to.equal(256); analysis.forEach((val) => { expect(val).is.within(-1, 1); }); anl.dispose(); noise.dispose(); done(); }, 300); }); it("throws an error if an invalid type is set", () => { const anl = new Analyser("fft", 512); expect(() => { // @ts-ignore anl.type = "invalid"; }).to.throw(Error); anl.dispose(); }); it("can do multichannel analysis", () => { const anl = new Analyser({ type: "waveform", channels: 2, size: 512, }); expect(anl.getValue().length).to.equal(2); expect((anl.getValue()[0] as Float32Array).length).to.equal(512); anl.dispose(); }); }); ================================================ FILE: Tone/component/analysis/Analyser.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { NormalRange, PowerOfTwo } from "../../core/type/Units.js"; import { assert, assertRange } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Split } from "../channel/Split.js"; export type AnalyserType = "fft" | "waveform"; export interface AnalyserOptions extends ToneAudioNodeOptions { size: PowerOfTwo; type: AnalyserType; smoothing: NormalRange; channels: number; } /** * Wrapper around the native Web Audio's [AnalyserNode](http://webaudio.github.io/web-audio-api/#idl-def-AnalyserNode). * Extracts FFT or Waveform data from the incoming signal. * @category Component */ export class Analyser extends ToneAudioNode { readonly name: string = "Analyser"; readonly input: InputNode; readonly output: OutputNode; /** * The analyser node. */ private _analyzers: AnalyserNode[] = []; /** * Input and output are a gain node */ private _gain: Gain; /** * The channel splitter node */ private _split: Split; /** * The analysis type */ private _type!: AnalyserType; /** * The buffer that the FFT data is written to */ private _buffers: Float32Array[] = []; /** * @param type The return type of the analysis, either "fft", or "waveform". * @param size The size of the FFT. This must be a power of two in the range 16 to 16384. */ constructor(type?: AnalyserType, size?: number); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Analyser.getDefaults(), arguments, ["type", "size"] ); super(options); this.input = this.output = this._gain = new Gain({ context: this.context }); this._split = new Split({ context: this.context, channels: options.channels, }); this.input.connect(this._split); assertRange(options.channels, 1); // create the analyzers for (let channel = 0; channel < options.channels; channel++) { this._analyzers[channel] = this.context.createAnalyser(); this._split.connect(this._analyzers[channel], channel, 0); } // set the values initially this.size = options.size; this.type = options.type; this.smoothing = options.smoothing; } static getDefaults(): AnalyserOptions { return Object.assign(ToneAudioNode.getDefaults(), { size: 1024, smoothing: 0.8, type: "fft" as AnalyserType, channels: 1, }); } /** * Run the analysis given the current settings. If {@link channels} = 1, * it will return a Float32Array. If {@link channels} > 1, it will * return an array of Float32Arrays where each index in the array * represents the analysis done on a channel. */ getValue(): Float32Array | Float32Array[] { this._analyzers.forEach((analyser, index) => { const buffer = this._buffers[index]; if (this._type === "fft") { analyser.getFloatFrequencyData(buffer); } else if (this._type === "waveform") { analyser.getFloatTimeDomainData(buffer); } }); if (this.channels === 1) { return this._buffers[0]; } else { return this._buffers; } } /** * The size of analysis. This must be a power of two in the range 16 to 16384. */ get size(): PowerOfTwo { return this._analyzers[0].frequencyBinCount; } set size(size: PowerOfTwo) { this._analyzers.forEach((analyser, index) => { analyser.fftSize = size * 2; this._buffers[index] = new Float32Array(size); }); } /** * The number of channels the analyser does the analysis on. Channel * separation is done using {@link Split} */ get channels(): number { return this._analyzers.length; } /** * The analysis function returned by analyser.getValue(), either "fft" or "waveform". */ get type(): AnalyserType { return this._type; } set type(type: AnalyserType) { assert( type === "waveform" || type === "fft", `Analyser: invalid type: ${type}` ); this._type = type; } /** * 0 represents no time averaging with the last analysis frame. */ get smoothing(): NormalRange { return this._analyzers[0].smoothingTimeConstant; } set smoothing(val: NormalRange) { this._analyzers.forEach((a) => (a.smoothingTimeConstant = val)); } /** * Clean up. */ dispose(): this { super.dispose(); this._analyzers.forEach((a) => a.disconnect()); this._split.dispose(); this._gain.dispose(); return this; } } ================================================ FILE: Tone/component/analysis/DCMeter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { DCMeter } from "./DCMeter.js"; describe("DCMeter", () => { BasicTests(DCMeter); context("DCMetering", () => { it("passes the audio through", () => { return PassAudio((input) => { const meter = new DCMeter().toDestination(); input.connect(meter); }); }); it("can get the rms level of the incoming signal", (done) => { const meter = new DCMeter(); const osc = new Signal(2).connect(meter); setTimeout(() => { expect(meter.getValue()).to.be.closeTo(2, 0.1); meter.dispose(); osc.dispose(); done(); }, 400); }); }); }); ================================================ FILE: Tone/component/analysis/DCMeter.ts ================================================ import { optionsFromArguments } from "../../core/util/Defaults.js"; import { MeterBase, MeterBaseOptions } from "./MeterBase.js"; export type DCMeterOptions = MeterBaseOptions; /** * DCMeter gets the raw value of the input signal at the current time. * @see {@link Meter}. * * @example * const meter = new Tone.DCMeter(); * const mic = new Tone.UserMedia(); * mic.open(); * // connect mic to the meter * mic.connect(meter); * // the current level of the mic * const level = meter.getValue(); * @category Component */ export class DCMeter extends MeterBase { readonly name: string = "DCMeter"; constructor(options?: Partial); constructor() { super(optionsFromArguments(DCMeter.getDefaults(), arguments)); this._analyser.type = "waveform"; this._analyser.size = 256; } /** * Get the signal value of the incoming signal */ getValue(): number { const value = this._analyser.getValue() as Float32Array; return value[0]; } } ================================================ FILE: Tone/component/analysis/FFT.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Noise } from "../../source/Noise.js"; import { FFT } from "./FFT.js"; describe("FFT", () => { BasicTests(FFT); it("can get and set properties", () => { const fft = new FFT(); fft.set({ size: 128, smoothing: 0.4, }); const values = fft.get(); expect(values.size).to.equal(128); expect(values.smoothing).to.equal(0.4); fft.dispose(); }); it("can correctly set the size", () => { const fft = new FFT(512); expect(fft.size).to.equal(512); fft.size = 1024; expect(fft.size).to.equal(1024); fft.dispose(); }); it("can set the smoothing", () => { const fft = new FFT(512); fft.smoothing = 0.2; expect(fft.smoothing).to.equal(0.2); fft.dispose(); }); it("can get the frequency values of each index of the return array", () => { const fft = new FFT(32); expect(fft.getFrequencyOfIndex(0)).to.be.closeTo(0, 1); expect(fft.getFrequencyOfIndex(16)).to.be.closeTo( fft.context.sampleRate / 4, 1 ); fft.dispose(); }); it("can run waveform analysis", (done) => { const noise = new Noise(); const fft = new FFT(256); noise.connect(fft); noise.start(); setTimeout(() => { const analysis = fft.getValue(); expect(analysis.length).to.equal(256); analysis.forEach((value) => { expect(value).is.within(-Infinity, 0); }); fft.dispose(); noise.dispose(); done(); }, 300); }); it("outputs a normal range", (done) => { const noise = new Noise(); const fft = new FFT({ normalRange: true, }); noise.connect(fft); noise.start(); setTimeout(() => { const analysis = fft.getValue(); analysis.forEach((value) => { expect(value).is.within(0, 1); }); fft.dispose(); noise.dispose(); done(); }, 300); }); }); ================================================ FILE: Tone/component/analysis/FFT.ts ================================================ import { ToneAudioNode } from "../../core/context/ToneAudioNode.js"; import { dbToGain } from "../../core/type/Conversions.js"; import { Hertz, NormalRange, PowerOfTwo } from "../../core/type/Units.js"; import { assert } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { MeterBase, MeterBaseOptions } from "./MeterBase.js"; export interface FFTOptions extends MeterBaseOptions { size: PowerOfTwo; smoothing: NormalRange; normalRange: boolean; } /** * 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 { readonly name: string = "FFT"; /** * If the output should be in decibels or normal range between 0-1. If `normalRange` is false, * the output range will be the measured decibel value, otherwise the decibel value will be converted to * the range of 0-1 */ normalRange: boolean; /** * @param size The size of the FFT. Value must be a power of two in the range 16 to 16384. */ constructor(size?: PowerOfTwo); constructor(options?: Partial); constructor() { const options = optionsFromArguments(FFT.getDefaults(), arguments, [ "size", ]); super(options); this.normalRange = options.normalRange; this._analyser.type = "fft"; this.size = options.size; } static getDefaults(): FFTOptions { return Object.assign(ToneAudioNode.getDefaults(), { normalRange: false, size: 1024, smoothing: 0.8, }); } /** * Gets the current frequency data from the connected audio source. * Returns the frequency data of length {@link size} as a Float32Array of decibel values. */ getValue(): Float32Array { const values = this._analyser.getValue() as Float32Array; return values.map((v) => (this.normalRange ? dbToGain(v) : v)); } /** * The size of analysis. This must be a power of two in the range 16 to 16384. * Determines the size of the array returned by {@link getValue} (i.e. the number of * frequency bins). Large FFT sizes may be costly to compute. */ get size(): PowerOfTwo { return this._analyser.size; } set size(size) { this._analyser.size = size; } /** * 0 represents no time averaging with the last analysis frame. */ get smoothing(): NormalRange { return this._analyser.smoothing; } set smoothing(val) { this._analyser.smoothing = val; } /** * Returns the frequency value in hertz of each of the indices of the FFT's {@link getValue} response. * @example * const fft = new Tone.FFT(32); * console.log([0, 1, 2, 3, 4].map(index => fft.getFrequencyOfIndex(index))); */ getFrequencyOfIndex(index: number): Hertz { assert( 0 <= index && index < this.size, `index must be greater than or equal to 0 and less than ${this.size}` ); return (index * this.context.sampleRate) / (this.size * 2); } } ================================================ FILE: Tone/component/analysis/Follower.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { Follower } from "./Follower.js"; describe("Follower", () => { BasicTests(Follower); context("Envelope Following", () => { it("handles getter/setter as Object", () => { const foll = new Follower(); const values = { smoothing: 0.2, }; foll.set(values); expect(foll.get()).to.have.keys(["smoothing"]); expect(foll.get().smoothing).to.be.closeTo(0.2, 0.001); foll.dispose(); }); it("can be constructed with an object", () => { const follower = new Follower({ smoothing: 0.5, }); expect(follower.smoothing).to.be.closeTo(0.5, 0.001); follower.dispose(); }); it("smooths the incoming signal at 0.1", async () => { const buffer = await Offline(() => { const foll = new Follower(0.1).toDestination(); const sig = new Signal(0); sig.connect(foll); sig.setValueAtTime(1, 0.1); sig.setValueAtTime(0, 0.3); }, 0.41); expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.0, 0.01); expect(buffer.getValueAtTime(0.15)).to.be.closeTo(0.95, 0.05); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.35)).to.be.closeTo(0.05, 0.05); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0, 0.01); }); it("smooths the incoming signal at 0.05", async () => { const buffer = await Offline(() => { const foll = new Follower(0.05).toDestination(); const sig = new Signal(0); sig.connect(foll); sig.setValueAtTime(1, 0.1); sig.setValueAtTime(0, 0.3); }, 0.41); expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.0, 0.01); expect(buffer.getValueAtTime(0.125)).to.be.closeTo(0.95, 0.05); expect(buffer.getValueAtTime(0.15)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.325)).to.be.closeTo(0.05, 0.05); expect(buffer.getValueAtTime(0.35)).to.be.closeTo(0, 0.01); }); it("smooths the incoming signal at 0.2", async () => { const buffer = await Offline(() => { const foll = new Follower(0.2).toDestination(); const sig = new Signal(0); sig.connect(foll); sig.setValueAtTime(1, 0.1); sig.setValueAtTime(0, 0.3); }, 0.51); expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.0, 0.01); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.95, 0.05); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0.05, 0.05); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0, 0.01); }); it("smooths the incoming signal at 0.5", async () => { const buffer = await Offline(() => { const foll = new Follower(0.5).toDestination(); const sig = new Signal(0); sig.connect(foll); sig.setValueAtTime(1, 0.1); sig.setValueAtTime(0, 0.6); }, 1.11); expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.0, 0.01); expect(buffer.getValueAtTime(0.35)).to.be.closeTo(0.95, 0.05); expect(buffer.getValueAtTime(0.6)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.85)).to.be.closeTo(0.05, 0.05); expect(buffer.getValueAtTime(1.1)).to.be.closeTo(0, 0.01); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const follower = new Follower().toDestination(); input.connect(follower); }); }); }); }); ================================================ FILE: Tone/component/analysis/Follower.ts ================================================ import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Abs } from "../../signal/Abs.js"; import { OnePoleFilter } from "../filter/OnePoleFilter.js"; export interface FollowerOptions extends ToneAudioNodeOptions { smoothing: Time; } /** * Follower is a simple envelope follower. * It's implemented by applying a lowpass filter to the absolute value of the incoming signal. * ``` * +-----+ +---------------+ * Input +--> Abs +----> OnePoleFilter +--> Output * +-----+ +---------------+ * ``` * @category Component */ export class Follower extends ToneAudioNode { readonly name: string = "Follower"; readonly input: InputNode; readonly output: OutputNode; /** * Private reference to the smoothing parameter */ private _smoothing: Time; /** * The lowpass filter */ private _lowpass: OnePoleFilter; /** * The absolute value */ private _abs: Abs; /** * @param smoothing The rate of change of the follower. */ constructor(smoothing?: Time); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Follower.getDefaults(), arguments, ["smoothing"] ); super(options); this._abs = this.input = new Abs({ context: this.context }); this._lowpass = this.output = new OnePoleFilter({ context: this.context, frequency: 1 / this.toSeconds(options.smoothing), type: "lowpass", }); this._abs.connect(this._lowpass); this._smoothing = options.smoothing; } static getDefaults(): FollowerOptions { return Object.assign(ToneAudioNode.getDefaults(), { smoothing: 0.05, }); } /** * The amount of time it takes a value change to arrive at the updated value. */ get smoothing(): Time { return this._smoothing; } set smoothing(smoothing) { this._smoothing = smoothing; this._lowpass.frequency = 1 / this.toSeconds(this.smoothing); } dispose(): this { super.dispose(); this._abs.dispose(); this._lowpass.dispose(); return this; } } ================================================ FILE: Tone/component/analysis/Meter.test.ts ================================================ import { expect } from "chai"; import { BasicTests, warns } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { Merge } from "../channel/Merge.js"; import { Meter } from "./Meter.js"; describe("Meter", () => { BasicTests(Meter); context("Metering", () => { it("handles getter/setter as Object", () => { const meter = new Meter(); const values = { smoothing: 0.2, }; meter.set(values); expect(meter.get().smoothing).to.equal(0.2); meter.dispose(); }); it("can be constructed with the smoothing", () => { const meter = new Meter(0.5); expect(meter.smoothing).to.equal(0.5); meter.dispose(); }); it("returns an array of channels if channels > 1", () => { const meter = new Meter({ channelCount: 4, }); expect((meter.getValue() as number[]).length).to.equal(4); meter.dispose(); }); it("can be constructed with an object", () => { const meter = new Meter({ smoothing: 0.3, }); expect(meter.smoothing).to.equal(0.3); meter.dispose(); }); it("passes the audio through", () => { return PassAudio((input) => { const meter = new Meter().toDestination(); input.connect(meter); }); }); it("warns of deprecated method", () => { warns(() => { const meter = new Meter().toDestination(); meter.getLevel(); meter.dispose(); }); }); it("can get the rms level of the incoming signal", (done) => { const meter = new Meter(); const osc = new Oscillator().connect(meter).start(); osc.volume.value = -6; setTimeout(() => { expect(meter.getValue()).to.be.closeTo(-9, 1); meter.dispose(); osc.dispose(); done(); }, 400); }); it("returns 0 below a threshold", (done) => { const meter = new Meter({ normalRange: true, }); const osc = new Oscillator().connect(meter).start(); osc.volume.value = -101; setTimeout(() => { expect(meter.getValue()).to.equal(0); meter.dispose(); osc.dispose(); done(); }, 400); }); it("can get the values in normal range", (done) => { const meter = new Meter({ normalRange: true, }); const osc = new Oscillator().connect(meter).start(); osc.volume.value = -6; setTimeout(() => { expect(meter.getValue()).to.be.closeTo(0.35, 0.15); meter.dispose(); osc.dispose(); 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); }); }); }); ================================================ FILE: Tone/component/analysis/Meter.ts ================================================ import { dbToGain, gainToDb } from "../../core/type/Conversions.js"; import { NormalRange } from "../../core/type/Units.js"; import { warn } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Analyser } from "./Analyser.js"; import { MeterBase, MeterBaseOptions } from "./MeterBase.js"; export interface MeterOptions extends MeterBaseOptions { smoothing: NormalRange; normalRange: boolean; 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 {@link DCMeter}. * * @example * const meter = new Tone.Meter(); * const mic = new Tone.UserMedia(); * mic.open(); * // connect mic to the meter * mic.connect(meter); * // the current level of the mic * setInterval(() => console.log(meter.getValue()), 100); * @category Component */ export class Meter extends MeterBase { readonly name: string = "Meter"; /** * If the output should be in decibels or normal range between 0-1. If `normalRange` is false, * the output range will be the measured decibel value, otherwise the decibel value will be converted to * the range of 0-1 */ normalRange: boolean; /** * A value from between 0 and 1 where 0 represents no time averaging with the last analysis frame. */ smoothing: number; /** * The previous frame's value for each channel. */ private _rms: number[]; /** * @param smoothing The amount of smoothing applied between frames. */ constructor(smoothing?: NormalRange); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Meter.getDefaults(), arguments, [ "smoothing", ]); super(options); this.input = this.output = this._analyser = new Analyser({ context: this.context, size: 256, type: "waveform", 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, channelCount: 1, }); } /** * Use {@link getValue} instead. For the previous getValue behavior, use DCMeter. * @deprecated */ getLevel(): number | number[] { warn("'getLevel' has been changed to 'getValue'"); return this.getValue(); } /** * Below this threshold, stop smoothing. */ private minValue = dbToGain(-100); /** * Get the current value of the incoming signal. * Output is in decibels when {@link normalRange} is `false`. * If {@link channels} = 1, then the output is a single number * representing the value of the input signal. When {@link channels} > 1, * then each channel is returned as a value in a number array. */ getValue(): number | number[] { const aValues = this._analyser.getValue(); const channelValues = this.channels === 1 ? [aValues as Float32Array] : (aValues as Float32Array[]); const vals = channelValues.map((values, channel) => { const totalSquared = values.reduce( (total, current) => total + current * current, 0 ); const rms = Math.sqrt(totalSquared / values.length); if (rms < this.minValue) { this._rms[channel] = 0; } else { // the rms can only fall at the rate of the smoothing // but can jump up instantly this._rms[channel] = Math.max( rms, this._rms[channel] * this.smoothing ); } return this.normalRange ? this._rms[channel] : gainToDb(this._rms[channel]); }); if (this.channels === 1) { return vals[0]; } else { return vals; } } /** * The number of channels of analysis. */ get channels(): number { return this._analyser.channels; } dispose(): this { super.dispose(); this._analyser.dispose(); return this; } } ================================================ FILE: Tone/component/analysis/MeterBase.ts ================================================ import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Analyser } from "./Analyser.js"; export type MeterBaseOptions = ToneAudioNodeOptions; /** * The base class for Metering classes. */ export class MeterBase< Options extends MeterBaseOptions, > extends ToneAudioNode { readonly name: string = "MeterBase"; /** * The signal to be analyzed */ input: InputNode; /** * The output is just a pass through of the input */ output: OutputNode; /** * The analyser node for the incoming signal */ protected _analyser: Analyser; constructor(options?: Partial); constructor() { super(optionsFromArguments(MeterBase.getDefaults(), arguments)); this.input = this.output = this._analyser = new Analyser({ context: this.context, size: 256, type: "waveform", }); } dispose(): this { super.dispose(); this._analyser.dispose(); return this; } } ================================================ FILE: Tone/component/analysis/Waveform.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Noise } from "../../source/Noise.js"; import { Waveform } from "./Waveform.js"; describe("Waveform", () => { BasicTests(Waveform); it("can get and set properties", () => { const anl = new Waveform(); anl.set({ size: 128, }); const values = anl.get(); expect(values.size).to.equal(128); anl.dispose(); }); it("can correctly set the size", () => { const anl = new Waveform(512); expect(anl.size).to.equal(512); anl.size = 1024; expect(anl.size).to.equal(1024); anl.dispose(); }); it("can run waveform analysis", (done) => { const noise = new Noise(); const anl = new Waveform(256); noise.connect(anl); noise.start(); setTimeout(() => { const analysis = anl.getValue(); expect(analysis.length).to.equal(256); analysis.forEach((value) => { expect(value).is.within(-1, 1); }); anl.dispose(); noise.dispose(); done(); }, 300); }); }); ================================================ FILE: Tone/component/analysis/Waveform.ts ================================================ import { PowerOfTwo } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { MeterBase, MeterBaseOptions } from "./MeterBase.js"; export interface WaveformOptions extends MeterBaseOptions { /** * The size of the Waveform. Value must be a power of two in the range 16 to 16384. */ size: PowerOfTwo; } /** * Get the current waveform data of the connected audio source. * @category Component */ export class Waveform extends MeterBase { readonly name: string = "Waveform"; /** * @param size The size of the Waveform. Value must be a power of two in the range 16 to 16384. */ constructor(size?: PowerOfTwo); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Waveform.getDefaults(), arguments, ["size"] ); super(options); this._analyser.type = "waveform"; this.size = options.size; } static getDefaults(): WaveformOptions { return Object.assign(MeterBase.getDefaults(), { size: 1024, }); } /** * Return the waveform for the current time as a Float32Array where each value in the array * represents a sample in the waveform. */ getValue(): Float32Array { return this._analyser.getValue() as Float32Array; } /** * The size of analysis. This must be a power of two in the range 16 to 16384. * Determines the size of the array returned by {@link getValue}. */ get size(): PowerOfTwo { return this._analyser.size; } set size(size) { this._analyser.size = size; } } ================================================ FILE: Tone/component/channel/Channel.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { Channel } from "./Channel.js"; describe("Channel", () => { BasicTests(Channel); context("Channel", () => { it("can pass volume and panning into the constructor", () => { const channel = new Channel(-10, -1); expect(channel.pan.value).to.be.closeTo(-1, 0.01); expect(channel.volume.value).to.be.closeTo(-10, 0.01); channel.dispose(); }); it("can pass in an object into the constructor", () => { const channel = new Channel({ pan: 1, volume: 6, mute: false, solo: true, }); expect(channel.pan.value).to.be.closeTo(1, 0.01); expect(channel.volume.value).to.be.closeTo(6, 0.01); expect(channel.mute).to.be.false; expect(channel.solo).to.be.true; channel.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const channel = new Channel().toDestination(); input.connect(channel); }); }); it("can mute the input", async () => { const buffer = await Offline(() => { const channel = new Channel(0).toDestination(); new Signal(1).connect(channel); channel.mute = true; }); expect(buffer.isSilent()).to.be.true; }); it("reports itself as muted when either muted or another channel is soloed", () => { const channelA = new Channel(); const channelB = new Channel(); channelB.solo = true; expect(channelA.muted).to.be.true; expect(channelB.muted).to.be.false; channelB.mute = true; expect(channelA.muted).to.be.true; expect(channelB.muted).to.be.true; channelA.dispose(); channelB.dispose(); }); describe("bus", () => { it("can connect two channels together by name", () => { return PassAudio((input) => { const sendChannel = new Channel(); input.connect(sendChannel); sendChannel.send("test"); const recvChannel = new Channel().toDestination(); recvChannel.receive("test"); }); }); }); }); }); ================================================ FILE: Tone/component/channel/Channel.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { Param } from "../../core/context/Param.js"; import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { AudioRange, Decibels } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; import { PanVol } from "./PanVol.js"; import { Solo } from "./Solo.js"; export interface ChannelOptions extends ToneAudioNodeOptions { pan: AudioRange; volume: Decibels; solo: boolean; mute: boolean; channelCount: number; } /** * Channel provides a channel strip interface with volume, pan, solo and mute controls. * @see {@link PanVol} and {@link Solo} * @example * // pan the incoming signal left and drop the volume 12db * const channel = new Tone.Channel(-0.25, -12); * @category Component */ export class Channel extends ToneAudioNode { readonly name: string = "Channel"; readonly input: InputNode; readonly output: OutputNode; /** * The soloing interface */ private _solo: Solo; /** * The panning and volume node */ private _panVol: PanVol; /** * The L/R panning control. -1 = hard left, 1 = hard right. * @min -1 * @max 1 */ readonly pan: Param<"audioRange">; /** * The volume control in decibels. */ readonly volume: Param<"decibels">; /** * @param volume The output volume. * @param pan the initial pan */ constructor(volume?: Decibels, pan?: AudioRange); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Channel.getDefaults(), arguments, [ "volume", "pan", ]); super(options); this._solo = this.input = new Solo({ solo: options.solo, context: this.context, }); this._panVol = this.output = new PanVol({ context: this.context, pan: options.pan, volume: options.volume, mute: options.mute, channelCount: options.channelCount, }); this.pan = this._panVol.pan; this.volume = this._panVol.volume; this._solo.connect(this._panVol); readOnly(this, ["pan", "volume"]); } static getDefaults(): ChannelOptions { return Object.assign(ToneAudioNode.getDefaults(), { pan: 0, volume: 0, mute: false, solo: false, channelCount: 1, }); } /** * Solo/unsolo the channel. Soloing is only relative to other {@link Channel}s and {@link Solo} instances */ get solo(): boolean { return this._solo.solo; } set solo(solo) { this._solo.solo = solo; } /** * If the current instance is muted, i.e. another instance is soloed, * or the channel is muted */ get muted(): boolean { return this._solo.muted || this.mute; } /** * Mute/unmute the volume */ get mute(): boolean { return this._panVol.mute; } set mute(mute) { this._panVol.mute = mute; } /** * Store the send/receive channels by name. */ private static buses: Map = new Map(); /** * Get the gain node belonging to the bus name. Create it if * it doesn't exist * @param name The bus name */ private _getBus(name: string): Gain { if (!Channel.buses.has(name)) { Channel.buses.set(name, new Gain({ context: this.context })); } return Channel.buses.get(name) as Gain; } /** * Send audio to another channel using a string. `send` is a lot like * {@link connect}, except it uses a string instead of an object. This can * be useful in large applications to decouple sections since {@link send} * and {@link receive} can be invoked separately in order to connect an object * @param name The channel name to send the audio * @param volume The amount of the signal to send. * Defaults to 0db, i.e. send the entire signal * @returns Returns the gain node of this connection. */ send(name: string, volume: Decibels = 0): Gain<"decibels"> { const bus = this._getBus(name); const sendKnob = new Gain({ context: this.context, units: "decibels", gain: volume, }); this.connect(sendKnob); sendKnob.connect(bus); return sendKnob; } /** * Receive audio from a channel which was connected with {@link send}. * @param name The channel name to receive audio from. */ receive(name: string): this { const bus = this._getBus(name); bus.connect(this); return this; } dispose(): this { super.dispose(); this._panVol.dispose(); this.pan.dispose(); this.volume.dispose(); this._solo.dispose(); return this; } } ================================================ FILE: Tone/component/channel/CrossFade.test.ts ================================================ import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { ConstantOutput } from "../../../test/helper/ConstantOutput.js"; import { Signal } from "../../signal/Signal.js"; import { CrossFade } from "./CrossFade.js"; describe("CrossFade", () => { BasicTests(CrossFade); context("Fading", () => { it("handles input and output connections", () => { const comp = new CrossFade(); connectFrom().connect(comp.a); connectFrom().connect(comp.b); comp.connect(connectTo()); comp.dispose(); }); it("pass 100% of input 0", () => { return ConstantOutput( () => { const crossFade = new CrossFade(); const drySignal = new Signal(10); const wetSignal = new Signal(20); drySignal.connect(crossFade.a); wetSignal.connect(crossFade.b); crossFade.fade.value = 0; crossFade.toDestination(); }, 10, 0.05 ); }); it("pass 100% of input 1", () => { return ConstantOutput( () => { const crossFade = new CrossFade(); const drySignal = new Signal(10); const wetSignal = new Signal(20); drySignal.connect(crossFade.a); wetSignal.connect(crossFade.b); crossFade.fade.value = 1; crossFade.toDestination(); }, 20, 0.01 ); }); it("can mix two signals", () => { return ConstantOutput( () => { const crossFade = new CrossFade(); const drySignal = new Signal(2); const wetSignal = new Signal(1); drySignal.connect(crossFade.a); wetSignal.connect(crossFade.b); crossFade.fade.value = 0.5; crossFade.toDestination(); }, 2.12, 0.01 ); }); }); }); ================================================ FILE: Tone/component/channel/CrossFade.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { connect, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { NormalRange } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; import { GainToAudio } from "../../signal/GainToAudio.js"; import { Signal } from "../../signal/Signal.js"; import { ToneConstantSource } from "../../signal/ToneConstantSource.js"; interface CrossFadeOptions extends ToneAudioNodeOptions { fade: NormalRange; } /** * Tone.Crossfade provides equal power fading between two inputs. * More on crossfading technique [here](https://en.wikipedia.org/wiki/Fade_(audio_engineering)#Crossfading). * ``` * +---------+ * +> input a +>--+ * +-----------+ +---------------------+ | | | * | 1s signal +>--> stereoPannerNode L +>----> gain | | * +-----------+ | | +---------+ | * +-> pan R +>-+ | +--------+ * | +---------------------+ | +---> output +> * +------+ | | +---------+ | +--------+ * | fade +>----+ | +> input b +>--+ * +------+ | | | * +--> gain | * +---------+ * ``` * @example * const crossFade = new Tone.CrossFade().toDestination(); * // connect two inputs Tone.to a/b * const inputA = new Tone.Oscillator(440, "square").connect(crossFade.a).start(); * const inputB = new Tone.Oscillator(440, "sine").connect(crossFade.b).start(); * // use the fade to control the mix between the two * crossFade.fade.value = 0.5; * @category Component */ export class CrossFade extends ToneAudioNode { readonly name: string = "CrossFade"; /** * The crossfading is done by a StereoPannerNode */ private _panner: StereoPannerNode = this.context.createStereoPanner(); /** * Split the output of the panner node into two values used to control the gains. */ private _split: ChannelSplitterNode = this.context.createChannelSplitter(2); /** * Convert the fade value into an audio range value so it can be connected * to the panner.pan AudioParam */ private _g2a: GainToAudio = new GainToAudio({ context: this.context }); /** * The constant source which is used to control the panner */ private _constant: ToneConstantSource; /** * The input which is at full level when fade = 0 */ readonly a: Gain = new Gain({ context: this.context, gain: 0, }); /** * The input which is at full level when fade = 1 */ readonly b: Gain = new Gain({ context: this.context, gain: 0, }); /** * The output is a mix between `a` and `b` at the ratio of `fade` */ readonly output: Gain = new Gain({ context: this.context }); /** * CrossFade has no input, you must choose either `a` or `b` */ readonly input: undefined; /** * The mix between the two inputs. A fade value of 0 * will output 100% crossFade.a and * a value of 1 will output 100% crossFade.b. */ readonly fade: Signal<"normalRange">; protected _internalChannels = [this.a, this.b]; /** * @param fade The initial fade value [0, 1]. */ constructor(fade?: NormalRange); constructor(options?: Partial); constructor() { const options = optionsFromArguments( CrossFade.getDefaults(), arguments, ["fade"] ); super(options); this.fade = new Signal({ context: this.context, units: "normalRange", value: options.fade, }); readOnly(this, "fade"); this._constant = new ToneConstantSource({ context: this.context, offset: 1, }).start(); this._constant.connect(this._panner); this._panner.connect(this._split); // this is necessary for standardized-audio-context // doesn't make any difference for the native AudioContext // https://github.com/chrisguttandin/standardized-audio-context/issues/647 this._panner.channelCount = 1; this._panner.channelCountMode = "explicit"; connect(this._split, this.a.gain, 0); connect(this._split, this.b.gain, 1); this.fade.chain(this._g2a, this._panner.pan); this.a.connect(this.output); this.b.connect(this.output); } static getDefaults(): CrossFadeOptions { return Object.assign(ToneAudioNode.getDefaults(), { fade: 0.5, }); } dispose(): this { super.dispose(); this.a.dispose(); this.b.dispose(); this.output.dispose(); this.fade.dispose(); this._g2a.dispose(); this._panner.disconnect(); this._split.disconnect(); this._constant.dispose(); return this; } } ================================================ FILE: Tone/component/channel/Merge.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { Merge } from "./Merge.js"; describe("Merge", () => { BasicTests(Merge); context("Merging", () => { it("handles input and output connections", () => { const merge = new Merge(); connectFrom().connect(merge); merge.connect(connectTo()); merge.dispose(); }); it("defaults to two channels", () => { const merge = new Merge(); expect(merge.numberOfInputs).to.equal(2); merge.dispose(); }); it("can pass in more channels", () => { const merge = new Merge(4); expect(merge.numberOfInputs).to.equal(4); connectFrom().connect(merge, 0, 0); connectFrom().connect(merge, 0, 1); connectFrom().connect(merge, 0, 2); connectFrom().connect(merge, 0, 3); merge.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const merge = new Merge().toDestination(); input.connect(merge); }); }); it("merge two signal into one stereo signal", async () => { const buffer = await Offline( () => { const sigL = new Signal(1); const sigR = new Signal(2); const merger = new Merge(); sigL.connect(merger, 0, 0); sigR.connect(merger, 0, 1); merger.toDestination(); }, 0.1, 2 ); expect(buffer.toArray()[0][0]).to.be.closeTo(1, 0.001); expect(buffer.toArray()[1][0]).to.be.closeTo(2, 0.001); }); }); }); ================================================ FILE: Tone/component/channel/Merge.ts ================================================ import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Positive } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; interface MergeOptions extends ToneAudioNodeOptions { channels: Positive; } /** * Merge brings multiple mono input channels into a single multichannel output channel. * * @example * const merge = new Tone.Merge().toDestination(); * // routing a sine tone in the left channel * const osc = new Tone.Oscillator().connect(merge, 0, 0).start(); * // and noise in the right channel * const noise = new Tone.Noise().connect(merge, 0, 1).start();; * @category Component */ export class Merge extends ToneAudioNode { readonly name: string = "Merge"; /** * The merger node for the channels. */ private _merger: ChannelMergerNode; /** * The output is the input channels combined into a single (multichannel) output */ readonly output: ChannelMergerNode; /** * Multiple input connections combine into a single output. */ readonly input: ChannelMergerNode; /** * @param channels The number of channels to merge. */ constructor(channels?: Positive); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Merge.getDefaults(), arguments, [ "channels", ]); super(options); this._merger = this.output = this.input = this.context.createChannelMerger(options.channels); } static getDefaults(): MergeOptions { return Object.assign(ToneAudioNode.getDefaults(), { channels: 2, }); } dispose(): this { super.dispose(); this._merger.disconnect(); return this; } } ================================================ FILE: Tone/component/channel/MidSideMerge.test.ts ================================================ import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { MidSideMerge } from "./MidSideMerge.js"; describe("MidSideMerge", () => { BasicTests(MidSideMerge); context("Merging", () => { it("handles inputs and outputs", () => { const merge = new MidSideMerge(); merge.connect(connectTo()); connectFrom().connect(merge.mid); connectFrom().connect(merge.side); merge.dispose(); }); it("passes the mid signal through", () => { return PassAudio((input) => { const merge = new MidSideMerge().toDestination(); input.connect(merge.mid); }); }); }); }); ================================================ FILE: Tone/component/channel/MidSideMerge.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Add } from "../../signal/Add.js"; import { Multiply } from "../../signal/Multiply.js"; import { Subtract } from "../../signal/Subtract.js"; import { Merge } from "./Merge.js"; export type MidSideMergeOptions = ToneAudioNodeOptions; /** * MidSideMerge merges the mid and side signal after they've been separated by {@link MidSideSplit} * ``` * Mid = (Left+Right)/sqrt(2); // obtain mid-signal from left and right * Side = (Left-Right)/sqrt(2); // obtain side-signal from left and right * ``` * @category Component */ export class MidSideMerge extends ToneAudioNode { readonly name: string = "MidSideMerge"; /** * There is no input, connect sources to either {@link mid} or {@link side} inputs. */ readonly input: undefined; /** * The merged signal */ readonly output: Merge; /** * Merge the incoming signal into left and right channels */ private _merge: Merge; /** * The "mid" input. */ readonly mid: ToneAudioNode; /** * The "side" input. */ readonly side: ToneAudioNode; /** * Recombine the mid/side into Left */ private _left: Add; /** * Recombine the mid/side into Right */ private _right: Subtract; /** * Multiply the right by sqrt(1/2) */ private _leftMult: Multiply; /** * Multiply the left by sqrt(1/2) */ private _rightMult: Multiply; constructor(options?: Partial); constructor() { super(optionsFromArguments(MidSideMerge.getDefaults(), arguments)); this.mid = new Gain({ context: this.context }); this.side = new Gain({ context: this.context }); this._left = new Add({ context: this.context }); this._leftMult = new Multiply({ context: this.context, value: Math.SQRT1_2, }); this._right = new Subtract({ context: this.context }); this._rightMult = new Multiply({ context: this.context, value: Math.SQRT1_2, }); this._merge = this.output = new Merge({ context: this.context }); this.mid.fan(this._left); this.side.connect(this._left.addend); this.mid.connect(this._right); this.side.connect(this._right.subtrahend); this._left.connect(this._leftMult); this._right.connect(this._rightMult); this._leftMult.connect(this._merge, 0, 0); this._rightMult.connect(this._merge, 0, 1); } dispose(): this { super.dispose(); this.mid.dispose(); this.side.dispose(); this._leftMult.dispose(); this._rightMult.dispose(); this._left.dispose(); this._right.dispose(); return this; } } ================================================ FILE: Tone/component/channel/MidSideSplit.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { Offline } from "../../../test/helper/Offline.js"; import { Signal } from "../../signal/Signal.js"; import { Merge } from "./Merge.js"; import { MidSideMerge } from "./MidSideMerge.js"; import { MidSideSplit } from "./MidSideSplit.js"; describe("MidSideSplit", () => { BasicTests(MidSideSplit); context("Splitting", () => { it("handles inputs and outputs", () => { const split = new MidSideSplit(); connectFrom().connect(split); split.mid.connect(connectTo()); split.side.connect(connectTo()); split.dispose(); }); it("mid is if both L and R are the same", async () => { const buffer = await Offline(() => { const split = new MidSideSplit(); split.mid.toDestination(); const merge = new Merge().connect(split); new Signal(0.5).connect(merge, 0, 0); new Signal(0.5).connect(merge, 0, 1); }); expect(buffer.min()).to.be.closeTo(0.707, 0.01); expect(buffer.max()).to.be.closeTo(0.707, 0.01); }); it("side is 0 if both L and R are the same", async () => { const buffer = await Offline(() => { const split = new MidSideSplit(); split.side.toDestination(); const merge = new Merge().connect(split); new Signal(0.5).connect(merge, 0, 0); new Signal(0.5).connect(merge, 0, 1); }); expect(buffer.min()).to.be.closeTo(0, 0.01); expect(buffer.max()).to.be.closeTo(0, 0.01); }); it("mid is 0 if both L and R opposites", async () => { const buffer = await Offline(() => { const split = new MidSideSplit(); split.mid.toDestination(); const merge = new Merge().connect(split); new Signal(-1).connect(merge, 0, 0); new Signal(1).connect(merge, 0, 1); }); expect(buffer.min()).to.be.closeTo(0, 0.01); expect(buffer.max()).to.be.closeTo(0, 0.01); }); it("can decompose and reconstruct a signal", async () => { const buffer = await Offline( () => { const midSideMerge = new MidSideMerge().toDestination(); const split = new MidSideSplit(); split.mid.connect(midSideMerge.mid); split.side.connect(midSideMerge.side); const merge = new Merge().connect(split); new Signal(0.2).connect(merge, 0, 0); new Signal(0.4).connect(merge, 0, 1); }, 0.1, 2 ); buffer .toArray()[0] .forEach((l) => expect(l).to.be.closeTo(0.2, 0.01)); buffer .toArray()[1] .forEach((r) => expect(r).to.be.closeTo(0.4, 0.01)); }); }); }); ================================================ FILE: Tone/component/channel/MidSideSplit.ts ================================================ import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Add } from "../../signal/Add.js"; import { Multiply } from "../../signal/Multiply.js"; import { Subtract } from "../../signal/Subtract.js"; import { Split } from "./Split.js"; export type MidSideSplitOptions = ToneAudioNodeOptions; /** * Mid/Side processing separates the the 'mid' signal (which comes out of both the left and the right channel) * and the 'side' (which only comes out of the the side channels). * ``` * Mid = (Left+Right)/sqrt(2); // obtain mid-signal from left and right * Side = (Left-Right)/sqrt(2); // obtain side-signal from left and right * ``` * @category Component */ export class MidSideSplit extends ToneAudioNode { readonly name: string = "MidSideSplit"; readonly input: Split; /** * There is no output node, use either {@link mid} or {@link side} outputs. */ readonly output: undefined; /** * Split the incoming signal into left and right channels */ private _split: Split; /** * Sums the left and right channels */ private _midAdd: Add; /** * Subtract left and right channels. */ private _sideSubtract: Subtract; /** * The "mid" output. `(Left+Right)/sqrt(2)` */ readonly mid: ToneAudioNode; /** * The "side" output. `(Left-Right)/sqrt(2)` */ readonly side: ToneAudioNode; constructor(options?: Partial); constructor() { super(optionsFromArguments(MidSideSplit.getDefaults(), arguments)); this._split = this.input = new Split({ channels: 2, context: this.context, }); this._midAdd = new Add({ context: this.context }); this.mid = new Multiply({ context: this.context, value: Math.SQRT1_2, }); this._sideSubtract = new Subtract({ context: this.context }); this.side = new Multiply({ context: this.context, value: Math.SQRT1_2, }); this._split.connect(this._midAdd, 0); this._split.connect(this._midAdd.addend, 1); this._split.connect(this._sideSubtract, 0); this._split.connect(this._sideSubtract.subtrahend, 1); this._midAdd.connect(this.mid); this._sideSubtract.connect(this.side); } dispose(): this { super.dispose(); this.mid.dispose(); this.side.dispose(); this._midAdd.dispose(); this._sideSubtract.dispose(); this._split.dispose(); return this; } } ================================================ FILE: Tone/component/channel/Mono.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { StereoSignal } from "../../../test/helper/StereoSignal.js"; import { Signal } from "../../signal/Signal.js"; import { Mono } from "./Mono.js"; describe("Mono", () => { BasicTests(Mono); context("Mono", () => { it("Makes a mono signal in both channels", async () => { const buffer = await Offline( () => { const mono = new Mono().toDestination(); const signal = new Signal(2).connect(mono); }, 0.1, 2 ); expect(buffer.toArray()[0][0]).to.equal(2); expect(buffer.toArray()[1][0]).to.equal(2); expect(buffer.toArray()[0][100]).to.equal(2); expect(buffer.toArray()[1][100]).to.equal(2); expect(buffer.toArray()[0][1000]).to.equal(2); expect(buffer.toArray()[1][1000]).to.equal(2); }); it("Sums a stereo signal into a mono signal", async () => { const buffer = await Offline( () => { const mono = new Mono().toDestination(); const signal = StereoSignal(2, 2).connect(mono); }, 0.1, 2 ); expect(buffer.toArray()[0][0]).to.equal(2); expect(buffer.toArray()[1][0]).to.equal(2); expect(buffer.toArray()[0][100]).to.equal(2); expect(buffer.toArray()[1][100]).to.equal(2); expect(buffer.toArray()[0][1000]).to.equal(2); expect(buffer.toArray()[1][1000]).to.equal(2); }); }); }); ================================================ FILE: Tone/component/channel/Mono.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Merge } from "./Merge.js"; export type MonoOptions = ToneAudioNodeOptions; /** * Mono coerces the incoming mono or stereo signal into a mono signal * where both left and right channels have the same value. This can be useful * for [stereo imaging](https://en.wikipedia.org/wiki/Stereo_imaging). * @category Component */ export class Mono extends ToneAudioNode { readonly name: string = "Mono"; /** * merge the signal */ private _merge: Merge; /** * The summed output of the multiple inputs */ readonly output: OutputNode; /** * The stereo signal to sum to mono */ readonly input: Gain; constructor(options?: Partial); constructor() { super(optionsFromArguments(Mono.getDefaults(), arguments)); this.input = new Gain({ context: this.context }); this._merge = this.output = new Merge({ channels: 2, context: this.context, }); this.input.connect(this._merge, 0, 0); this.input.connect(this._merge, 0, 1); } dispose(): this { super.dispose(); this._merge.dispose(); this.input.dispose(); return this; } } ================================================ FILE: Tone/component/channel/MultibandSplit.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { MultibandSplit } from "./MultibandSplit.js"; describe("MultibandSplit", () => { BasicTests(MultibandSplit); it("handles input and output connections", () => { const split = new MultibandSplit(); connectFrom().connect(split); split.low.connect(connectTo()); split.mid.connect(connectTo()); split.high.connect(connectTo()); split.dispose(); }); it("can be constructed with an object", () => { const split = new MultibandSplit({ Q: 8, highFrequency: 2700, lowFrequency: 500, }); expect(split.lowFrequency.value).to.be.closeTo(500, 0.01); expect(split.highFrequency.value).to.be.closeTo(2700, 0.01); expect(split.Q.value).to.be.closeTo(8, 0.01); split.dispose(); }); it("can be get and set through object", () => { const split = new MultibandSplit(); split.set({ Q: 4, lowFrequency: 250, }); expect(split.get().Q).to.be.closeTo(4, 0.1); expect(split.get().lowFrequency).to.be.closeTo(250, 0.01); split.dispose(); }); it("passes the incoming signal through low", () => { return PassAudio((input) => { const split = new MultibandSplit().low.toDestination(); input.connect(split); }); }); it("passes the incoming signal through mid", () => { return PassAudio((input) => { const split = new MultibandSplit().mid.toDestination(); input.connect(split); }); }); it("passes the incoming signal through high", () => { return PassAudio((input) => { const split = new MultibandSplit({ highFrequency: 10, lowFrequency: 5, }).high.toDestination(); input.connect(split); }); }); }); ================================================ FILE: Tone/component/channel/MultibandSplit.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Frequency, Positive } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, writable } from "../../core/util/Interface.js"; import { Signal } from "../../signal/Signal.js"; import { Filter } from "../filter/Filter.js"; interface MultibandSplitOptions extends ToneAudioNodeOptions { Q: Positive; lowFrequency: Frequency; highFrequency: Frequency; } /** * Split the incoming signal into three bands (low, mid, high) * with two crossover frequency controls. * ``` * +----------------------+ * +-> input < lowFrequency +------------------> low * | +----------------------+ * | * | +--------------------------------------+ * input ---+-> lowFrequency < input < highFrequency +--> mid * | +--------------------------------------+ * | * | +-----------------------+ * +-> highFrequency < input +-----------------> high * +-----------------------+ * ``` * @category Component */ export class MultibandSplit extends ToneAudioNode { readonly name: string = "MultibandSplit"; /** * the input */ readonly input = new Gain({ context: this.context }); /** * no output node, use either low, mid or high outputs */ readonly output = undefined; /** * The low band. */ readonly low = new Filter({ context: this.context, frequency: 0, type: "lowpass", }); /** * the lower filter of the mid band */ private _lowMidFilter = new Filter({ context: this.context, frequency: 0, type: "highpass", }); /** * The mid band output. */ readonly mid = new Filter({ context: this.context, frequency: 0, type: "lowpass", }); /** * The high band output. */ readonly high = new Filter({ context: this.context, frequency: 0, type: "highpass", }); /** * The low/mid crossover frequency. */ readonly lowFrequency: Signal<"frequency">; /** * The mid/high crossover frequency. */ readonly highFrequency: Signal<"frequency">; protected _internalChannels = [this.low, this.mid, this.high]; /** * The Q or Quality of the filter */ readonly Q: Signal<"positive">; /** * @param lowFrequency the low/mid crossover frequency * @param highFrequency the mid/high crossover frequency */ constructor(lowFrequency?: Frequency, highFrequency?: Frequency); constructor(options?: Partial); constructor() { const options = optionsFromArguments( MultibandSplit.getDefaults(), arguments, ["lowFrequency", "highFrequency"] ); super(options); this.lowFrequency = new Signal({ context: this.context, units: "frequency", value: options.lowFrequency, }); this.highFrequency = new Signal({ context: this.context, units: "frequency", value: options.highFrequency, }); this.Q = new Signal({ context: this.context, units: "positive", value: options.Q, }); this.input.fan(this.low, this.high); this.input.chain(this._lowMidFilter, this.mid); // the frequency control signal this.lowFrequency.fan(this.low.frequency, this._lowMidFilter.frequency); this.highFrequency.fan(this.mid.frequency, this.high.frequency); // the Q value this.Q.connect(this.low.Q); this.Q.connect(this._lowMidFilter.Q); this.Q.connect(this.mid.Q); this.Q.connect(this.high.Q); readOnly(this, ["high", "mid", "low", "highFrequency", "lowFrequency"]); } static getDefaults(): MultibandSplitOptions { return Object.assign(ToneAudioNode.getDefaults(), { Q: 1, highFrequency: 2500, lowFrequency: 400, }); } /** * Clean up. */ dispose(): this { super.dispose(); writable(this, ["high", "mid", "low", "highFrequency", "lowFrequency"]); this.low.dispose(); this._lowMidFilter.dispose(); this.mid.dispose(); this.high.dispose(); this.lowFrequency.dispose(); this.highFrequency.dispose(); this.Q.dispose(); return this; } } ================================================ FILE: Tone/component/channel/PanVol.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { PanVol } from "./PanVol.js"; describe("PanVol", () => { BasicTests(PanVol); context("Pan and Volume", () => { it("can be constructed with the panning and volume value", () => { const panVol = new PanVol(0.3, -12); expect(panVol.pan.value).to.be.closeTo(0.3, 0.001); expect(panVol.volume.value).to.be.closeTo(-12, 0.1); panVol.dispose(); }); it("can be constructed with an options object", () => { const panVol = new PanVol({ mute: true, pan: 0.2, }); expect(panVol.pan.value).to.be.closeTo(0.2, 0.001); expect(panVol.mute).to.be.true; panVol.dispose(); }); it("can set/get with an object", () => { const panVol = new PanVol(); panVol.set({ volume: -10, }); expect(panVol.get().volume).to.be.closeTo(-10, 0.1); panVol.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const panVol = new PanVol().toDestination(); input.connect(panVol); }); }); it("can mute the volume", async () => { const buffer = await Offline(() => { const vol = new PanVol(0).toDestination(); new Signal(1).connect(vol); vol.mute = true; }); expect(buffer.isSilent()).to.be.true; }); }); }); ================================================ FILE: Tone/component/channel/PanVol.ts ================================================ import { Param } from "../../core/context/Param.js"; import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { AudioRange, Decibels } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; import { Panner } from "./Panner.js"; import { Volume } from "./Volume.js"; export interface PanVolOptions extends ToneAudioNodeOptions { pan: AudioRange; volume: Decibels; mute: boolean; channelCount: number; } /** * PanVol is a Tone.Panner and Tone.Volume in one. * @example * // pan the incoming signal left and drop the volume * const panVol = new Tone.PanVol(-0.25, -12).toDestination(); * const osc = new Tone.Oscillator().connect(panVol).start(); * @category Component */ export class PanVol extends ToneAudioNode { readonly name: string = "PanVol"; readonly input: InputNode; readonly output: OutputNode; /** * The panning node */ private _panner: Panner; /** * The L/R panning control. -1 = hard left, 1 = hard right. * @min -1 * @max 1 */ readonly pan: Param<"audioRange">; /** * The volume node */ private _volume: Volume; /** * The volume control in decibels. */ readonly volume: Param<"decibels">; /** * @param pan the initial pan * @param volume The output volume. */ constructor(pan?: AudioRange, volume?: Decibels); constructor(options?: Partial); constructor() { const options = optionsFromArguments(PanVol.getDefaults(), arguments, [ "pan", "volume", ]); super(options); this._panner = this.input = new Panner({ context: this.context, pan: options.pan, channelCount: options.channelCount, }); this.pan = this._panner.pan; this._volume = this.output = new Volume({ context: this.context, volume: options.volume, }); this.volume = this._volume.volume; // connections this._panner.connect(this._volume); this.mute = options.mute; readOnly(this, ["pan", "volume"]); } static getDefaults(): PanVolOptions { return Object.assign(ToneAudioNode.getDefaults(), { mute: false, pan: 0, volume: 0, channelCount: 1, }); } /** * Mute/unmute the volume */ get mute(): boolean { return this._volume.mute; } set mute(mute) { this._volume.mute = mute; } dispose(): this { super.dispose(); this._panner.dispose(); this.pan.dispose(); this._volume.dispose(); this.volume.dispose(); return this; } } ================================================ FILE: Tone/component/channel/Panner.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { Panner } from "./Panner.js"; describe("Panner", () => { BasicTests(Panner); context("Panning", () => { it("can be constructed with the panning value", () => { const panner = new Panner(0.3); expect(panner.pan.value).to.be.closeTo(0.3, 0.001); panner.dispose(); }); it("can be constructed with an options object", () => { const panner = new Panner({ pan: 0.5, }); expect(panner.pan.value).to.be.closeTo(0.5, 0.001); panner.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const panner = new Panner().toDestination(); input.connect(panner); }); }); it("pans hard left when the pan is set to -1", async () => { const buffer = await Offline( () => { const panner = new Panner(-1).toDestination(); new Signal(1).connect(panner); }, 0.1, 2 ); const l = buffer.toArray()[0]; const r = buffer.toArray()[1]; expect(l[0]).to.be.closeTo(1, 0.01); expect(r[0]).to.be.closeTo(0, 0.01); }); it("pans hard right when the pan is set to 1", async () => { const buffer = await Offline( () => { const panner = new Panner(1).toDestination(); new Signal(1).connect(panner); }, 0.1, 2 ); const l = buffer.toArray()[0]; const r = buffer.toArray()[1]; expect(l[0]).to.be.closeTo(0, 0.01); expect(r[0]).to.be.closeTo(1, 0.01); }); it("mixes the signal in equal power when panned center", async () => { const buffer = await Offline( () => { const panner = new Panner(0).toDestination(); new Signal(1).connect(panner); }, 0.1, 2 ); const l = buffer.toArray()[0]; const r = buffer.toArray()[1]; expect(l[0]).to.be.closeTo(0.707, 0.01); expect(r[0]).to.be.closeTo(0.707, 0.01); }); it("can chain two panners when channelCount is 2", async () => { const buffer = await Offline( () => { const panner1 = new Panner({ channelCount: 2, }).toDestination(); const panner0 = new Panner(-1).connect(panner1); new Signal(1).connect(panner0); }, 0.1, 2 ); const l = buffer.toArray()[0]; const r = buffer.toArray()[1]; expect(l[0]).to.be.closeTo(1, 0.01); expect(r[0]).to.be.closeTo(0, 0.01); }); }); }); ================================================ FILE: Tone/component/channel/Panner.ts ================================================ import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { AudioRange } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; interface TonePannerOptions extends ToneAudioNodeOptions { pan: AudioRange; channelCount: number; } /** * Panner is an equal power Left/Right Panner. It is a wrapper around the StereoPannerNode. * @example * return Tone.Offline(() => { * // move the input signal from right to left * const panner = new Tone.Panner(1).toDestination(); * panner.pan.rampTo(-1, 0.5); * const osc = new Tone.Oscillator(100).connect(panner).start(); * }, 0.5, 2); * @category Component */ export class Panner extends ToneAudioNode { readonly name: string = "Panner"; /** * the panner node */ private _panner: StereoPannerNode = this.context.createStereoPanner(); readonly input: StereoPannerNode = this._panner; readonly output: StereoPannerNode = this._panner; /** * The pan control. -1 = hard left, 1 = hard right. * @min -1 * @max 1 * @example * return Tone.Offline(() => { * // pan hard right * const panner = new Tone.Panner(1).toDestination(); * // pan hard left * panner.pan.setValueAtTime(-1, 0.25); * const osc = new Tone.Oscillator(50, "triangle").connect(panner).start(); * }, 0.5, 2); */ readonly pan: Param<"audioRange">; constructor(options?: Partial); /** * @param pan The initial panner value (Defaults to 0 = "center"). */ constructor(pan?: AudioRange); constructor() { const options = optionsFromArguments(Panner.getDefaults(), arguments, [ "pan", ]); super(options); this.pan = new Param({ context: this.context, param: this._panner.pan, value: options.pan, minValue: -1, maxValue: 1, }); // this is necessary for standardized-audio-context // doesn't make any difference for the native AudioContext // https://github.com/chrisguttandin/standardized-audio-context/issues/647 this._panner.channelCount = options.channelCount; this._panner.channelCountMode = "explicit"; // initial value readOnly(this, "pan"); } static getDefaults(): TonePannerOptions { return Object.assign(ToneAudioNode.getDefaults(), { pan: 0, channelCount: 1, }); } dispose(): this { super.dispose(); this._panner.disconnect(); this.pan.dispose(); return this; } } ================================================ FILE: Tone/component/channel/Panner3D.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Panner3D } from "./Panner3D.js"; describe("Panner3D", () => { BasicTests(Panner3D); it("passes the incoming signal through", () => { return PassAudio((input) => { const panner = new Panner3D().toDestination(); input.connect(panner); }); }); it("can get/set the position individually", () => { const panner = new Panner3D(); panner.positionX.value = 10; expect(panner.positionX.value).to.equal(10); panner.positionY.value = 20; expect(panner.positionY.value).to.equal(20); panner.positionZ.value = -1; expect(panner.positionZ.value).to.equal(-1); panner.dispose(); }); it("can get/set the orientation individually", () => { const panner = new Panner3D(); panner.orientationX.value = 2; expect(panner.orientationX.value).to.equal(2); panner.orientationY.value = 4; expect(panner.orientationY.value).to.equal(4); panner.orientationZ.value = -3; expect(panner.orientationZ.value).to.equal(-3); panner.dispose(); }); it("can get/set the position through setPosition", () => { const panner = new Panner3D(); panner.setPosition(3, -11, 2); expect(panner.positionX.value).to.equal(3); expect(panner.positionY.value).to.equal(-11); expect(panner.positionZ.value).to.equal(2); panner.dispose(); }); it("can get/set the orientation through setOrientation", () => { const panner = new Panner3D(); panner.setOrientation(2, -1, 0.5); expect(panner.orientationX.value).to.equal(2); expect(panner.orientationY.value).to.equal(-1); expect(panner.orientationZ.value).to.equal(0.5); panner.dispose(); }); it("can get/set all of the other attributes", () => { const values = { coneInnerAngle: 120, coneOuterAngle: 280, coneOuterGain: 0.3, distanceModel: "exponential", maxDistance: 10002, panningModel: "HRTF", refDistance: 0.3, rolloffFactor: 3, }; const panner = new Panner3D(); for (const v in values) { if (v in values) { panner[v] = values[v]; expect(panner[v]).to.equal(values[v]); } } panner.dispose(); }); }); ================================================ FILE: Tone/component/channel/Panner3D.ts ================================================ import "../../core/context/Listener.js"; import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Degrees, GainFactor } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; export interface Panner3DOptions extends ToneAudioNodeOptions { coneInnerAngle: Degrees; coneOuterAngle: Degrees; coneOuterGain: GainFactor; distanceModel: DistanceModelType; maxDistance: number; orientationX: number; orientationY: number; orientationZ: number; panningModel: PanningModelType; positionX: number; positionY: number; positionZ: number; refDistance: number; rolloffFactor: number; } /** * A spatialized panner node which supports equalpower or HRTF panning. * @category Component */ export class Panner3D extends ToneAudioNode { readonly name: string = "Panner3D"; /** * The panning object */ private _panner: PannerNode; readonly input: PannerNode; readonly output: PannerNode; readonly positionX: Param<"number">; readonly positionY: Param<"number">; readonly positionZ: Param<"number">; readonly orientationX: Param<"number">; readonly orientationY: Param<"number">; readonly orientationZ: Param<"number">; /** * @param positionX The initial x position. * @param positionY The initial y position. * @param positionZ The initial z position. */ constructor(positionX: number, positionY: number, positionZ: number); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Panner3D.getDefaults(), arguments, ["positionX", "positionY", "positionZ"] ); super(options); this._panner = this.input = this.output = this.context.createPanner(); // set some values this.panningModel = options.panningModel; this.maxDistance = options.maxDistance; this.distanceModel = options.distanceModel; this.coneOuterGain = options.coneOuterGain; this.coneOuterAngle = options.coneOuterAngle; this.coneInnerAngle = options.coneInnerAngle; this.refDistance = options.refDistance; this.rolloffFactor = options.rolloffFactor; this.positionX = new Param({ context: this.context, param: this._panner.positionX, value: options.positionX, }); this.positionY = new Param({ context: this.context, param: this._panner.positionY, value: options.positionY, }); this.positionZ = new Param({ context: this.context, param: this._panner.positionZ, value: options.positionZ, }); this.orientationX = new Param({ context: this.context, param: this._panner.orientationX, value: options.orientationX, }); this.orientationY = new Param({ context: this.context, param: this._panner.orientationY, value: options.orientationY, }); this.orientationZ = new Param({ context: this.context, param: this._panner.orientationZ, value: options.orientationZ, }); } static getDefaults(): Panner3DOptions { return Object.assign(ToneAudioNode.getDefaults(), { coneInnerAngle: 360, coneOuterAngle: 360, coneOuterGain: 0, distanceModel: "inverse" as DistanceModelType, maxDistance: 10000, orientationX: 0, orientationY: 0, orientationZ: 0, panningModel: "equalpower" as PanningModelType, positionX: 0, positionY: 0, positionZ: 0, refDistance: 1, rolloffFactor: 1, }); } /** * Sets the position of the source in 3d space. */ setPosition(x: number, y: number, z: number): this { this.positionX.value = x; this.positionY.value = y; this.positionZ.value = z; return this; } /** * Sets the orientation of the source in 3d space. */ setOrientation(x: number, y: number, z: number): this { this.orientationX.value = x; this.orientationY.value = y; this.orientationZ.value = z; return this; } /** * The panning model. Either "equalpower" or "HRTF". */ get panningModel(): PanningModelType { return this._panner.panningModel; } set panningModel(val) { this._panner.panningModel = val; } /** * A reference distance for reducing volume as source move further from the listener */ get refDistance(): number { return this._panner.refDistance; } set refDistance(val) { this._panner.refDistance = val; } /** * Describes how quickly the volume is reduced as source moves away from listener. */ get rolloffFactor(): number { return this._panner.rolloffFactor; } set rolloffFactor(val) { this._panner.rolloffFactor = val; } /** * The distance model used by, "linear", "inverse", or "exponential". */ get distanceModel(): DistanceModelType { return this._panner.distanceModel; } set distanceModel(val) { this._panner.distanceModel = val; } /** * The angle, in degrees, inside of which there will be no volume reduction */ get coneInnerAngle(): Degrees { return this._panner.coneInnerAngle; } set coneInnerAngle(val) { this._panner.coneInnerAngle = val; } /** * The angle, in degrees, outside of which the volume will be reduced * to a constant value of coneOuterGain */ get coneOuterAngle(): Degrees { return this._panner.coneOuterAngle; } set coneOuterAngle(val) { this._panner.coneOuterAngle = val; } /** * The gain outside of the coneOuterAngle */ get coneOuterGain(): GainFactor { return this._panner.coneOuterGain; } set coneOuterGain(val) { this._panner.coneOuterGain = val; } /** * The maximum distance between source and listener, * after which the volume will not be reduced any further. */ get maxDistance(): number { return this._panner.maxDistance; } set maxDistance(val) { this._panner.maxDistance = val; } dispose(): this { super.dispose(); this._panner.disconnect(); this.orientationX.dispose(); this.orientationY.dispose(); this.orientationZ.dispose(); this.positionX.dispose(); this.positionY.dispose(); this.positionZ.dispose(); return this; } } ================================================ FILE: Tone/component/channel/Recorder.test.ts ================================================ import { expect } from "chai"; import { connectFrom } from "../../../test/helper/Connect.js"; import { Context } from "../../core/context/Context.js"; import { ToneWithContext } from "../../core/context/ToneWithContext.js"; import { Synth } from "../../instrument/Synth.js"; import { Recorder } from "./Recorder.js"; describe("Recorder", () => { context("basic", () => { it("can be created and disposed", () => { const rec = new Recorder(); rec.dispose(); }); it("handles input connections", () => { const rec = new Recorder(); connectFrom().connect(rec); rec.dispose(); }); it("reports if it is supported or not", () => { expect(Recorder.supported).to.be.a("boolean"); }); it("can get the mime type", () => { const rec = new Recorder(); expect(rec.mimeType).to.be.a("string"); rec.dispose(); }); it("can set a different context", () => { const testContext = new Context(); const rec = new Recorder({ context: testContext, }); for (const member in rec) { if (rec[member] instanceof ToneWithContext) { expect(rec[member].context, `member: ${member}`).to.equal( testContext ); } } testContext.dispose(); rec.dispose(); return testContext.close(); }); }); function wait(time) { return new Promise((done) => setTimeout(done, time)); } context("start/stop/pause", () => { it("can be started", () => { const rec = new Recorder(); rec.start(); expect(rec.state).to.equal("started"); rec.dispose(); }); it("can be paused after starting", async () => { const rec = new Recorder(); rec.start(); expect(rec.state).to.equal("started"); await wait(100); rec.pause(); expect(rec.state).to.equal("paused"); rec.dispose(); }); it("can be resumed after pausing", async () => { const rec = new Recorder(); rec.start(); expect(rec.state).to.equal("started"); await wait(100); rec.pause(); expect(rec.state).to.equal("paused"); await wait(100); rec.start(); expect(rec.state).to.equal("started"); rec.dispose(); }); it("can be stopped after starting", async () => { const rec = new Recorder(); rec.start(); expect(rec.state).to.equal("started"); await wait(100); rec.stop(); expect(rec.state).to.equal("stopped"); rec.dispose(); }); it("throws an error if stopped or paused before starting", async () => { const rec = new Recorder(); let didThrow = false; try { await rec.stop(); } catch (e) { didThrow = true; } expect(didThrow).to.be.true; expect(() => { rec.pause(); }).to.throw(Error); rec.dispose(); }); it("stop returns a blob", async () => { const rec = new Recorder(); rec.start(); await wait(100); const recording = await rec.stop(); expect(recording).to.be.instanceOf(Blob); rec.dispose(); }); it("can record some sound", async () => { const rec = new Recorder(); const synth = new Synth().connect(rec); rec.start(); synth.triggerAttack("C3"); await wait(200); const recording = await rec.stop(); expect(recording.size).to.be.greaterThan(0); rec.dispose(); synth.dispose(); }); }); }); ================================================ FILE: Tone/component/channel/Recorder.ts ================================================ import { theWindow } from "../../core/context/AudioContext.js"; import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { assert } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { PlaybackState } from "../../core/util/StateTimeline.js"; export interface RecorderOptions extends ToneAudioNodeOptions { mimeType?: string; } /** * A wrapper around the MediaRecorder API. Unlike the rest of Tone.js, this module does not offer * any sample-accurate scheduling because it is not a feature of the MediaRecorder API. * This is only natively supported in Chrome and Firefox. * For a cross-browser shim, install [audio-recorder-polyfill](https://www.npmjs.com/package/audio-recorder-polyfill). * @example * const recorder = new Tone.Recorder(); * const synth = new Tone.Synth().connect(recorder); * // start recording * recorder.start(); * // generate a few notes * synth.triggerAttackRelease("C3", 0.5); * synth.triggerAttackRelease("C4", 0.5, "+1"); * synth.triggerAttackRelease("C5", 0.5, "+2"); * // wait for the notes to end and stop the recording * setTimeout(async () => { * // the recorded audio is returned as a blob * const recording = await recorder.stop(); * // download the recording by creating an anchor element and blob url * const url = URL.createObjectURL(recording); * const anchor = document.createElement("a"); * anchor.download = "recording.webm"; * anchor.href = url; * anchor.click(); * }, 4000); * @category Component */ export class Recorder extends ToneAudioNode { readonly name = "Recorder"; /** * Recorder uses the Media Recorder API */ private _recorder: MediaRecorder; /** * MediaRecorder requires */ private _stream: MediaStreamAudioDestinationNode; readonly input: Gain; readonly output: undefined; constructor(options?: Partial); constructor() { const options = optionsFromArguments(Recorder.getDefaults(), arguments); super(options); this.input = new Gain({ context: this.context, }); assert(Recorder.supported, "Media Recorder API is not available"); this._stream = this.context.createMediaStreamDestination(); this.input.connect(this._stream); this._recorder = new MediaRecorder(this._stream.stream, { mimeType: options.mimeType, }); } static getDefaults(): RecorderOptions { return ToneAudioNode.getDefaults(); } /** * The mime type is the format that the audio is encoded in. For Chrome * that is typically webm encoded as "vorbis". */ get mimeType(): string { return this._recorder.mimeType; } /** * Test if your platform supports the Media Recorder API. If it's not available, * try installing this (polyfill)[https://www.npmjs.com/package/audio-recorder-polyfill]. */ static get supported(): boolean { return theWindow !== null && Reflect.has(theWindow, "MediaRecorder"); } /** * Get the playback state of the Recorder, either "started", "stopped" or "paused" */ get state(): PlaybackState { if (this._recorder.state === "inactive") { return "stopped"; } else if (this._recorder.state === "paused") { return "paused"; } else { return "started"; } } /** * Start/Resume the Recorder. Returns a promise which resolves * when the recorder has started. */ async start() { assert(this.state !== "started", "Recorder is already started"); const startPromise = new Promise((done) => { const handleStart = () => { this._recorder.removeEventListener("start", handleStart, false); done(); }; this._recorder.addEventListener("start", handleStart, false); }); if (this.state === "stopped") { this._recorder.start(); } else { this._recorder.resume(); } return await startPromise; } /** * Stop the recorder. Returns a promise with the recorded content until this point * encoded as {@link mimeType} */ async stop(): Promise { assert(this.state !== "stopped", "Recorder is not started"); const dataPromise: Promise = new Promise((done) => { const handleData = (e: BlobEvent) => { this._recorder.removeEventListener( "dataavailable", handleData, false ); done(e.data); }; this._recorder.addEventListener("dataavailable", handleData, false); }); this._recorder.stop(); return await dataPromise; } /** * Pause the recorder */ pause(): this { assert(this.state === "started", "Recorder must be started"); this._recorder.pause(); return this; } dispose(): this { super.dispose(); this.input.dispose(); this._stream.disconnect(); return this; } } ================================================ FILE: Tone/component/channel/Solo.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { ConstantOutput } from "../../../test/helper/ConstantOutput.js"; import { Signal } from "../../signal/Signal.js"; import { Solo } from "./Solo.js"; describe("Solo", () => { BasicTests(Solo); context("Soloing", () => { it("can be soloed an unsoloed", () => { const sol = new Solo(); sol.solo = true; expect(sol.solo).to.be.true; sol.solo = false; expect(sol.solo).to.be.false; sol.dispose(); }); it("can be passed into the constructor", () => { const sol = new Solo(true); expect(sol.solo).to.be.true; sol.dispose(); }); it("can be passed into the constructor with an object", () => { const sol = new Solo({ solo: true }); expect(sol.solo).to.be.true; sol.dispose(); }); it("other instances are unsoloed when one is soloed", () => { const solA = new Solo(); const solB = new Solo(); solA.solo = true; solB.solo = false; expect(solA.solo).to.be.true; expect(solB.solo).to.be.false; solB.solo = true; expect(solA.solo).to.be.true; expect(solB.solo).to.be.true; solA.solo = false; expect(solA.solo).to.be.false; expect(solB.solo).to.be.true; solA.dispose(); solB.dispose(); }); it("other instances report themselves as muted", () => { const solA = new Solo(); const solB = new Solo(); solA.solo = true; solB.solo = false; expect(solA.muted).to.be.false; expect(solB.muted).to.be.true; solA.dispose(); solB.dispose(); }); it("all instances are unmuted when there is no solo", () => { const solA = new Solo(); const solB = new Solo(); solA.solo = true; solB.solo = false; solA.solo = false; expect(solA.muted).to.be.false; expect(solB.muted).to.be.false; solA.dispose(); solB.dispose(); }); it("a newly created instance will be muted if there is already a soloed instance", () => { const solA = new Solo(true); const solB = new Solo(); expect(solA.muted).to.be.false; expect(solB.muted).to.be.true; solA.dispose(); solB.dispose(); }); it("passes both signals when nothing is soloed", () => { return ConstantOutput( () => { const soloA = new Solo().toDestination(); const soloB = new Solo().toDestination(); new Signal(10).connect(soloA); new Signal(20).connect(soloB); }, 30, 0.01 ); }); it("passes one signal when it is soloed", () => { return ConstantOutput( () => { const soloA = new Solo().toDestination(); const soloB = new Solo().toDestination(); new Signal(10).connect(soloA); new Signal(20).connect(soloB); soloA.solo = true; }, 10, 0.01 ); }); it("can solo multiple at once", () => { return ConstantOutput( () => { const soloA = new Solo().toDestination(); const soloB = new Solo().toDestination(); new Signal(10).connect(soloA); new Signal(20).connect(soloB); soloA.solo = true; soloB.solo = true; }, 30, 0.01 ); }); it("can unsolo all", () => { return ConstantOutput( () => { const soloA = new Solo().toDestination(); const soloB = new Solo().toDestination(); new Signal(10).connect(soloA); new Signal(20).connect(soloB); soloA.solo = true; soloB.solo = true; soloA.solo = false; soloB.solo = false; }, 30, 0.01 ); }); it("can solo and unsolo while keeping previous soloed", () => { return ConstantOutput( () => { const soloA = new Solo().toDestination(); const soloB = new Solo().toDestination(); new Signal(10).connect(soloA); new Signal(20).connect(soloB); soloA.solo = true; soloB.solo = true; soloB.solo = false; }, 10, 0.01 ); }); }); }); ================================================ FILE: Tone/component/channel/Solo.ts ================================================ import { BaseContext } from "../../core/context/BaseContext.js"; import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; export interface SoloOptions extends ToneAudioNodeOptions { solo: boolean; } /** * Solo lets you isolate a specific audio stream. When an instance is set to `solo=true`, * it will mute all other instances of Solo. * @example * const soloA = new Tone.Solo().toDestination(); * const oscA = new Tone.Oscillator("C4", "sawtooth").connect(soloA); * const soloB = new Tone.Solo().toDestination(); * const oscB = new Tone.Oscillator("E4", "square").connect(soloB); * soloA.solo = true; * // no audio will pass through soloB * @category Component */ export class Solo extends ToneAudioNode { readonly name: string = "Solo"; readonly input: Gain; readonly output: Gain; /** * @param solo If the connection should be initially solo'ed. */ constructor(solo?: boolean); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Solo.getDefaults(), arguments, [ "solo", ]); super(options); this.input = this.output = new Gain({ context: this.context, }); if (!Solo._allSolos.has(this.context)) { Solo._allSolos.set(this.context, new Set()); } (Solo._allSolos.get(this.context) as Set).add(this); // set initially this.solo = options.solo; } static getDefaults(): SoloOptions { return Object.assign(ToneAudioNode.getDefaults(), { solo: false, }); } /** * Hold all of the solo'ed tracks belonging to a specific context */ private static _allSolos: Map> = new Map(); /** * Hold the currently solo'ed instance(s) */ private static _soloed: Map> = new Map(); /** * Isolates this instance and mutes all other instances of Solo. * Only one instance can be soloed at a time. A soloed * instance will report `solo=false` when another instance is soloed. */ get solo(): boolean { return this._isSoloed(); } set solo(solo) { if (solo) { this._addSolo(); } else { this._removeSolo(); } (Solo._allSolos.get(this.context) as Set).forEach((instance) => instance._updateSolo() ); } /** * If the current instance is muted, i.e. another instance is soloed */ get muted(): boolean { return this.input.gain.value === 0; } /** * Add this to the soloed array */ private _addSolo(): void { if (!Solo._soloed.has(this.context)) { Solo._soloed.set(this.context, new Set()); } (Solo._soloed.get(this.context) as Set).add(this); } /** * Remove this from the soloed array */ private _removeSolo(): void { if (Solo._soloed.has(this.context)) { (Solo._soloed.get(this.context) as Set).delete(this); } } /** * Is this on the soloed array */ private _isSoloed(): boolean { return ( Solo._soloed.has(this.context) && (Solo._soloed.get(this.context) as Set).has(this) ); } /** * Returns true if no one is soloed */ private _noSolos(): boolean { // either does not have any soloed added return ( !Solo._soloed.has(this.context) || // or has a solo set but doesn't include any items (Solo._soloed.has(this.context) && (Solo._soloed.get(this.context) as Set).size === 0) ); } /** * Solo the current instance and unsolo all other instances. */ private _updateSolo(): void { if (this._isSoloed()) { this.input.gain.value = 1; } else if (this._noSolos()) { // no one is soloed this.input.gain.value = 1; } else { this.input.gain.value = 0; } } dispose(): this { super.dispose(); (Solo._allSolos.get(this.context) as Set).delete(this); this._removeSolo(); return this; } } ================================================ FILE: Tone/component/channel/Split.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectTo } from "../../../test/helper/Connect.js"; import { ConstantOutput } from "../../../test/helper/ConstantOutput.js"; import { StereoSignal } from "../../../test/helper/StereoSignal.js"; import { Split } from "./Split.js"; describe("Split", () => { BasicTests(Split); context("Splitting", () => { it("defaults to two channels", () => { const split = new Split(); expect(split.numberOfOutputs).to.equal(2); split.dispose(); }); it("can pass in more channels", () => { const split = new Split(4); expect(split.numberOfOutputs).to.equal(4); split.connect(connectTo(), 0, 0); split.connect(connectTo(), 1, 0); split.connect(connectTo(), 2, 0); split.connect(connectTo(), 3, 0); split.dispose(); }); it("passes the incoming signal through on the left side", () => { return ConstantOutput(({ destination }) => { const split = new Split(); const signal = StereoSignal(1, 2).connect(split); split.connect(destination, 0, 0); }, 1); }); it("passes the incoming signal through on the right side", () => { return ConstantOutput(({ destination }) => { const split = new Split(); const signal = StereoSignal(1, 2).connect(split); split.connect(destination, 1, 0); }, 2); }); // it("merges two signal into one stereo signal and then split them back into two signals on left side", () => { // return ConstantOutput(({destination}) => { // const split = new Split(); // const signal = StereoSignal(1, 2).connect(split); // split.connect(destination, 0, 0); // }, 1); // }); // it("merges two signal into one stereo signal and then split them back into two signals on right side", () => { // return ConstantOutput(({destination}) => { // const split = new Split(); // const signal = StereoSignal(1, 2).connect(split); // split.connect(destination, 1, 0); // }, 2); // }); }); }); ================================================ FILE: Tone/component/channel/Split.ts ================================================ import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; interface SplitOptions extends ToneAudioNodeOptions { channels: number; } /** * Split splits an incoming signal into the number of given channels. * * @example * const split = new Tone.Split(); * // stereoSignal.connect(split); * @category Component */ export class Split extends ToneAudioNode { readonly name: string = "Split"; /** * The splitting node */ private _splitter: ChannelSplitterNode; readonly input: ChannelSplitterNode; readonly output: ChannelSplitterNode; /** * @param channels The number of channels to merge. */ constructor(channels?: number); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Split.getDefaults(), arguments, [ "channels", ]); super(options); this._splitter = this.input = this.output = this.context.createChannelSplitter(options.channels); this._internalChannels = [this._splitter]; } static getDefaults(): SplitOptions { return Object.assign(ToneAudioNode.getDefaults(), { channels: 2, }); } dispose(): this { super.dispose(); this._splitter.disconnect(); return this; } } ================================================ FILE: Tone/component/channel/Volume.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Signal } from "../../signal/Signal.js"; import { Volume } from "./Volume.js"; describe("Volume", () => { BasicTests(Volume); context("Volume", () => { it("handles input and output connections", () => { const vol = new Volume(); vol.connect(connectTo()); connectFrom().connect(vol); connectFrom().connect(vol.volume); vol.dispose(); }); it("can be constructed with volume value", () => { const vol = new Volume(-12); expect(vol.volume.value).to.be.closeTo(-12, 0.1); vol.dispose(); }); it("can be constructed with an options object", () => { const vol = new Volume({ volume: 2, }); expect(vol.volume.value).to.be.closeTo(2, 0.1); vol.dispose(); }); it("can be constructed with an options object and muted", () => { const vol = new Volume({ mute: true, }); expect(vol.mute).to.equal(true); vol.dispose(); }); it("can set/get with an object", () => { const vol = new Volume(); vol.set({ volume: -10, }); expect(vol.get().volume).to.be.closeTo(-10, 0.1); vol.dispose(); }); it("unmuting returns to previous volume", () => { const vol = new Volume(-10); vol.mute = true; expect(vol.mute).to.equal(true); expect(vol.volume.value).to.equal(-Infinity); vol.mute = false; // returns the volume to what it was expect(vol.volume.value).to.be.closeTo(-10, 0.1); vol.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const vol = new Volume().toDestination(); input.connect(vol); }); }); it.skip("passes the incoming stereo signal through", () => { // return PassAudioStereo(function(input) { // const vol = new Volume().toDestination(); // input.connect(vol); // }); }); it("can lower the volume", async () => { const buffer = await Offline(() => { const vol = new Volume(-10).toDestination(); new Signal(1).connect(vol); }); expect(buffer.value()).to.be.closeTo(0.315, 0.01); }); it("can mute the volume", async () => { const buffer = await Offline(() => { const vol = new Volume(0).toDestination(); new Signal(1).connect(vol); vol.mute = true; }); expect(buffer.isSilent()).to.equal(true); }); it("muted when volume is set to -Infinity", async () => { const buffer = await Offline(() => { const vol = new Volume(-Infinity).toDestination(); new Signal(1).connect(vol); expect(vol.mute).to.equal(true); }); expect(buffer.isSilent()).to.equal(true); }); it("setting the volume unmutes it and reports itself as unmuted", () => { const vol = new Volume(0).toDestination(); vol.mute = true; expect(vol.mute).to.equal(true); vol.volume.value = 0; expect(vol.mute).is.equal(false); vol.dispose(); }); it("multiple calls to mute still return the vol to the original", () => { const vol = new Volume(-20); vol.mute = true; vol.mute = true; expect(vol.mute).to.equal(true); expect(vol.volume.value).to.equal(-Infinity); vol.mute = false; vol.mute = false; expect(vol.volume.value).to.be.closeTo(-20, 0.5); vol.dispose(); }); }); }); ================================================ FILE: Tone/component/channel/Volume.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Decibels } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; interface VolumeOptions extends ToneAudioNodeOptions { volume: Decibels; mute: boolean; } /** * Volume is a simple volume node, useful for creating a volume fader. * * @example * const vol = new Tone.Volume(-12).toDestination(); * const osc = new Tone.Oscillator().connect(vol).start(); * @category Component */ export class Volume extends ToneAudioNode { readonly name: string = "Volume"; /** * the output node */ output: Gain<"decibels">; /** * Input and output are the same */ input: Gain<"decibels">; /** * The unmuted volume */ private _unmutedVolume: Decibels; /** * The volume control in decibels. * @example * const vol = new Tone.Volume().toDestination(); * const osc = new Tone.Oscillator().connect(vol).start(); * vol.volume.value = -20; */ volume: Param<"decibels">; /** * @param volume the initial volume in decibels */ constructor(volume?: Decibels); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Volume.getDefaults(), arguments, [ "volume", ]); super(options); this.input = this.output = new Gain({ context: this.context, gain: options.volume, units: "decibels", }); this.volume = this.output.gain; readOnly(this, "volume"); this._unmutedVolume = options.volume; // set the mute initially this.mute = options.mute; } static getDefaults(): VolumeOptions { return Object.assign(ToneAudioNode.getDefaults(), { mute: false, volume: 0, }); } /** * Mute the output. * @example * const vol = new Tone.Volume(-12).toDestination(); * const osc = new Tone.Oscillator().connect(vol).start(); * // mute the output * vol.mute = true; */ get mute(): boolean { return this.volume.value === -Infinity; } set mute(mute: boolean) { if (!this.mute && mute) { this._unmutedVolume = this.volume.value; // maybe it should ramp here? this.volume.value = -Infinity; } else if (this.mute && !mute) { this.volume.value = this._unmutedVolume; } } /** * clean up */ dispose(): this { super.dispose(); this.input.dispose(); this.volume.dispose(); return this; } } ================================================ FILE: Tone/component/dynamics/Compressor.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Compressor } from "./Compressor.js"; describe("Compressor", () => { BasicTests(Compressor); context("Compression", () => { it("passes the incoming signal through", () => { return PassAudio((input) => { const comp = new Compressor().toDestination(); input.connect(comp); }); }); it("can be get and set through object", () => { const comp = new Compressor(); const values = { attack: 0.03, knee: 20, ratio: 12, release: 0.5, threshold: -30, }; comp.set(values); expect(comp.get()).to.have.keys([ "ratio", "threshold", "release", "attack", "ratio", ]); comp.dispose(); }); it("can be get and constructed with an object", () => { const comp = new Compressor({ attack: 0.03, knee: 20, ratio: 12, release: 0.5, threshold: -30, }); expect(comp.threshold.value).to.have.be.closeTo(-30, 1); comp.dispose(); }); it("can be constructed with args", () => { const comp = new Compressor(-10, 4); expect(comp.threshold.value).to.have.be.closeTo(-10, 0.1); expect(comp.ratio.value).to.have.be.closeTo(4, 0.1); comp.dispose(); }); it("params have correct min and max values", () => { const comp = new Compressor(-10, 4); expect(comp.threshold.minValue).to.equal(-100); expect(comp.threshold.maxValue).to.equal(0); expect(comp.attack.minValue).to.equal(0); expect(comp.attack.maxValue).to.equal(1); expect(comp.release.minValue).to.equal(0); expect(comp.release.maxValue).to.equal(1); comp.dispose(); }); it("can get/set all interfaces", () => { const comp = new Compressor(); const values = { attack: 0.03, knee: 18, ratio: 12, release: 0.5, threshold: -30, }; comp.ratio.value = values.ratio; comp.threshold.value = values.threshold; comp.release.value = values.release; comp.attack.value = values.attack; comp.knee.value = values.knee; expect(comp.ratio.value).to.equal(values.ratio); expect(comp.threshold.value).to.equal(values.threshold); expect(comp.release.value).to.equal(values.release); expect(comp.attack.value).to.be.closeTo(values.attack, 0.01); expect(comp.knee.value).to.equal(values.knee); comp.dispose(); }); }); }); ================================================ FILE: Tone/component/dynamics/Compressor.ts ================================================ import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Decibels, Positive, Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; export interface CompressorOptions extends ToneAudioNodeOptions { attack: Time; knee: Decibels; ratio: Positive; release: Time; threshold: Decibels; } /** * Compressor is a thin wrapper around the Web Audio * [DynamicsCompressorNode](http://webaudio.github.io/web-audio-api/#the-dynamicscompressornode-interface). * Compression reduces the volume of loud sounds or amplifies quiet sounds * by narrowing or "compressing" an audio signal's dynamic range. * Read more on [Wikipedia](https://en.wikipedia.org/wiki/Dynamic_range_compression). * @example * const comp = new Tone.Compressor(-30, 3); * @category Component */ export class Compressor extends ToneAudioNode { readonly name: string = "Compressor"; /** * the compressor node */ private _compressor: DynamicsCompressorNode = this.context.createDynamicsCompressor(); readonly input = this._compressor; readonly output = this._compressor; /** * The decibel value above which the compression will start taking effect. * @min -100 * @max 0 */ readonly threshold: Param<"decibels">; /** * The amount of time (in seconds) to reduce the gain by 10dB. * @min 0 * @max 1 */ readonly attack: Param<"time">; /** * The amount of time (in seconds) to increase the gain by 10dB. * @min 0 * @max 1 */ readonly release: Param<"time">; /** * A decibel value representing the range above the threshold where the * curve smoothly transitions to the "ratio" portion. * @min 0 * @max 40 */ readonly knee: Param<"decibels">; /** * The amount of dB change in input for a 1 dB change in output. * @min 1 * @max 20 */ readonly ratio: Param<"positive">; /** * @param threshold The value above which the compression starts to be applied. * @param ratio The gain reduction ratio. */ constructor(threshold?: Decibels, ratio?: Positive); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Compressor.getDefaults(), arguments, ["threshold", "ratio"] ); super(options); this.threshold = new Param({ minValue: this._compressor.threshold.minValue, maxValue: this._compressor.threshold.maxValue, context: this.context, convert: false, param: this._compressor.threshold, units: "decibels", value: options.threshold, }); this.attack = new Param({ minValue: this._compressor.attack.minValue, maxValue: this._compressor.attack.maxValue, context: this.context, param: this._compressor.attack, units: "time", value: options.attack, }); this.release = new Param({ minValue: this._compressor.release.minValue, maxValue: this._compressor.release.maxValue, context: this.context, param: this._compressor.release, units: "time", value: options.release, }); this.knee = new Param({ minValue: this._compressor.knee.minValue, maxValue: this._compressor.knee.maxValue, context: this.context, convert: false, param: this._compressor.knee, units: "decibels", value: options.knee, }); this.ratio = new Param({ minValue: this._compressor.ratio.minValue, maxValue: this._compressor.ratio.maxValue, context: this.context, convert: false, param: this._compressor.ratio, units: "positive", value: options.ratio, }); // set the defaults readOnly(this, ["knee", "release", "attack", "ratio", "threshold"]); } static getDefaults(): CompressorOptions { return Object.assign(ToneAudioNode.getDefaults(), { attack: 0.003, knee: 30, ratio: 12, release: 0.25, threshold: -24, }); } /** * A read-only decibel value for metering purposes, representing the current amount of gain * reduction that the compressor is applying to the signal. If fed no signal the value will be 0 (no gain reduction). */ get reduction(): Decibels { return this._compressor.reduction; } dispose(): this { super.dispose(); this._compressor.disconnect(); this.attack.dispose(); this.release.dispose(); this.threshold.dispose(); this.ratio.dispose(); this.knee.dispose(); return this; } } ================================================ FILE: Tone/component/dynamics/Gate.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { CompareToFile } from "../../../test/helper/CompareToFile.js"; import { Offline } from "../../../test/helper/Offline.js"; import { Signal } from "../../signal/Signal.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { Gate } from "./Gate.js"; describe("Gate", () => { BasicTests(Gate); it.only("matches a file", () => { return CompareToFile( () => { const gate = new Gate(-10, 0.1).toDestination(); const osc = new Oscillator().connect(gate); osc.start(0); osc.volume.value = -100; osc.volume.exponentialRampToValueAtTime(0, 0.5); }, "gate.wav", 0.18 ); }); context("Signal Gating", () => { it("handles getter/setter as Object", () => { const gate = new Gate(); const values = { smoothing: 0.2, threshold: -20, }; gate.set(values); expect(gate.get().smoothing).to.be.closeTo(0.2, 0.001); expect(gate.get().threshold).to.be.closeTo(-20, 0.1); gate.dispose(); }); it("can be constructed with an object", () => { const gate = new Gate({ smoothing: 0.3, threshold: -5, }); expect(gate.smoothing).to.be.closeTo(0.3, 0.001); expect(gate.threshold).to.be.closeTo(-5, 0.1); gate.dispose(); }); it("gates the incoming signal when below the threshold", async () => { const buffer = await Offline(() => { const gate = new Gate(-9); const sig = new Signal(-12, "decibels"); sig.connect(gate); gate.toDestination(); }); expect(buffer.isSilent()).to.be.true; }); it("passes the incoming signal when above the threshold", async () => { const buffer = await Offline(() => { const gate = new Gate(-11); const sig = new Signal(-10, "decibels"); sig.connect(gate); gate.toDestination(); }); expect(buffer.min()).to.be.above(0); }); }); }); ================================================ FILE: Tone/component/dynamics/Gate.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { dbToGain, gainToDb } from "../../core/type/Conversions.js"; import { Decibels, Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { GreaterThan } from "../../signal/GreaterThan.js"; import { Follower } from "../analysis/Follower.js"; export interface GateOptions extends ToneAudioNodeOptions { threshold: Decibels; smoothing: Time; } /** * Gate only passes a signal through when the incoming * signal exceeds a specified threshold. It uses {@link Follower} to follow the ampltiude * of the incoming signal and compares it to the {@link threshold} value using {@link GreaterThan}. * * @example * const gate = new Tone.Gate(-30, 0.2).toDestination(); * const mic = new Tone.UserMedia().connect(gate); * // the gate will only pass through the incoming * // signal when it's louder than -30db * @category Component */ export class Gate extends ToneAudioNode { readonly name: string = "Gate"; readonly input: ToneAudioNode; readonly output: ToneAudioNode; /** * Follow the incoming signal */ private _follower: Follower; /** * Test if it's greater than the threshold */ private _gt: GreaterThan; /** * Gate the incoming signal when it does not exceed the threshold */ private _gate: Gain; /** * @param threshold The threshold above which the gate will open. * @param smoothing The follower's smoothing time */ constructor(threshold?: Decibels, smoothing?: Time); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Gate.getDefaults(), arguments, [ "threshold", "smoothing", ]); super(options); this._follower = new Follower({ context: this.context, smoothing: options.smoothing, }); this._gt = new GreaterThan({ context: this.context, value: dbToGain(options.threshold), }); this.input = new Gain({ context: this.context }); this._gate = this.output = new Gain({ context: this.context }); // connections this.input.connect(this._gate); // the control signal this.input.chain(this._follower, this._gt, this._gate.gain); } static getDefaults(): GateOptions { return Object.assign(ToneAudioNode.getDefaults(), { smoothing: 0.1, threshold: -40, }); } /** * The threshold of the gate in decibels */ get threshold(): Decibels { return gainToDb(this._gt.value); } set threshold(thresh) { this._gt.value = dbToGain(thresh); } /** * The attack/decay speed of the gate. * @see {@link Follower.smoothing} */ get smoothing(): Time { return this._follower.smoothing; } set smoothing(smoothingTime) { this._follower.smoothing = smoothingTime; } dispose(): this { super.dispose(); this.input.dispose(); this._follower.dispose(); this._gt.dispose(); this._gate.dispose(); return this; } } ================================================ FILE: Tone/component/dynamics/Limiter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Limiter } from "./Limiter.js"; describe("Limiter", () => { BasicTests(Limiter); context("Limiting", () => { it("passes the incoming signal through", () => { return PassAudio((input) => { const limiter = new Limiter().toDestination(); input.connect(limiter); }); }); it("can be get and set through object", () => { const limiter = new Limiter(); const values = { threshold: -30, }; limiter.set(values); expect(limiter.get().threshold).to.be.closeTo(-30, 0.1); limiter.dispose(); }); it("can set the threshold", () => { const limiter = new Limiter(); limiter.threshold.value = -10; expect(limiter.threshold.value).to.be.closeTo(-10, 0.1); limiter.dispose(); }); it("reduction is 0 when not connected", () => { const limiter = new Limiter(); expect(limiter.reduction).to.be.closeTo(0, 0.01); limiter.dispose(); }); }); }); ================================================ FILE: Tone/component/dynamics/Limiter.ts ================================================ import { Param } from "../../core/context/Param.js"; import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Decibels } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly } from "../../core/util/Interface.js"; import { Compressor } from "./Compressor.js"; export interface LimiterOptions extends ToneAudioNodeOptions { threshold: Decibels; } /** * Limiter will limit the loudness of an incoming signal. * Under the hood its composed of a {@link Compressor} with a fast attack * and release and max compression ratio. * * @example * const limiter = new Tone.Limiter(-20).toDestination(); * const oscillator = new Tone.Oscillator().connect(limiter); * oscillator.start(); * @category Component */ export class Limiter extends ToneAudioNode { readonly name: string = "Limiter"; readonly input: InputNode; readonly output: OutputNode; /** * The compressor which does the limiting */ private _compressor: Compressor; readonly threshold: Param<"decibels">; /** * @param threshold The threshold above which the gain reduction is applied. */ constructor(threshold?: Decibels); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Limiter.getDefaults(), arguments, [ "threshold", ]); super(options); this._compressor = this.input = this.output = new Compressor({ context: this.context, ratio: 20, attack: 0.003, release: 0.01, threshold: options.threshold, }); this.threshold = this._compressor.threshold; readOnly(this, "threshold"); } static getDefaults(): LimiterOptions { return Object.assign(ToneAudioNode.getDefaults(), { threshold: -12, }); } /** * A read-only decibel value for metering purposes, representing the current amount of gain * reduction that the compressor is applying to the signal. */ get reduction(): Decibels { return this._compressor.reduction; } dispose(): this { super.dispose(); this._compressor.dispose(); this.threshold.dispose(); return this; } } ================================================ FILE: Tone/component/dynamics/MidSideCompressor.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { MidSideCompressor } from "./MidSideCompressor.js"; describe("MidSideCompressor", () => { BasicTests(MidSideCompressor); context("Compression", () => { it("passes the incoming signal through", () => { return PassAudio((input) => { const comp = new MidSideCompressor().toDestination(); input.connect(comp); }); }); it("can be get and set through object", () => { const comp = new MidSideCompressor(); const values = { mid: { ratio: 16, threshold: -30, }, side: { release: 0.5, attack: 0.03, knee: 20, }, }; comp.set(values); expect(comp.get()).to.have.keys(["mid", "side"]); expect(comp.get().mid.ratio).be.closeTo(16, 0.01); expect(comp.get().side.release).be.closeTo(0.5, 0.01); comp.dispose(); }); it("can be constructed with an options object", () => { const comp = new MidSideCompressor({ mid: { ratio: 16, threshold: -30, }, side: { release: 0.5, attack: 0.03, knee: 20, }, }); expect(comp.mid.ratio.value).be.closeTo(16, 0.01); expect(comp.mid.threshold.value).be.closeTo(-30, 0.01); expect(comp.side.release.value).be.closeTo(0.5, 0.01); expect(comp.side.attack.value).be.closeTo(0.03, 0.01); comp.dispose(); }); }); }); ================================================ FILE: Tone/component/dynamics/MidSideCompressor.ts ================================================ import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, RecursivePartial } from "../../core/util/Interface.js"; import { MidSideMerge } from "../channel/MidSideMerge.js"; import { MidSideSplit } from "../channel/MidSideSplit.js"; import { Compressor, CompressorOptions } from "./Compressor.js"; export interface MidSideCompressorOptions extends ToneAudioNodeOptions { mid: Omit; side: Omit; } /** * MidSideCompressor applies two different compressors to the {@link mid} * and {@link side} signal components of the input. * @see {@link MidSideSplit} and {@link MidSideMerge}. * @category Component */ export class MidSideCompressor extends ToneAudioNode { readonly name: string = "MidSideCompressor"; readonly input: InputNode; readonly output: OutputNode; /** * Split the incoming signal into Mid/Side */ private _midSideSplit: MidSideSplit; /** * Merge the compressed signal back into a single stream */ private _midSideMerge: MidSideMerge; /** * The compression applied to the mid signal */ readonly mid: Compressor; /** * The compression applied to the side signal */ readonly side: Compressor; constructor(options?: RecursivePartial); constructor() { const options = optionsFromArguments( MidSideCompressor.getDefaults(), arguments ); super(options); this._midSideSplit = this.input = new MidSideSplit({ context: this.context, }); this._midSideMerge = this.output = new MidSideMerge({ context: this.context, }); this.mid = new Compressor( Object.assign(options.mid, { context: this.context }) ); this.side = new Compressor( Object.assign(options.side, { context: this.context }) ); this._midSideSplit.mid.chain(this.mid, this._midSideMerge.mid); this._midSideSplit.side.chain(this.side, this._midSideMerge.side); readOnly(this, ["mid", "side"]); } static getDefaults(): MidSideCompressorOptions { return Object.assign(ToneAudioNode.getDefaults(), { mid: { ratio: 3, threshold: -24, release: 0.03, attack: 0.02, knee: 16, }, side: { ratio: 6, threshold: -30, release: 0.25, attack: 0.03, knee: 10, }, }); } dispose(): this { super.dispose(); this.mid.dispose(); this.side.dispose(); this._midSideSplit.dispose(); this._midSideMerge.dispose(); return this; } } ================================================ FILE: Tone/component/dynamics/MultibandCompressor.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { MultibandCompressor } from "./MultibandCompressor.js"; describe("MultibandCompressor", () => { BasicTests(MultibandCompressor); context("Compression", () => { it("passes the incoming signal through", () => { return PassAudio((input) => { const comp = new MultibandCompressor().toDestination(); input.connect(comp); }); }); it("can be get and set through object", () => { const comp = new MultibandCompressor(); const values = { mid: { ratio: 16, threshold: -30, }, high: { release: 0.5, attack: 0.03, knee: 20, }, }; comp.set(values); expect(comp.get()).to.have.keys([ "low", "mid", "high", "lowFrequency", "highFrequency", ]); expect(comp.get().mid.ratio).be.closeTo(16, 0.01); expect(comp.get().high.release).be.closeTo(0.5, 0.01); comp.dispose(); }); it("can be constructed with an options object", () => { const comp = new MultibandCompressor({ mid: { ratio: 16, threshold: -30, }, lowFrequency: 100, }); expect(comp.mid.ratio.value).be.closeTo(16, 0.01); expect(comp.mid.threshold.value).be.closeTo(-30, 0.01); expect(comp.lowFrequency.value).be.closeTo(100, 0.01); comp.dispose(); }); }); }); ================================================ FILE: Tone/component/dynamics/MultibandCompressor.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { InputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Frequency } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, RecursivePartial } from "../../core/util/Interface.js"; import { Signal } from "../../signal/Signal.js"; import { MultibandSplit } from "../channel/MultibandSplit.js"; import { Compressor, CompressorOptions } from "./Compressor.js"; export interface MultibandCompressorOptions extends ToneAudioNodeOptions { mid: Omit; low: Omit; high: Omit; lowFrequency: Frequency; highFrequency: Frequency; } /** * A compressor with separate controls over low/mid/high dynamics. * @see {@link Compressor} and {@link MultibandSplit} * * @example * const multiband = new Tone.MultibandCompressor({ * lowFrequency: 200, * highFrequency: 1300, * low: { * threshold: -12 * } * }); * @category Component */ export class MultibandCompressor extends ToneAudioNode { readonly name: string = "MultibandCompressor"; readonly input: InputNode; readonly output: ToneAudioNode; /** * Split the incoming signal into high/mid/low */ private _splitter: MultibandSplit; /** * low/mid crossover frequency. */ readonly lowFrequency: Signal<"frequency">; /** * mid/high crossover frequency. */ readonly highFrequency: Signal<"frequency">; /** * The compressor applied to the low frequencies */ readonly low: Compressor; /** * The compressor applied to the mid frequencies */ readonly mid: Compressor; /** * The compressor applied to the high frequencies */ readonly high: Compressor; constructor(options?: RecursivePartial); constructor() { const options = optionsFromArguments( MultibandCompressor.getDefaults(), arguments ); super(options); this._splitter = this.input = new MultibandSplit({ context: this.context, lowFrequency: options.lowFrequency, highFrequency: options.highFrequency, }); this.lowFrequency = this._splitter.lowFrequency; this.highFrequency = this._splitter.highFrequency; this.output = new Gain({ context: this.context }); this.low = new Compressor( Object.assign(options.low, { context: this.context }) ); this.mid = new Compressor( Object.assign(options.mid, { context: this.context }) ); this.high = new Compressor( Object.assign(options.high, { context: this.context }) ); // connect the compressor this._splitter.low.chain(this.low, this.output); this._splitter.mid.chain(this.mid, this.output); this._splitter.high.chain(this.high, this.output); readOnly(this, ["high", "mid", "low", "highFrequency", "lowFrequency"]); } static getDefaults(): MultibandCompressorOptions { return Object.assign(ToneAudioNode.getDefaults(), { lowFrequency: 250, highFrequency: 2000, low: { ratio: 6, threshold: -30, release: 0.25, attack: 0.03, knee: 10, }, mid: { ratio: 3, threshold: -24, release: 0.03, attack: 0.02, knee: 16, }, high: { ratio: 3, threshold: -24, release: 0.03, attack: 0.02, knee: 16, }, }); } dispose(): this { super.dispose(); this._splitter.dispose(); this.low.dispose(); this.mid.dispose(); this.high.dispose(); this.output.dispose(); return this; } } ================================================ FILE: Tone/component/envelope/AmplitudeEnvelope.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { CompareToFile } from "../../../test/helper/CompareToFile.js"; import { Offline } from "../../../test/helper/Offline.js"; import { Signal } from "../../signal/Signal.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { AmplitudeEnvelope } from "./AmplitudeEnvelope.js"; import { Envelope } from "./Envelope.js"; describe("AmplitudeEnvelope", () => { BasicTests(AmplitudeEnvelope); context("Comparisons", () => { it("matches a file", () => { return CompareToFile(() => { const ampEnv = new AmplitudeEnvelope({ attack: 0.1, decay: 0.2, release: 0.2, sustain: 0.1, }).toDestination(); const osc = new Oscillator().start(0).connect(ampEnv); ampEnv.triggerAttack(0); ampEnv.triggerRelease(0.3); }, "ampEnvelope.wav"); }); it("matches a file with multiple retriggers", () => { return CompareToFile( () => { const ampEnv = new AmplitudeEnvelope({ attack: 0.1, decay: 0.2, release: 0.2, sustain: 0.1, }).toDestination(); const osc = new Oscillator().start(0).connect(ampEnv); ampEnv.triggerAttack(0); ampEnv.triggerAttack(0.3); }, "ampEnvelope2.wav", 0.004 ); }); it("matches a file with ripple attack/release", () => { return CompareToFile( () => { const ampEnv = new AmplitudeEnvelope({ attack: 0.5, attackCurve: "ripple", decay: 0.2, release: 0.3, releaseCurve: "ripple", sustain: 0.1, }).toDestination(); const osc = new Oscillator().start(0).connect(ampEnv); ampEnv.triggerAttack(0); ampEnv.triggerRelease(0.7); ampEnv.triggerAttack(1); ampEnv.triggerRelease(1.6); }, "ampEnvelope3.wav", 0.002 ); }); }); context("Envelope", () => { it("extends envelope", () => { const ampEnv = new AmplitudeEnvelope(); expect(ampEnv).to.be.instanceOf(Envelope); ampEnv.dispose(); }); it("passes no signal before being triggered", async () => { const buffer = await Offline(() => { const ampEnv = new AmplitudeEnvelope().toDestination(); new Signal(1).connect(ampEnv); }); expect(buffer.isSilent()).to.be.true; }); it("passes signal once triggered", async () => { const buffer = await Offline(() => { const ampEnv = new AmplitudeEnvelope().toDestination(); new Signal(1).connect(ampEnv); ampEnv.triggerAttack(0.1); }, 0.2); expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.1, 0.001); }); }); }); ================================================ FILE: Tone/component/envelope/AmplitudeEnvelope.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { NormalRange, Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Envelope, EnvelopeOptions } from "./Envelope.js"; /** * AmplitudeEnvelope is a Tone.Envelope connected to a gain node. * Unlike Tone.Envelope, which outputs the envelope's value, AmplitudeEnvelope accepts * an audio signal as the input and will apply the envelope to the amplitude * of the signal. * Read more about ADSR Envelopes on [Wikipedia](https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope). * * @example * return Tone.Offline(() => { * const ampEnv = new Tone.AmplitudeEnvelope({ * attack: 0.1, * decay: 0.2, * sustain: 1.0, * release: 0.8 * }).toDestination(); * // create an oscillator and connect it * const osc = new Tone.Oscillator().connect(ampEnv).start(); * // trigger the envelopes attack and release "8t" apart * ampEnv.triggerAttackRelease("8t"); * }, 1.5, 1); * @category Component */ export class AmplitudeEnvelope extends Envelope { readonly name: string = "AmplitudeEnvelope"; private _gainNode: Gain = new Gain({ context: this.context, gain: 0, }); output: Gain = this._gainNode; input: Gain = this._gainNode; /** * @param attack The amount of time it takes for the envelope to go from 0 to its maximum value. * @param decay The period of time after the attack that it takes for the envelope * to fall to the sustain value. Value must be greater than 0. * @param sustain The percent of the maximum value that the envelope rests at until * the release is triggered. * @param release The amount of time after the release is triggered it takes to reach 0. * Value must be greater than 0. */ constructor( attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time ); constructor(options?: Partial); constructor() { super( optionsFromArguments(AmplitudeEnvelope.getDefaults(), arguments, [ "attack", "decay", "sustain", "release", ]) ); this._sig.connect(this._gainNode.gain); this.output = this._gainNode; this.input = this._gainNode; } /** * Clean up */ dispose(): this { super.dispose(); this._gainNode.dispose(); return this; } } ================================================ FILE: Tone/component/envelope/Envelope.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectTo } from "../../../test/helper/Connect.js"; import { Offline } from "../../../test/helper/Offline.js"; import { SignalConnectAndDisconnect } from "../../../test/helper/SignalTests.js"; import { Envelope, EnvelopeCurve } from "./Envelope.js"; describe("Envelope", () => { BasicTests(Envelope); SignalConnectAndDisconnect(Envelope); context("Envelope", () => { it("has an output connections", () => { const env = new Envelope(); env.connect(connectTo()); env.dispose(); }); it("can get and set values an Objects", () => { const env = new Envelope(); const values = { attack: 0, decay: 0.5, release: "4n", sustain: 1, }; env.set(values); expect(env.get()).to.contain.keys(Object.keys(values)); env.dispose(); }); it("passes no signal before being triggered", async () => { const buffer = await Offline(() => { new Envelope().toDestination(); }); expect(buffer.isSilent()).to.equal(true); }); it("passes signal once triggered", async () => { const buffer = await Offline(() => { const env = new Envelope().toDestination(); env.triggerAttack(0.05); }, 0.1); expect(buffer.getTimeOfFirstSound()).to.be.closeTo(0.05, 0.001); }); it("can take parameters as both an object and as arguments", () => { const env0 = new Envelope({ attack: 0, decay: 0.5, sustain: 1, }); expect(env0.attack).to.equal(0); expect(env0.decay).to.equal(0.5); expect(env0.sustain).to.equal(1); env0.dispose(); const env1 = new Envelope(0.1, 0.2, 0.3); expect(env1.attack).to.equal(0.1); expect(env1.decay).to.equal(0.2); expect(env1.sustain).to.equal(0.3); env1.dispose(); }); it("ensures that none of the values go below 0", () => { const env = new Envelope(); expect(() => { env.attack = -1; }).to.throw(RangeError); expect(() => { env.decay = -1; }).to.throw(RangeError); expect(() => { env.sustain = 2; }).to.throw(RangeError); expect(() => { env.release = -1; }).to.throw(RangeError); env.dispose(); }); it("can set attack to exponential or linear", () => { const env = new Envelope(0.01, 0.01, 0.5, 0.3); env.attackCurve = "exponential"; expect(env.attackCurve).to.equal("exponential"); env.triggerAttack(); env.dispose(); // and can be linear const env2 = new Envelope(0.01, 0.01, 0.5, 0.3); env2.attackCurve = "linear"; expect(env2.attackCurve).to.equal("linear"); env2.triggerAttack(); // and test a non-curve expect(() => { // @ts-ignore env2.attackCurve = "other"; }).to.throw(Error); env2.dispose(); }); it("can set decay to exponential or linear", () => { const env = new Envelope(0.01, 0.01, 0.5, 0.3); env.decayCurve = "exponential"; expect(env.decayCurve).to.equal("exponential"); env.triggerAttack(); env.dispose(); // and can be linear const env2 = new Envelope(0.01, 0.01, 0.5, 0.3); env2.decayCurve = "linear"; expect(env2.decayCurve).to.equal("linear"); env2.triggerAttack(); // and test a non-curve expect(() => { // @ts-ignore env2.decayCurve = "other"; }).to.throw(Error); env2.dispose(); }); it("can set release to exponential or linear", () => { const env = new Envelope(0.01, 0.01, 0.5, 0.3); env.releaseCurve = "exponential"; expect(env.releaseCurve).to.equal("exponential"); env.triggerRelease(); env.dispose(); // and can be linear const env2 = new Envelope(0.01, 0.01, 0.5, 0.3); env2.releaseCurve = "linear"; expect(env2.releaseCurve).to.equal("linear"); env2.triggerRelease(); // and test a non-curve expect(() => { // @ts-ignore env2.releaseCurve = "other"; }).to.throw(Error); env2.dispose(); }); it("can set release to exponential or linear", async () => { const buffer = await Offline(() => { const env = new Envelope({ release: 0, }); env.toDestination(); env.triggerAttackRelease(0.4, 0); }, 0.7); expect(buffer.getValueAtTime(0.3)).to.be.above(0); expect(buffer.getValueAtTime(0.401)).to.equal(0); }); it("schedule a release at the moment when the attack portion is done", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.5, decay: 0.0, sustain: 1, release: 0.5, }).toDestination(); env.triggerAttackRelease(0.5); }, 0.7); // make sure that it's got the rising edge expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.2, 0.01); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.4, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.6, 0.01); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0.8, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.be.closeTo(1, 0.001); }); it("correctly schedules an exponential attack", async () => { const e = { attack: 0.01, decay: 0.4, release: 0.1, sustain: 0.5, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.attackCurve = "exponential"; env.toDestination(); env.triggerAttack(0); }, 0.7); buffer.forEachBetween( (sample) => { expect(sample).to.be.within(0, 1); }, 0, e.attack ); buffer.forEachBetween( (sample) => { expect(sample).to.be.within(e.sustain - 0.001, 1); }, e.attack, e.attack + e.decay ); buffer.forEachBetween((sample) => { expect(sample).to.be.closeTo(e.sustain, 0.01); }, e.attack + e.decay); }); it("correctly schedules a linear release", async () => { const e = { attack: 0.01, decay: 0.4, release: 0.1, sustain: 0.5, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.attackCurve = "exponential"; env.toDestination(); env.triggerAttack(0); }, 0.7); buffer.forEachBetween( (sample, time) => { const target = 1 - (time - 0.2) * 10; expect(sample).to.be.closeTo(target, 0.01); }, 0.2, 0.2 ); }); it("correctly schedules a linear decay", async () => { const e = { attack: 0.1, decay: 0.5, release: 0.1, sustain: 0, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.decayCurve = "linear"; env.toDestination(); env.triggerAttack(0); }, 0.7); expect(buffer.getValueAtTime(0.05)).to.be.closeTo(0.5, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.8, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.6, 0.01); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0.4, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0.2, 0.01); expect(buffer.getValueAtTime(0.6)).to.be.closeTo(0, 0.01); }); it("correctly schedules an exponential decay", async () => { const e = { attack: 0.1, decay: 0.5, release: 0.1, sustain: 0, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.decayCurve = "exponential"; env.toDestination(); env.triggerAttack(0); }, 0.7); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.27, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.07, 0.01); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0.02, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0.005, 0.01); expect(buffer.getValueAtTime(0.6)).to.be.closeTo(0, 0.01); }); it("can schedule a very short attack", async () => { const e = { attack: 0.001, decay: 0.01, release: 0.1, sustain: 0.1, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.attackCurve = "exponential"; env.toDestination(); env.triggerAttack(0); }, 0.2); buffer.forEachBetween( (sample) => { expect(sample).to.be.within(0, 1); }, 0, e.attack ); buffer.forEachBetween( (sample) => { expect(sample).to.be.within(e.sustain - 0.001, 1); }, e.attack, e.attack + e.decay ); buffer.forEachBetween((sample) => { expect(sample).to.be.closeTo(e.sustain, 0.01); }, e.attack + e.decay); }); it("can schedule an attack of time 0", async () => { const buffer = await Offline(() => { const env = new Envelope(0, 0.1); env.toDestination(); env.triggerAttack(0.1); }, 0.2); expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.001); expect(buffer.getValueAtTime(0.0999)).to.be.closeTo(0, 0.001); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.001); }); it("correctly schedule a release", async () => { const e = { attack: 0.001, decay: 0.01, release: 0.3, sustain: 0.5, }; const releaseTime = 0.2; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.attackCurve = "exponential"; env.toDestination(); env.triggerAttackRelease(releaseTime); }, 0.6); const sustainStart = e.attack + e.decay; const sustainEnd = sustainStart + releaseTime; buffer.forEachBetween( (sample) => { expect(sample).to.be.below(e.sustain + 0.01); }, sustainStart, sustainEnd ); buffer.forEachBetween((sample) => { expect(sample).to.be.closeTo(0, 0.01); }, releaseTime + e.release); }); it("can retrigger a short attack at the same time as previous release", async () => { const buffer = await Offline(() => { const env = new Envelope(0.001, 0.1, 0.5); env.attackCurve = "linear"; env.toDestination(); env.triggerAttack(0); env.triggerRelease(0.4); env.triggerAttack(0.4); }, 0.6); expect(buffer.getValueAtTime(0.4)).be.closeTo(0.5, 0.01); expect(buffer.getValueAtTime(0.40025)).be.closeTo(0.75, 0.01); expect(buffer.getValueAtTime(0.4005)).be.closeTo(1, 0.01); }); it("is silent before and after triggering", async () => { const e = { attack: 0.001, decay: 0.01, release: 0.3, sustain: 0.5, }; const releaseTime = 0.2; const attackTime = 0.1; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.attackCurve = "exponential"; env.toDestination(); env.triggerAttack(attackTime); env.triggerRelease(releaseTime); }, 0.6); expect(buffer.getValueAtTime(attackTime - 0.001)).to.equal(0); expect( buffer.getValueAtTime( e.attack + e.decay + releaseTime + e.release ) ).to.be.below(0.01); }); it("is silent after decay if sustain is 0", async () => { const e = { attack: 0.01, decay: 0.04, sustain: 0, }; const attackTime = 0.1; const buffer = await Offline(() => { const env = new Envelope(e.attack, e.decay, e.sustain); env.toDestination(); env.triggerAttack(attackTime); }, 0.4); buffer.forEach((sample, time) => { expect(buffer.getValueAtTime(attackTime - 0.001)).to.equal(0); expect( buffer.getValueAtTime(attackTime + e.attack + e.decay) ).to.be.below(0.01); }); }); it("correctly schedule an attack release envelope", async () => { const e = { attack: 0.08, decay: 0.2, release: 0.2, sustain: 0.1, }; const releaseTime = 0.4; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.toDestination(); env.triggerAttack(0); env.triggerRelease(releaseTime); }); buffer.forEach((sample, time) => { if (time < e.attack) { expect(sample).to.be.within(0, 1); } else if (time < e.attack + e.decay) { expect(sample).to.be.within(e.sustain, 1); } else if (time < releaseTime) { expect(sample).to.be.closeTo(e.sustain, 0.1); } else if (time < releaseTime + e.release) { expect(sample).to.be.within(0, e.sustain + 0.01); } else { expect(sample).to.be.below(0.0001); } }); }); it("can schedule a combined AttackRelease", async () => { const e = { attack: 0.1, decay: 0.2, release: 0.1, sustain: 0.35, }; const releaseTime = 0.4; const duration = 0.4; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.toDestination(); env.triggerAttack(0); env.triggerRelease(releaseTime); }, 0.7); buffer.forEach((sample, time) => { if (time < e.attack) { expect(sample).to.be.within(0, 1); } else if (time < e.attack + e.decay) { expect(sample).to.be.within(e.sustain - 0.001, 1); } else if (time < duration) { expect(sample).to.be.closeTo(e.sustain, 0.1); } else if (time < duration + e.release) { expect(sample).to.be.within(0, e.sustain + 0.01); } else { expect(sample).to.be.below(0.0015); } }); }); it("can schedule a combined AttackRelease with velocity", async () => { const e = { attack: 0.1, decay: 0.2, release: 0.1, sustain: 0.35, }; const releaseTime = 0.4; const duration = 0.4; const velocity = 0.4; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.toDestination(); env.triggerAttack(0, velocity); env.triggerRelease(releaseTime); }, 0.7); buffer.forEach((sample, time) => { if (time < e.attack) { expect(sample).to.be.within(0, velocity + 0.01); } else if (time < e.attack + e.decay) { expect(sample).to.be.within( e.sustain * velocity - 0.01, velocity + 0.01 ); } else if (time < duration) { expect(sample).to.be.closeTo(e.sustain * velocity, 0.1); } else if (time < duration + e.release) { expect(sample).to.be.within(0, e.sustain * velocity + 0.01); } else { expect(sample).to.be.below(0.01); } }); }); it("can schedule multiple envelopes", async () => { const e = { attack: 0.1, decay: 0.2, release: 0.1, sustain: 0.0, }; const buffer = await Offline(() => { const env = new Envelope( e.attack, e.decay, e.sustain, e.release ); env.toDestination(); env.triggerAttack(0); env.triggerAttack(0.5); }, 0.85); // first trigger expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0, 0.01); // second trigger expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0, 0.01); expect(buffer.getValueAtTime(0.6)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.8)).to.be.closeTo(0, 0.01); }); it("can schedule multiple attack/releases with no discontinuities", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 0.2, 0.4).toDestination(); env.triggerAttackRelease(0, 0.4); env.triggerAttackRelease(0.4, 0.11); env.triggerAttackRelease(0.45, 0.1); env.triggerAttackRelease(1.1, 0.09); env.triggerAttackRelease(1.5, 0.3); env.triggerAttackRelease(1.8, 0.29); }, 2); // test for discontinuities let lastSample = 0; buffer.forEach((sample, time) => { expect(sample).to.be.at.most(1); const diff = Math.abs(lastSample - sample); expect(diff).to.be.lessThan(0.001); lastSample = sample; }); }); it("can schedule multiple 'linear' attack/releases with no discontinuities", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 0.2, 0.4).toDestination(); env.attackCurve = "linear"; env.releaseCurve = "linear"; env.triggerAttackRelease(0, 0.4); env.triggerAttackRelease(0.4, 0.11); env.triggerAttackRelease(0.45, 0.1); env.triggerAttackRelease(1.1, 0.09); env.triggerAttackRelease(1.5, 0.3); env.triggerAttackRelease(1.8, 0.29); }, 2); // test for discontinuities let lastSample = 0; buffer.forEach((sample, time) => { expect(sample).to.be.at.most(1); const diff = Math.abs(lastSample - sample); expect(diff).to.be.lessThan(0.001); lastSample = sample; }); }); it("can schedule multiple 'exponential' attack/releases with no discontinuities", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 0.2, 0.4).toDestination(); env.attackCurve = "exponential"; env.releaseCurve = "exponential"; env.triggerAttackRelease(0, 0.4); env.triggerAttackRelease(0.4, 0.11); env.triggerAttackRelease(0.45, 0.1); env.triggerAttackRelease(1.1, 0.09); env.triggerAttackRelease(1.5, 0.3); env.triggerAttackRelease(1.8, 0.29); }, 2); // test for discontinuities let lastSample = 0; buffer.forEach((sample, time) => { expect(sample).to.be.at.most(1); const diff = Math.abs(lastSample - sample); expect(diff).to.be.lessThan(0.0035); lastSample = sample; }); }); it("can schedule multiple 'sine' attack/releases with no discontinuities", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 0.2, 0.4).toDestination(); env.attackCurve = "sine"; env.releaseCurve = "sine"; env.triggerAttackRelease(0, 0.4); env.triggerAttackRelease(0.4, 0.11); env.triggerAttackRelease(0.45, 0.1); env.triggerAttackRelease(1.1, 0.09); env.triggerAttackRelease(1.5, 0.3); env.triggerAttackRelease(1.8, 0.29); }, 2); // test for discontinuities let lastSample = 0; buffer.forEach((sample, time) => { expect(sample).to.be.at.most(1); const diff = Math.abs(lastSample - sample); expect(diff).to.be.lessThan(0.0035); lastSample = sample; }); }); it("can schedule multiple 'cosine' attack/releases with no discontinuities", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 0.2, 0.4).toDestination(); env.attackCurve = "cosine"; env.releaseCurve = "cosine"; env.triggerAttackRelease(0, 0.4); env.triggerAttackRelease(0.4, 0.11); env.triggerAttackRelease(0.45, 0.1); env.triggerAttackRelease(1.1, 0.09); env.triggerAttackRelease(1.5, 0.3); env.triggerAttackRelease(1.8, 0.29); }, 2); // test for discontinuities let lastSample = 0; buffer.forEach((sample, time) => { expect(sample).to.be.at.most(1); const diff = Math.abs(lastSample - sample); expect(diff).to.be.lessThan(0.002); lastSample = sample; }); }); it("reports its current envelope value (.value)", async () => { const buffer = await Offline(() => { const env = new Envelope(1, 0.2, 1).toDestination(); expect(env.value).to.be.closeTo(0, 0.01); env.triggerAttack(); return (time) => { expect(env.value).to.be.closeTo(time, 0.01); }; }, 0.5); }); it("can cancel a schedule envelope", async () => { const buffer = await Offline(() => { const env = new Envelope(0.1, 0.2, 1).toDestination(); env.triggerAttack(0.2); env.cancel(0.2); }, 0.3); expect(buffer.isSilent()).to.be.true; }); }); context("Attack/Release Curves", () => { const envelopeCurves: EnvelopeCurve[] = [ "linear", "exponential", "bounce", "cosine", "ripple", "sine", "step", ]; it("can get set all of the types as the attackCurve", () => { const env = new Envelope(); envelopeCurves.forEach((type) => { env.attackCurve = type; expect(env.attackCurve).to.equal(type); }); env.dispose(); }); it("can get set all of the types as the releaseCurve", () => { const env = new Envelope(); envelopeCurves.forEach((type) => { env.releaseCurve = type; expect(env.releaseCurve).to.equal(type); }); env.dispose(); }); it("outputs a signal when the attack/release curves are set to 'bounce'", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: "bounce", decay: 0, release: 0.3, releaseCurve: "bounce", sustain: 1, }).toDestination(); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEachBetween( (sample) => { expect(sample).to.be.above(0); }, 0.101, 0.7 ); }); it("outputs a signal when the attack/release curves are set to 'ripple'", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: "ripple", decay: 0, release: 0.3, releaseCurve: "ripple", sustain: 1, }).toDestination(); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEachBetween( (sample) => { expect(sample).to.be.above(0); }, 0.101, 0.7 ); }); it("outputs a signal when the attack/release curves are set to 'sine'", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: "sine", decay: 0, release: 0.3, releaseCurve: "sine", sustain: 1, }).toDestination(); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEachBetween( (sample) => { expect(sample).to.be.above(0); }, 0.101, 0.7 ); }); it("outputs a signal when the attack/release curves are set to 'cosine'", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: "cosine", decay: 0, release: 0.3, releaseCurve: "cosine", sustain: 1, }).toDestination(); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEachBetween( (sample) => { expect(sample).to.be.above(0); }, 0.101, 0.7 ); }); it("outputs a signal when the attack/release curves are set to 'step'", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: "step", decay: 0, release: 0.3, releaseCurve: "step", sustain: 1, }).toDestination(); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEach((sample, time) => { if (time > 0.3 && time < 0.5) { expect(sample).to.be.above(0); } else if (time < 0.1) { expect(sample).to.equal(0); } }); }); it("outputs a signal when the attack/release curves are set to an array", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: [0, 1, 0, 1], decay: 0, release: 0.3, releaseCurve: [1, 0, 1, 0], sustain: 1, }).toDestination(); expect(env.attackCurve).to.deep.equal([0, 1, 0, 1]); env.triggerAttackRelease(0.3, 0.1); }, 0.8); buffer.forEach((sample, time) => { if (time > 0.4 && time < 0.5) { expect(sample).to.be.above(0); } else if (time < 0.1) { expect(sample).to.equal(0); } }); }); it("can scale a velocity with a custom curve", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.3, attackCurve: [0, 1, 0, 1], decay: 0, release: 0.3, releaseCurve: [1, 0, 1, 0], sustain: 1, }).toDestination(); env.triggerAttackRelease(0.4, 0.1, 0.5); }, 0.8); buffer.forEach((sample) => { expect(sample).to.be.at.most(0.51); }); }); it("can render the envelope to a curve", async () => { const env = new Envelope(); const curve = await env.asArray(); expect(curve.some((v) => v > 0)).to.be.true; curve.forEach((v) => expect(v).to.be.within(0, 1)); env.dispose(); }); it("can render the envelope to an array with a given length", async () => { const env = new Envelope(); const curve = await env.asArray(256); expect(curve.length).to.equal(256); env.dispose(); }); it("can retrigger partial envelope with custom type", async () => { const buffer = await Offline(() => { const env = new Envelope({ attack: 0.5, attackCurve: "cosine", decay: 0, release: 0.5, releaseCurve: "sine", sustain: 1, }).toDestination(); env.triggerAttack(0); env.triggerRelease(0.2); env.triggerAttack(0.5); }, 1); expect(buffer.getValueAtTime(0)).to.equal(0); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.32, 0.01); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.6, 0.01); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.53, 0.01); expect(buffer.getValueAtTime(0.4)).to.be.closeTo(0.38, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0.2, 0.01); expect(buffer.getValueAtTime(0.6)).to.be.closeTo(0.52, 0.01); expect(buffer.getValueAtTime(0.7)).to.be.closeTo(0.78, 0.01); expect(buffer.getValueAtTime(0.8)).to.be.closeTo(0.95, 0.01); expect(buffer.getValueAtTime(0.9)).to.be.closeTo(1, 0.01); }); }); }); ================================================ FILE: Tone/component/envelope/Envelope.ts ================================================ import { OfflineContext } from "../../core/context/OfflineContext.js"; import { InputNode, OutputNode } from "../../core/context/ToneAudioNode.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { NormalRange, Time } from "../../core/type/Units.js"; import { assert } from "../../core/util/Debug.js"; import { range, timeRange } from "../../core/util/Decorator.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { isArray, isObject, isString } from "../../core/util/TypeCheck.js"; import { connectSignal, disconnectSignal, Signal, } from "../../signal/Signal.js"; type BasicEnvelopeCurve = "linear" | "exponential"; type InternalEnvelopeCurve = BasicEnvelopeCurve | number[]; export type EnvelopeCurve = EnvelopeCurveName | number[]; export interface EnvelopeOptions extends ToneAudioNodeOptions { attack: Time; decay: Time; sustain: NormalRange; release: Time; attackCurve: EnvelopeCurve; releaseCurve: EnvelopeCurve; decayCurve: BasicEnvelopeCurve; } /** * Envelope is an [ADSR](https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope) * envelope generator. Envelope outputs a signal which * can be connected to an AudioParam or Tone.Signal. * ``` * /\ * / \ * / \ * / \ * / \___________ * / \ * / \ * / \ * / \ * ``` * @example * return Tone.Offline(() => { * const env = new Tone.Envelope({ * attack: 0.1, * decay: 0.2, * sustain: 0.5, * release: 0.8, * }).toDestination(); * env.triggerAttackRelease(0.5); * }, 1.5, 1); * @category Component */ export class Envelope extends ToneAudioNode { readonly name: string = "Envelope"; /** * When triggerAttack is called, the attack time is the amount of * time it takes for the envelope to reach its maximum value. * ``` * /\ * /X \ * /XX \ * /XXX \ * /XXXX \___________ * /XXXXX \ * /XXXXXX \ * /XXXXXXX \ * /XXXXXXXX \ * ``` * @min 0 * @max 2 */ @timeRange(0) attack: Time; /** * After the attack portion of the envelope, the value will fall * over the duration of the decay time to its sustain value. * ``` * /\ * / X\ * / XX\ * / XXX\ * / XXXX\___________ * / XXXXX \ * / XXXXX \ * / XXXXX \ * / XXXXX \ * ``` * @min 0 * @max 2 */ @timeRange(0) decay: Time; /** * The sustain value is the value * which the envelope rests at after triggerAttack is * called, but before triggerRelease is invoked. * ``` * /\ * / \ * / \ * / \ * / \___________ * / XXXXXXXXXXX\ * / XXXXXXXXXXX \ * / XXXXXXXXXXX \ * / XXXXXXXXXXX \ * ``` */ @range(0, 1) sustain: NormalRange; /** * After triggerRelease is called, the envelope's * value will fall to its minimum value over the * duration of the release time. * ``` * /\ * / \ * / \ * / \ * / \___________ * / X\ * / XX\ * / XXX\ * / XXXX\ * ``` * @min 0 * @max 5 */ @timeRange(0) release: Time; /** * The automation curve type for the attack */ private _attackCurve!: InternalEnvelopeCurve; /** * The automation curve type for the decay */ private _decayCurve!: InternalEnvelopeCurve; /** * The automation curve type for the release */ private _releaseCurve!: InternalEnvelopeCurve; /** * the signal which is output. */ protected _sig: Signal<"normalRange"> = new Signal({ context: this.context, value: 0, }); /** * The output signal of the envelope */ output: OutputNode = this._sig; /** * Envelope has no input */ input: InputNode | undefined = undefined; /** * @param attack The amount of time it takes for the envelope to go from * 0 to its maximum value. * @param decay The period of time after the attack that it takes for the envelope * to fall to the sustain value. Value must be greater than 0. * @param sustain The percent of the maximum value that the envelope rests at until * the release is triggered. * @param release The amount of time after the release is triggered it takes to reach 0. * Value must be greater than 0. */ constructor( attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time ); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"] ); super(options); this.attack = options.attack; this.decay = options.decay; this.sustain = options.sustain; this.release = options.release; this.attackCurve = options.attackCurve; this.releaseCurve = options.releaseCurve; this.decayCurve = options.decayCurve; } static getDefaults(): EnvelopeOptions { return Object.assign(ToneAudioNode.getDefaults(), { attack: 0.01, attackCurve: "linear" as EnvelopeCurveName, decay: 0.1, decayCurve: "exponential" as BasicEnvelopeCurve, release: 1, releaseCurve: "exponential" as EnvelopeCurveName, sustain: 0.5, }); } /** * Read the current value of the envelope. Useful for * synchronizing visual output to the envelope. */ get value(): NormalRange { return this.getValueAtTime(this.now()); } /** * Get the curve * @param curve * @param direction In/Out * @return The curve name */ private _getCurve( curve: InternalEnvelopeCurve, direction: EnvelopeDirection ): EnvelopeCurve { if (isString(curve)) { return curve; } else { // look up the name in the curves array let curveName: EnvelopeCurveName; for (curveName in EnvelopeCurves) { if (EnvelopeCurves[curveName][direction] === curve) { return curveName; } } // return the custom curve return curve; } } /** * Assign a the curve to the given name using the direction * @param name * @param direction In/Out * @param curve */ private _setCurve( name: "_attackCurve" | "_decayCurve" | "_releaseCurve", direction: EnvelopeDirection, curve: EnvelopeCurve ): void { // check if it's a valid type if (isString(curve) && Reflect.has(EnvelopeCurves, curve)) { const curveDef = EnvelopeCurves[curve]; if (isObject(curveDef)) { if (name !== "_decayCurve") { this[name] = curveDef[direction]; } } else { this[name] = curveDef; } } else if (isArray(curve) && name !== "_decayCurve") { this[name] = curve; } else { throw new Error("Envelope: invalid curve: " + curve); } } /** * The shape of the attack. * Can be any of these strings: * * "linear" * * "exponential" * * "sine" * * "cosine" * * "bounce" * * "ripple" * * "step" * * Can also be an array which describes the curve. Values * in the array are evenly subdivided and linearly * interpolated over the duration of the attack. * @example * return Tone.Offline(() => { * const env = new Tone.Envelope(0.4).toDestination(); * env.attackCurve = "linear"; * env.triggerAttack(); * }, 1, 1); */ get attackCurve(): EnvelopeCurve { return this._getCurve(this._attackCurve, "In"); } set attackCurve(curve) { this._setCurve("_attackCurve", "In", curve); } /** * The shape of the release. See the attack curve types. * @example * return Tone.Offline(() => { * const env = new Tone.Envelope({ * release: 0.8 * }).toDestination(); * env.triggerAttack(); * // release curve could also be defined by an array * env.releaseCurve = [1, 0.3, 0.4, 0.2, 0.7, 0]; * env.triggerRelease(0.2); * }, 1, 1); */ get releaseCurve(): EnvelopeCurve { return this._getCurve(this._releaseCurve, "Out"); } set releaseCurve(curve) { this._setCurve("_releaseCurve", "Out", curve); } /** * The shape of the decay either "linear" or "exponential" * @example * return Tone.Offline(() => { * const env = new Tone.Envelope({ * sustain: 0.1, * decay: 0.5 * }).toDestination(); * env.decayCurve = "linear"; * env.triggerAttack(); * }, 1, 1); */ get decayCurve(): EnvelopeCurve { return this._getCurve(this._decayCurve, "Out"); } set decayCurve(curve) { this._setCurve("_decayCurve", "Out", curve); } /** * Trigger the attack/decay portion of the ADSR envelope. * @param time When the attack should start. * @param velocity The velocity of the envelope scales the vales. * number between 0-1 * @example * const env = new Tone.AmplitudeEnvelope().toDestination(); * const osc = new Tone.Oscillator().connect(env).start(); * // trigger the attack 0.5 seconds from now with a velocity of 0.2 * env.triggerAttack("+0.5", 0.2); */ triggerAttack(time?: Time, velocity: NormalRange = 1): this { this.log("triggerAttack", time, velocity); time = this.toSeconds(time); const originalAttack = this.toSeconds(this.attack); let attack = originalAttack; const decay = this.toSeconds(this.decay); // check if it's not a complete attack const currentValue = this.getValueAtTime(time); if (currentValue > 0) { // subtract the current value from the attack time const attackRate = 1 / attack; const remainingDistance = 1 - currentValue; // the attack is now the remaining time attack = remainingDistance / attackRate; } // attack if (attack < this.sampleTime) { this._sig.cancelScheduledValues(time); // case where the attack time is 0 should set instantly this._sig.setValueAtTime(velocity, time); } else if (this._attackCurve === "linear") { this._sig.linearRampTo(velocity, attack, time); } else if (this._attackCurve === "exponential") { this._sig.targetRampTo(velocity, attack, time); } else { this._sig.cancelAndHoldAtTime(time); let curve = this._attackCurve; // find the starting position in the curve for (let i = 1; i < curve.length; i++) { // the starting index is between the two values if (curve[i - 1] <= currentValue && currentValue <= curve[i]) { curve = this._attackCurve.slice(i); // the first index is the current value curve[0] = currentValue; break; } } this._sig.setValueCurveAtTime(curve, time, attack, velocity); } // decay if (decay && this.sustain < 1) { const decayValue = velocity * this.sustain; const decayStart = time + attack; this.log("decay", decayStart); if (this._decayCurve === "linear") { this._sig.linearRampToValueAtTime( decayValue, decay + decayStart ); } else { this._sig.exponentialApproachValueAtTime( decayValue, decayStart, decay ); } } return this; } /** * Triggers the release of the envelope. * @param time When the release portion of the envelope should start. * @example * const env = new Tone.AmplitudeEnvelope().toDestination(); * const osc = new Tone.Oscillator({ * type: "sawtooth" * }).connect(env).start(); * env.triggerAttack(); * // trigger the release half a second after the attack * env.triggerRelease("+0.5"); */ triggerRelease(time?: Time): this { this.log("triggerRelease", time); time = this.toSeconds(time); const currentValue = this.getValueAtTime(time); if (currentValue > 0) { const release = this.toSeconds(this.release); if (release < this.sampleTime) { this._sig.setValueAtTime(0, time); } else if (this._releaseCurve === "linear") { this._sig.linearRampTo(0, release, time); } else if (this._releaseCurve === "exponential") { this._sig.targetRampTo(0, release, time); } else { assert( isArray(this._releaseCurve), "releaseCurve must be either 'linear', 'exponential' or an array" ); this._sig.cancelAndHoldAtTime(time); this._sig.setValueCurveAtTime( this._releaseCurve, time, release, currentValue ); } } return this; } /** * Get the scheduled value at the given time. This will * return the unconverted (raw) value. * @example * const env = new Tone.Envelope(0.5, 1, 0.4, 2); * env.triggerAttackRelease(2); * setInterval(() => console.log(env.getValueAtTime(Tone.now())), 100); */ getValueAtTime(time: Time): NormalRange { return this._sig.getValueAtTime(time); } /** * triggerAttackRelease is shorthand for triggerAttack, then waiting * some duration, then triggerRelease. * @param duration The duration of the sustain. * @param time When the attack should be triggered. * @param velocity The velocity of the envelope. * @example * const env = new Tone.AmplitudeEnvelope().toDestination(); * const osc = new Tone.Oscillator().connect(env).start(); * // trigger the release 0.5 seconds after the attack * env.triggerAttackRelease(0.5); */ triggerAttackRelease( duration: Time, time?: Time, velocity: NormalRange = 1 ): this { time = this.toSeconds(time); this.triggerAttack(time, velocity); this.triggerRelease(time + this.toSeconds(duration)); return this; } /** * Cancels all scheduled envelope changes after the given time. */ cancel(after?: Time): this { this._sig.cancelScheduledValues(this.toSeconds(after)); return this; } /** * Connect the envelope to a destination node. */ connect(destination: InputNode, outputNumber = 0, inputNumber = 0): this { connectSignal(this, destination, outputNumber, inputNumber); return this; } /** @inheritdoc */ disconnect( destination?: InputNode, outputNumber = 0, inputNumber = 0 ): this { disconnectSignal(this, destination, outputNumber, inputNumber); return this; } /** * 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. */ async asArray(length = 1024): Promise { const duration = length / this.context.sampleRate; const context = new OfflineContext( 1, duration, this.context.sampleRate ); // normalize the ADSR for the given duration with 20% sustain time const attackPortion = this.toSeconds(this.attack) + this.toSeconds(this.decay); const envelopeDuration = attackPortion + this.toSeconds(this.release); const sustainTime = envelopeDuration * 0.1; const totalDuration = envelopeDuration + sustainTime; // @ts-ignore const clone = new this.constructor( Object.assign(this.get(), { attack: (duration * this.toSeconds(this.attack)) / totalDuration, decay: (duration * this.toSeconds(this.decay)) / totalDuration, release: (duration * this.toSeconds(this.release)) / totalDuration, context, }) ) as Envelope; clone._sig.toDestination(); clone.triggerAttackRelease( (duration * (attackPortion + sustainTime)) / totalDuration, 0 ); const buffer = await context.render(); return buffer.getChannelData(0); } dispose(): this { super.dispose(); this._sig.dispose(); return this; } } interface EnvelopeCurveObject { In: number[]; Out: number[]; } type EnvelopeDirection = keyof EnvelopeCurveObject; interface EnvelopeCurveMap { linear: "linear"; exponential: "exponential"; bounce: EnvelopeCurveObject; cosine: EnvelopeCurveObject; sine: EnvelopeCurveObject; ripple: EnvelopeCurveObject; step: EnvelopeCurveObject; } type EnvelopeCurveName = keyof EnvelopeCurveMap; /** * Generate some complex envelope curves. */ const EnvelopeCurves: EnvelopeCurveMap = (() => { const curveLen = 128; let i: number; let k: number; // cosine curve const cosineCurve: number[] = []; for (i = 0; i < curveLen; i++) { cosineCurve[i] = Math.sin((i / (curveLen - 1)) * (Math.PI / 2)); } // ripple curve const rippleCurve: number[] = []; const rippleCurveFreq = 6.4; for (i = 0; i < curveLen - 1; i++) { k = i / (curveLen - 1); const sineWave = Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1; rippleCurve[i] = sineWave / 10 + k * 0.83; } rippleCurve[curveLen - 1] = 1; // stairs curve const stairsCurve: number[] = []; const steps = 5; for (i = 0; i < curveLen; i++) { stairsCurve[i] = Math.ceil((i / (curveLen - 1)) * steps) / steps; } // in-out easing curve const sineCurve: number[] = []; for (i = 0; i < curveLen; i++) { k = i / (curveLen - 1); sineCurve[i] = 0.5 * (1 - Math.cos(Math.PI * k)); } // a bounce curve const bounceCurve: number[] = []; for (i = 0; i < curveLen; i++) { k = i / (curveLen - 1); const freq = Math.pow(k, 3) * 4 + 0.2; const val = Math.cos(freq * Math.PI * 2 * k); bounceCurve[i] = Math.abs(val * (1 - k)); } /** * Invert a value curve to make it work for the release */ function invertCurve(curve: number[]): number[] { const out = new Array(curve.length); for (let j = 0; j < curve.length; j++) { out[j] = 1 - curve[j]; } return out; } /** * reverse the curve */ function reverseCurve(curve: number[]): number[] { return curve.slice(0).reverse(); } /** * attack and release curve arrays */ return { bounce: { In: invertCurve(bounceCurve), Out: bounceCurve, }, cosine: { In: cosineCurve, Out: reverseCurve(cosineCurve), }, exponential: "exponential" as const, linear: "linear" as const, ripple: { In: rippleCurve, Out: invertCurve(rippleCurve), }, sine: { In: sineCurve, Out: invertCurve(sineCurve), }, step: { In: stairsCurve, Out: invertCurve(stairsCurve), }, }; })(); ================================================ FILE: Tone/component/envelope/FrequencyEnvelope.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { Offline } from "../../../test/helper/Offline.js"; import { Envelope } from "./Envelope.js"; import { FrequencyEnvelope } from "./FrequencyEnvelope.js"; describe("FrequencyEnvelope", () => { BasicTests(FrequencyEnvelope); context("FrequencyEnvelope", () => { it("has an output connections", () => { const freqEnv = new FrequencyEnvelope(); freqEnv.connect(connectTo()); connectFrom().connect(freqEnv); freqEnv.dispose(); }); it("extends Envelope", () => { const freqEnv = new FrequencyEnvelope(); expect(freqEnv).to.be.instanceOf(Envelope); freqEnv.dispose(); }); it("can get and set values an Objects", () => { const freqEnv = new FrequencyEnvelope(); const values = { attack: 0, release: "4n", baseFrequency: 20, octaves: 4, }; freqEnv.set(values); expect(freqEnv.get()).to.contain.keys(Object.keys(values)); expect(freqEnv.baseFrequency).to.equal(20); expect(freqEnv.octaves).to.equal(4); freqEnv.dispose(); }); it("can take parameters as both an object and as arguments", () => { const env0 = new FrequencyEnvelope({ attack: 0, decay: 0.5, sustain: 1, exponent: 3, }); expect(env0.attack).to.equal(0); expect(env0.decay).to.equal(0.5); expect(env0.sustain).to.equal(1); expect(env0.exponent).to.equal(3); env0.dispose(); const env1 = new FrequencyEnvelope(0.1, 0.2, 0.3); expect(env1.attack).to.equal(0.1); expect(env1.decay).to.equal(0.2); expect(env1.sustain).to.equal(0.3); env1.exponent = 2; expect(env1.exponent).to.equal(2); env1.dispose(); }); it("can set a negative octave", () => { const freqEnv = new FrequencyEnvelope(); freqEnv.octaves = -2; freqEnv.dispose(); }); it("goes to the scaled range", async () => { const e = { attack: 0.01, decay: 0.4, sustain: 1, }; const buffer = await Offline(() => { const freqEnv = new FrequencyEnvelope( e.attack, e.decay, e.sustain ); freqEnv.baseFrequency = 200; freqEnv.octaves = 3; freqEnv.attackCurve = "exponential"; freqEnv.toDestination(); freqEnv.triggerAttack(0); }, 0.3); buffer.forEach((sample, time) => { if (time < e.attack) { expect(sample).to.be.within(200, 1600); } else if (time < e.attack + e.decay) { expect(sample).to.be.closeTo(1600, 10); } }); }); }); }); ================================================ FILE: Tone/component/envelope/FrequencyEnvelope.ts ================================================ import { Frequency, Hertz, NormalRange, Time } from "../../core/type/Units.js"; import { assertRange } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { Pow } from "../../signal/Pow.js"; import { Scale } from "../../signal/Scale.js"; import { Envelope, EnvelopeOptions } from "./Envelope.js"; export interface FrequencyEnvelopeOptions extends EnvelopeOptions { baseFrequency: Frequency; octaves: number; exponent: number; } /** * FrequencyEnvelope is an {@link Envelope} which ramps between {@link baseFrequency} * and {@link octaves}. It can also have an optional {@link exponent} to adjust the curve * which it ramps. * @example * const oscillator = new Tone.Oscillator().toDestination().start(); * const freqEnv = new Tone.FrequencyEnvelope({ * attack: 0.2, * baseFrequency: "C2", * octaves: 4 * }); * freqEnv.connect(oscillator.frequency); * freqEnv.triggerAttack(); * @category Component */ export class FrequencyEnvelope extends Envelope { readonly name: string = "FrequencyEnvelope"; /** * Private reference to the base frequency as a number */ private _baseFrequency: Hertz; /** * The number of octaves */ private _octaves: number; /** * Internal scaler from 0-1 to the final output range */ private _scale: Scale; /** * Apply a power curve to the output */ private _exponent: Pow; /** * @param attack the attack time in seconds * @param decay the decay time in seconds * @param sustain a percentage (0-1) of the full amplitude * @param release the release time in seconds */ constructor( attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time ); constructor(options?: Partial); constructor() { const options = optionsFromArguments( FrequencyEnvelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"] ); super(options); this._octaves = options.octaves; this._baseFrequency = this.toFrequency(options.baseFrequency); this._exponent = this.input = new Pow({ context: this.context, value: options.exponent, }); this._scale = this.output = new Scale({ context: this.context, min: this._baseFrequency, max: this._baseFrequency * Math.pow(2, this._octaves), }); this._sig.chain(this._exponent, this._scale); } static getDefaults(): FrequencyEnvelopeOptions { return Object.assign(Envelope.getDefaults(), { baseFrequency: 200, exponent: 1, octaves: 4, }); } /** * The envelope's minimum output value. This is the value which it * starts at. */ get baseFrequency(): Frequency { return this._baseFrequency; } set baseFrequency(min) { const freq = this.toFrequency(min); assertRange(freq, 0); this._baseFrequency = freq; this._scale.min = this._baseFrequency; // update the max value when the min changes this.octaves = this._octaves; } /** * The number of octaves above the baseFrequency that the * envelope will scale to. */ get octaves(): number { return this._octaves; } set octaves(octaves: number) { this._octaves = octaves; this._scale.max = this._baseFrequency * Math.pow(2, octaves); } /** * The envelope's exponent value. */ get exponent(): number { return this._exponent.value; } set exponent(exponent) { this._exponent.value = exponent; } /** * Clean up */ dispose(): this { super.dispose(); this._exponent.dispose(); this._scale.dispose(); return this; } } ================================================ FILE: Tone/component/filter/BiquadFilter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { BiquadFilter } from "./BiquadFilter.js"; describe("BiquadFilter", () => { BasicTests(BiquadFilter); context("BiquadFiltering", () => { it("can be constructed with a arguments", () => { const filter = new BiquadFilter(200, "highpass"); expect(filter.frequency.value).to.be.closeTo(200, 0.001); expect(filter.type).to.equal("highpass"); filter.dispose(); }); it("can be constructed with an object", () => { const filter = new BiquadFilter({ frequency: 340, type: "bandpass", }); expect(filter.frequency.value).to.be.closeTo(340, 0.001); expect(filter.type).to.equal("bandpass"); filter.dispose(); }); it("can set/get values as an Object", () => { const filter = new BiquadFilter(); const values = { Q: 2, frequency: 440, gain: -6, type: "lowshelf" as const, }; filter.set(values); expect(filter.get()).to.include.keys([ "type", "frequency", "Q", "gain", ]); expect(filter.type).to.equal(values.type); expect(filter.frequency.value).to.equal(values.frequency); expect(filter.Q.value).to.equal(values.Q); expect(filter.gain.value).to.be.closeTo(values.gain, 0.04); filter.dispose(); }); it("can get the frequency response curve", () => { const filter = new BiquadFilter(); const curve = filter.getFrequencyResponse(32); expect(curve.length).to.equal(32); expect(curve[0]).be.closeTo(1, 0.01); expect(curve[5]).be.closeTo(0.5, 0.1); expect(curve[15]).be.closeTo(0, 0.01); expect(curve[31]).be.closeTo(0, 0.01); filter.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const filter = new BiquadFilter().toDestination(); input.connect(filter); }); }); it("can set the basic filter types", () => { const filter = new BiquadFilter(); const types: BiquadFilterType[] = [ "lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking", ]; for (const type of types) { filter.type = type; expect(filter.type).to.equal(type); } expect(() => { // @ts-ignore filter.type = "nontype"; }).to.throw(Error); filter.dispose(); }); it("attenuates the incoming signal", async () => { const buffer = await Offline(() => { const filter = new BiquadFilter(700, "lowpass").toDestination(); filter.Q.value = 0; const osc = new Oscillator(880).connect(filter); osc.start(0); }, 0.2); expect(buffer.getRmsAtTime(0.05)).to.be.within(0.37, 0.53); expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53); }); }); }); ================================================ FILE: Tone/component/filter/BiquadFilter.ts ================================================ import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Cents, Frequency, GainFactor } from "../../core/type/Units.js"; import { assert } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; export interface BiquadFilterOptions extends ToneAudioNodeOptions { frequency: Frequency; detune: Cents; Q: number; type: BiquadFilterType; gain: GainFactor; } /** * Thin wrapper around the native Web Audio [BiquadFilterNode](https://webaudio.github.io/web-audio-api/#biquadfilternode). * BiquadFilter is similar to {@link Filter} but doesn't have the option to set the "rolloff" value. * @category Component */ export class BiquadFilter extends ToneAudioNode { readonly name: string = "BiquadFilter"; readonly input: BiquadFilterNode; readonly output: BiquadFilterNode; /** * The frequency of the filter */ readonly frequency: Param<"frequency">; /** * A detune value, in cents, for the frequency. */ readonly detune: Param<"cents">; /** * The Q factor of the filter. * For lowpass and highpass filters the Q value is interpreted to be in dB. * For these filters the nominal range is [−𝑄𝑙𝑖𝑚,𝑄𝑙𝑖𝑚] where 𝑄𝑙𝑖𝑚 is the largest value for which 10𝑄/20 does not overflow. This is approximately 770.63678. * For the bandpass, notch, allpass, and peaking filters, this value is a linear value. * The value is related to the bandwidth of the filter and hence should be a positive value. The nominal range is * [0,3.4028235𝑒38], the upper limit being the most-positive-single-float. * This is not used for the lowshelf and highshelf filters. */ readonly Q: Param<"number">; /** * The gain of the filter. Its value is in dB units. The gain is only used for lowshelf, highshelf, and peaking filters. */ readonly gain: Param<"decibels">; private readonly _filter: BiquadFilterNode; /** * @param frequency The cutoff frequency of the filter. * @param type The type of filter. */ constructor(frequency?: Frequency, type?: BiquadFilterType); constructor(options?: Partial); constructor() { const options = optionsFromArguments( BiquadFilter.getDefaults(), arguments, ["frequency", "type"] ); super(options); this._filter = this.context.createBiquadFilter(); this.input = this.output = this._filter; this.Q = new Param({ context: this.context, units: "number", value: options.Q, param: this._filter.Q, }); this.frequency = new Param({ context: this.context, units: "frequency", value: options.frequency, param: this._filter.frequency, }); this.detune = new Param({ context: this.context, units: "cents", value: options.detune, param: this._filter.detune, }); this.gain = new Param({ context: this.context, units: "decibels", convert: false, value: options.gain, param: this._filter.gain, }); this.type = options.type; } static getDefaults(): BiquadFilterOptions { return Object.assign(ToneAudioNode.getDefaults(), { Q: 1, type: "lowpass" as const, frequency: 350, detune: 0, gain: 0, }); } /** * The type of this BiquadFilterNode. For a complete list of types and their attributes, see the * [Web Audio API](https://webaudio.github.io/web-audio-api/#dom-biquadfiltertype-lowpass) */ get type(): BiquadFilterType { return this._filter.type; } set type(type) { const types: BiquadFilterType[] = [ "lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking", ]; assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`); this._filter.type = type; } /** * Get the frequency response curve. This curve represents how the filter * responses to frequencies between 20hz-20khz. * @param len The number of values to return * @return The frequency response curve between 20-20kHz */ getFrequencyResponse(len = 128): Float32Array { // start with all 1s const freqValues = new Float32Array(len); for (let i = 0; i < len; i++) { const norm = Math.pow(i / len, 2); const freq = norm * (20000 - 20) + 20; freqValues[i] = freq; } const magValues = new Float32Array(len); const phaseValues = new Float32Array(len); // clone the filter to remove any connections which may be changing the value const filterClone = this.context.createBiquadFilter(); filterClone.type = this.type; filterClone.Q.value = this.Q.value; filterClone.frequency.value = this.frequency.value as number; filterClone.gain.value = this.gain.value as number; filterClone.getFrequencyResponse(freqValues, magValues, phaseValues); return magValues; } dispose(): this { super.dispose(); this._filter.disconnect(); this.Q.dispose(); this.frequency.dispose(); this.gain.dispose(); this.detune.dispose(); return this; } } ================================================ FILE: Tone/component/filter/Convolver.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer.js"; import { Convolver } from "./Convolver.js"; describe("Convolver", () => { BasicTests(Convolver); const ir = new ToneAudioBuffer(); const testFile = "./test/audio/sineStereo.wav"; before(() => { return ir.load(testFile); }); context("API", () => { it("can pass in options in the constructor", () => { const convolver = new Convolver({ normalize: false, url: testFile, }); expect(convolver.normalize).to.be.false; convolver.dispose(); }); it("can get set normalize", () => { const convolver = new Convolver(); convolver.normalize = false; expect(convolver.normalize).to.be.false; convolver.dispose(); }); it("invokes the onload function when loaded", (done) => { const convolver = new Convolver({ url: testFile, onload(): void { convolver.dispose(); done(); }, }); }); it("load returns a Promise", async () => { const convolver = new Convolver(); await convolver.load(testFile); convolver.dispose(); }); it("load invokes the second callback", async () => { const convolver = new Convolver(); await convolver.load(testFile); convolver.dispose(); }); it("can assign the buffer twice", () => { const convolver = new Convolver(ir); convolver.buffer = ir; convolver.dispose(); }); it("can be constructed with a buffer", () => { const convolver = new Convolver(ir); expect((convolver.buffer as ToneAudioBuffer).get()).to.equal( ir.get() ); convolver.dispose(); }); it("can be constructed with loaded buffer", (done) => { const buffer = new ToneAudioBuffer({ url: testFile, onload(): void { const convolver = new Convolver(buffer); expect(convolver.buffer).is.not.null; buffer.dispose(); convolver.dispose(); done(); }, }); }); it("can be constructed with unloaded buffer", (done) => { const convolver = new Convolver({ url: new ToneAudioBuffer({ url: testFile, }), onload(): void { expect(convolver.buffer).is.not.null; convolver.dispose(); done(); }, }); // is null before then expect(convolver.buffer).to.be.null; }); }); }); ================================================ FILE: Tone/component/filter/Convolver.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { noOp } from "../../core/util/Interface.js"; export interface ConvolverOptions extends ToneAudioNodeOptions { onload: () => void; normalize: boolean; url?: string | AudioBuffer | ToneAudioBuffer; } /** * Convolver is a wrapper around the Native Web Audio * [ConvolverNode](http://webaudio.github.io/web-audio-api/#the-convolvernode-interface). * Convolution is useful for reverb and filter emulation. Read more about convolution reverb on * [Wikipedia](https://en.wikipedia.org/wiki/Convolution_reverb). * * @example * // initializing the convolver with an impulse response * const convolver = new Tone.Convolver("./path/to/ir.wav").toDestination(); * @category Component */ export class Convolver extends ToneAudioNode { readonly name: string = "Convolver"; /** * The native ConvolverNode */ private _convolver: ConvolverNode = this.context.createConvolver(); /** * The Buffer belonging to the convolver */ private _buffer: ToneAudioBuffer; readonly input: Gain; readonly output: Gain; /** * @param url The URL of the impulse response or the ToneAudioBuffer containing the impulse response. * @param onload The callback to invoke when the url is loaded. */ constructor( url?: string | AudioBuffer | ToneAudioBuffer, onload?: () => void ); constructor(options?: Partial); constructor() { const options = optionsFromArguments( Convolver.getDefaults(), arguments, ["url", "onload"] ); super(options); this._buffer = new ToneAudioBuffer(options.url, (buffer) => { this.buffer = buffer; options.onload(); }); this.input = new Gain({ context: this.context }); this.output = new Gain({ context: this.context }); // set if it's already loaded, set it immediately if (this._buffer.loaded) { this.buffer = this._buffer; } // initially set normalization this.normalize = options.normalize; // connect it up this.input.chain(this._convolver, this.output); } static getDefaults(): ConvolverOptions { return Object.assign(ToneAudioNode.getDefaults(), { normalize: true, onload: noOp, }); } /** * Load an impulse response url as an audio buffer. * Decodes the audio asynchronously and invokes * the callback once the audio buffer loads. * @param url The url of the buffer to load. filetype support depends on the browser. */ async load(url: string): Promise { this.buffer = await this._buffer.load(url); } /** * The convolver's buffer */ get buffer(): ToneAudioBuffer | null { if (this._buffer.length) { return this._buffer; } else { return null; } } set buffer(buffer) { if (buffer) { this._buffer.set(buffer); } // if it's already got a buffer, create a new one if (this._convolver.buffer) { // disconnect the old one this.input.disconnect(); this._convolver.disconnect(); // create and connect a new one this._convolver = this.context.createConvolver(); this.input.chain(this._convolver, this.output); } const buff = this._buffer.get(); this._convolver.buffer = buff ? buff : null; } /** * The normalize property of the ConvolverNode interface is a boolean that * controls whether the impulse response from the buffer will be scaled by * an equal-power normalization when the buffer attribute is set, or not. */ get normalize(): boolean { return this._convolver.normalize; } set normalize(norm) { this._convolver.normalize = norm; } dispose(): this { super.dispose(); this._buffer.dispose(); this._convolver.disconnect(); return this; } } ================================================ FILE: Tone/component/filter/EQ3.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { EQ3 } from "./EQ3.js"; describe("EQ3", () => { BasicTests(EQ3); context("EQing", () => { it("can be constructed with an object", () => { const eq3 = new EQ3({ high: -10, highFrequency: 2700, low: -8, lowFrequency: 500, mid: -9, }); expect(eq3.low.value).to.be.closeTo(-8, 0.1); expect(eq3.mid.value).to.be.closeTo(-9, 0.1); expect(eq3.high.value).to.be.closeTo(-10, 0.1); expect(eq3.lowFrequency.value).to.be.closeTo(500, 0.01); expect(eq3.highFrequency.value).to.be.closeTo(2700, 0.01); eq3.dispose(); }); it("can be get and set through object", () => { const eq3 = new EQ3(); eq3.set({ high: -4, lowFrequency: 250, }); expect(eq3.get().high).to.be.closeTo(-4, 0.1); expect(eq3.get().lowFrequency).to.be.closeTo(250, 0.01); eq3.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const eq3 = new EQ3({ high: 12, low: -20, }).toDestination(); input.connect(eq3); }); }); it.skip("passes the incoming stereo signal through", () => { // return PassAudioStereo(function(input){ // var eq3 = new EQ3({ // "mid" : -2, // "high" : 2 // }).toDestination(); // input.connect(eq3); // }); }); }); }); ================================================ FILE: Tone/component/filter/EQ3.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { Param } from "../../core/context/Param.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Decibels, Frequency } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, writable } from "../../core/util/Interface.js"; import { Signal } from "../../signal/Signal.js"; import { MultibandSplit } from "../channel/MultibandSplit.js"; interface EQ3Options extends ToneAudioNodeOptions { low: Decibels; mid: Decibels; high: Decibels; lowFrequency: Frequency; highFrequency: Frequency; } /** * EQ3 provides 3 equalizer bins: Low/Mid/High. * @category Component */ export class EQ3 extends ToneAudioNode { readonly name: string = "EQ3"; /** * the input */ readonly input: MultibandSplit; /** * the output */ readonly output = new Gain({ context: this.context }); /** * Splits the input into three outputs */ private _multibandSplit: MultibandSplit; /** * The gain for the lower signals */ private _lowGain: Gain<"decibels">; /** * The gain for the mid signals */ private _midGain: Gain<"decibels">; /** * The gain for the high signals */ private _highGain: Gain<"decibels">; /** * The gain in decibels of the low part */ readonly low: Param<"decibels">; /** * The gain in decibels of the mid part */ readonly mid: Param<"decibels">; /** * The gain in decibels of the high part */ readonly high: Param<"decibels">; /** * The Q value for all of the filters. */ readonly Q: Signal<"positive">; /** * The low/mid crossover frequency. */ readonly lowFrequency: Signal<"frequency">; /** * The mid/high crossover frequency. */ readonly highFrequency: Signal<"frequency">; protected _internalChannels: ToneAudioNode[] = []; constructor(lowLevel?: Decibels, midLevel?: Decibels, highLevel?: Decibels); constructor(options: Partial); constructor() { const options = optionsFromArguments(EQ3.getDefaults(), arguments, [ "low", "mid", "high", ]); super(options); this.input = this._multibandSplit = new MultibandSplit({ context: this.context, highFrequency: options.highFrequency, lowFrequency: options.lowFrequency, }); this._lowGain = new Gain({ context: this.context, gain: options.low, units: "decibels", }); this._midGain = new Gain({ context: this.context, gain: options.mid, units: "decibels", }); this._highGain = new Gain({ context: this.context, gain: options.high, units: "decibels", }); this.low = this._lowGain.gain; this.mid = this._midGain.gain; this.high = this._highGain.gain; this.Q = this._multibandSplit.Q; this.lowFrequency = this._multibandSplit.lowFrequency; this.highFrequency = this._multibandSplit.highFrequency; // the frequency bands this._multibandSplit.low.chain(this._lowGain, this.output); this._multibandSplit.mid.chain(this._midGain, this.output); this._multibandSplit.high.chain(this._highGain, this.output); readOnly(this, ["low", "mid", "high", "lowFrequency", "highFrequency"]); this._internalChannels = [this._multibandSplit]; } static getDefaults(): EQ3Options { return Object.assign(ToneAudioNode.getDefaults(), { high: 0, highFrequency: 2500, low: 0, lowFrequency: 400, mid: 0, }); } /** * Clean up. */ dispose(): this { super.dispose(); writable(this, ["low", "mid", "high", "lowFrequency", "highFrequency"]); this._multibandSplit.dispose(); this.lowFrequency.dispose(); this.highFrequency.dispose(); this._lowGain.dispose(); this._midGain.dispose(); this._highGain.dispose(); this.low.dispose(); this.mid.dispose(); this.high.dispose(); this.Q.dispose(); return this; } } ================================================ FILE: Tone/component/filter/FeedbackCombFilter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { BitCrusher } from "../../effect/BitCrusher.js"; import { Signal } from "../../signal/index.js"; import { FeedbackCombFilter } from "./FeedbackCombFilter.js"; describe("FeedbackCombFilter", () => { BasicTests(FeedbackCombFilter); context("Comb Filtering", () => { it("can be constructed with an object", () => { const fbcf = new FeedbackCombFilter({ delayTime: 0.2, resonance: 0.3, }); expect(fbcf.delayTime.value).to.be.closeTo(0.2, 0.001); expect(fbcf.resonance.value).to.be.closeTo(0.3, 0.001); fbcf.dispose(); }); it("can be get and set through object", () => { const fbcf = new FeedbackCombFilter(); fbcf.set({ delayTime: 0.2, resonance: 0.3, }); const values = fbcf.get(); expect(values.delayTime).to.be.closeTo(0.2, 0.001); expect(values.resonance).to.be.closeTo(0.3, 0.001); fbcf.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const fbcf = new FeedbackCombFilter({ delayTime: 0.0, resonance: 0, }).toDestination(); input.connect(fbcf); }); }); it("can delay by the delayTime", async () => { const buffer = await Offline(() => { const fbcf = new FeedbackCombFilter({ delayTime: 0.1, resonance: 0, }).toDestination(); const sig = new Signal(0).connect(fbcf); sig.setValueAtTime(1, 0); }, 0.2); expect(buffer.getValueAtTime(0)).to.equal(0); expect(buffer.getValueAtTime(0.999)).to.equal(0); expect(buffer.getValueAtTime(0.101)).to.equal(1); expect(buffer.getValueAtTime(0.15)).to.equal(1); }); it("can delay with feedback", async () => { const buffer = await Offline(() => { const fbcf = new FeedbackCombFilter({ delayTime: 0.1, resonance: 0.5, }).toDestination(); const sig = new Signal(0).connect(fbcf); sig.setValueAtTime(1, 0); sig.setValueAtTime(0, 0.1); }, 0.4); expect(buffer.getValueAtTime(0)).to.equal(0); expect(buffer.getValueAtTime(0.101)).to.equal(1); expect(buffer.getValueAtTime(0.201)).to.equal(0.5); expect(buffer.getValueAtTime(0.301)).to.equal(0.25); }); }); 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); }; }); }); ================================================ FILE: Tone/component/filter/FeedbackCombFilter.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { Param } from "../../core/context/Param.js"; import { connectSeries, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { NormalRange, Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, RecursivePartial } from "../../core/util/Interface.js"; import { ToneAudioWorklet } from "../../core/worklet/ToneAudioWorklet.js"; import { workletName } from "./FeedbackCombFilter.worklet.js"; export interface FeedbackCombFilterOptions extends ToneAudioNodeOptions { delayTime: Time; resonance: NormalRange; } /** * Comb filters are basic building blocks for physical modeling. Read more * about comb filters on [CCRMA's website](https://ccrma.stanford.edu/~jos/pasp/Feedback_Comb_Filters.html). * * This comb filter is implemented with the AudioWorkletNode which allows it to have feedback delays less than the * Web Audio processing block of 128 samples. There is a polyfill for browsers that don't yet support the * AudioWorkletNode, but it will add some latency and have slower performance than the AudioWorkletNode. * @category Component */ export class FeedbackCombFilter extends ToneAudioWorklet { readonly name = "FeedbackCombFilter"; /** * The amount of delay of the comb filter. */ readonly delayTime: Param<"time">; /** * The amount of feedback of the delayed signal. */ readonly resonance: Param<"normalRange">; readonly input: Gain; readonly output: Gain; /** * @param delayTime The delay time of the filter. * @param resonance The amount of feedback the filter has. */ constructor(delayTime?: Time, resonance?: NormalRange); constructor(options?: RecursivePartial); constructor() { const options = optionsFromArguments( FeedbackCombFilter.getDefaults(), arguments, ["delayTime", "resonance"] ); super(options); this.input = new Gain({ context: this.context }); this.output = new Gain({ context: this.context }); this.delayTime = new Param<"time">({ context: this.context, value: options.delayTime, units: "time", minValue: 0, maxValue: 1, param: this._dummyParam, swappable: true, }); this.resonance = new Param<"normalRange">({ context: this.context, value: options.resonance, units: "normalRange", param: this._dummyParam, swappable: true, }); readOnly(this, ["resonance", "delayTime"]); } protected _audioWorkletName(): string { return workletName; } /** * The default parameters */ static getDefaults(): FeedbackCombFilterOptions { return Object.assign(ToneAudioNode.getDefaults(), { delayTime: 0.1, resonance: 0.5, }); } onReady(node: AudioWorkletNode) { connectSeries(this.input, node, this.output); const delayTime = node.parameters.get("delayTime") as AudioParam; this.delayTime.setParam(delayTime); const feedback = node.parameters.get("feedback") as AudioParam; this.resonance.setParam(feedback); } dispose(): this { super.dispose(); this.input.dispose(); this.output.dispose(); this.delayTime.dispose(); this.resonance.dispose(); return this; } } ================================================ FILE: Tone/component/filter/FeedbackCombFilter.worklet.ts ================================================ import "../../core/worklet/SingleIOProcessor.worklet.js"; import "../../core/worklet/DelayLine.worklet.js"; import { registerProcessor } from "../../core/worklet/WorkletGlobalScope.js"; export const workletName = "feedback-comb-filter"; const feedbackCombFilter = /* javascript */ ` class FeedbackCombFilterWorklet extends SingleIOProcessor { constructor(options) { super(options); this.delayLine = new DelayLine(this.sampleRate, options.channelCount || 2); } static get parameterDescriptors() { return [{ name: "delayTime", defaultValue: 0.1, minValue: 0, maxValue: 1, automationRate: "k-rate" }, { name: "feedback", defaultValue: 0.5, minValue: 0, maxValue: 0.9999, automationRate: "k-rate" }]; } generate(input, channel, parameters) { const delayedSample = this.delayLine.get(channel, parameters.delayTime * this.sampleRate); this.delayLine.push(channel, input + delayedSample * parameters.feedback); return delayedSample; } } `; registerProcessor(workletName, feedbackCombFilter); ================================================ FILE: Tone/component/filter/Filter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { Filter, FilterRollOff } from "./Filter.js"; describe("Filter", () => { BasicTests(Filter); context("Filtering", () => { it("can be constructed with a arguments", () => { const filter = new Filter(200, "highpass"); expect(filter.frequency.value).to.be.closeTo(200, 0.001); expect(filter.type).to.equal("highpass"); filter.dispose(); }); it("can be constructed with an object", () => { const filter = new Filter({ frequency: 340, type: "bandpass", }); expect(filter.frequency.value).to.be.closeTo(340, 0.001); expect(filter.type).to.equal("bandpass"); filter.dispose(); }); it("can set/get values as an Object", () => { const filter = new Filter(); const values = { Q: 2, frequency: 440, gain: -6, rolloff: -24 as FilterRollOff, type: "highpass" as BiquadFilterType, }; filter.set(values); expect(filter.get()).to.include.keys([ "type", "frequency", "rolloff", "Q", "gain", ]); expect(filter.type).to.equal(values.type); expect(filter.frequency.value).to.equal(values.frequency); expect(filter.rolloff).to.equal(values.rolloff); expect(filter.Q.value).to.equal(values.Q); expect(filter.gain.value).to.be.closeTo(values.gain, 0.04); filter.dispose(); }); it("can get the frequency response curve", () => { const filter = new Filter(); const curve = filter.getFrequencyResponse(32); expect(curve.length).to.equal(32); expect(curve[0]).be.closeTo(1, 0.01); expect(curve[5]).be.closeTo(0.5, 0.1); expect(curve[15]).be.closeTo(0, 0.01); expect(curve[31]).be.closeTo(0, 0.01); filter.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const filter = new Filter().toDestination(); input.connect(filter); }); }); it("only accepts filter values -12, -24, -48 and -96", () => { const filter = new Filter(); filter.rolloff = -12; expect(filter.rolloff).to.equal(-12); // @ts-ignore filter.rolloff = "-24"; expect(filter.rolloff).to.equal(-24); filter.rolloff = -48; expect(filter.rolloff).to.equal(-48); filter.rolloff = -96; expect(filter.rolloff).to.equal(-96); expect(() => { // @ts-ignore filter.rolloff = -95; }).to.throw(Error); filter.dispose(); }); it("can set the basic filter types", () => { const filter = new Filter(); const types: BiquadFilterType[] = [ "lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking", ]; for (const type of types) { filter.type = type; expect(filter.type).to.equal(type); } expect(() => { // @ts-ignore filter.type = "nontype"; }).to.throw(Error); filter.dispose(); }); it("attenuates the incoming signal", async () => { const buffer = await Offline(() => { const filter = new Filter(700, "lowpass").toDestination(); filter.Q.value = 0; const osc = new Oscillator(880).connect(filter); osc.start(0); }, 0.2); expect(buffer.getRmsAtTime(0.05)).to.be.within(0.37, 0.53); expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53); }); }); }); ================================================ FILE: Tone/component/filter/Filter.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { connectSeries, ToneAudioNode, } from "../../core/context/ToneAudioNode.js"; import { Frequency } from "../../core/type/Units.js"; import { assert } from "../../core/util/Debug.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { readOnly, writable } from "../../core/util/Interface.js"; import { isNumber } from "../../core/util/TypeCheck.js"; import { Signal } from "../../signal/Signal.js"; import { BiquadFilter, BiquadFilterOptions } from "./BiquadFilter.js"; export type FilterRollOff = -12 | -24 | -48 | -96; export type FilterOptions = BiquadFilterOptions & { rolloff: FilterRollOff; }; /** * Tone.Filter is a filter which allows for all of the same native methods * as the [BiquadFilterNode](http://webaudio.github.io/web-audio-api/#the-biquadfilternode-interface). * Tone.Filter has the added ability to set the filter rolloff at -12 * (default), -24 and -48. * @example * const filter = new Tone.Filter(1500, "highpass").toDestination(); * filter.frequency.rampTo(20000, 10); * const noise = new Tone.Noise().connect(filter).start(); * @category Component */ export class Filter extends ToneAudioNode { readonly name: string = "Filter"; readonly input = new Gain({ context: this.context }); readonly output = new Gain({ context: this.context }); private _filters: BiquadFilter[] = []; /** * the rolloff value of the filter */ private _rolloff!: FilterRollOff; private _type: BiquadFilterType; /** * The Q or Quality of the filter */ readonly Q: Signal<"positive">; /** * The cutoff frequency of the filter. */ readonly frequency: Signal<"frequency">; /** * The detune parameter */ readonly detune: Signal<"cents">; /** * The gain of the filter, only used in certain filter types */ readonly gain: Signal<"decibels">; /** * @param frequency The cutoff frequency of the filter. * @param type The type of filter. * @param rolloff The drop in decibels per octave after the cutoff frequency */ constructor( frequency?: Frequency, type?: BiquadFilterType, rolloff?: FilterRollOff ); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Filter.getDefaults(), arguments, [ "frequency", "type", "rolloff", ]); super(options); this._filters = []; this.Q = new Signal({ context: this.context, units: "positive", value: options.Q, }); this.frequency = new Signal({ context: this.context, units: "frequency", value: options.frequency, }); this.detune = new Signal({ context: this.context, units: "cents", value: options.detune, }); this.gain = new Signal({ context: this.context, units: "decibels", convert: false, value: options.gain, }); this._type = options.type; this.rolloff = options.rolloff; readOnly(this, ["detune", "frequency", "gain", "Q"]); } static getDefaults(): FilterOptions { return Object.assign(ToneAudioNode.getDefaults(), { Q: 1, detune: 0, frequency: 350, gain: 0, rolloff: -12 as FilterRollOff, type: "lowpass" as BiquadFilterType, }); } /** * The type of the filter. Types: "lowpass", "highpass", * "bandpass", "lowshelf", "highshelf", "notch", "allpass", or "peaking". */ get type(): BiquadFilterType { return this._type; } set type(type: BiquadFilterType) { const types: BiquadFilterType[] = [ "lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking", ]; assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`); this._type = type; this._filters.forEach((filter) => (filter.type = type)); } /** * The rolloff of the filter which is the drop in db * per octave. Implemented internally by cascading filters. * Only accepts the values -12, -24, -48 and -96. */ get rolloff(): FilterRollOff { return this._rolloff; } set rolloff(rolloff) { const rolloffNum = isNumber(rolloff) ? rolloff : (parseInt(rolloff, 10) as FilterRollOff); const possibilities = [-12, -24, -48, -96]; let cascadingCount = possibilities.indexOf(rolloffNum); // check the rolloff is valid assert( cascadingCount !== -1, `rolloff can only be ${possibilities.join(", ")}` ); cascadingCount += 1; this._rolloff = rolloffNum; this.input.disconnect(); this._filters.forEach((filter) => filter.disconnect()); this._filters = new Array(cascadingCount); for (let count = 0; count < cascadingCount; count++) { const filter = new BiquadFilter({ context: this.context, }); filter.type = this._type; this.frequency.connect(filter.frequency); this.detune.connect(filter.detune); this.Q.connect(filter.Q); this.gain.connect(filter.gain); this._filters[count] = filter; } this._internalChannels = this._filters; connectSeries(this.input, ...this._internalChannels, this.output); } /** * Get the frequency response curve. This curve represents how the filter * responses to frequencies between 20hz-20khz. * @param len The number of values to return * @return The frequency response curve between 20-20kHz */ getFrequencyResponse(len = 128): Float32Array { const filterClone = new BiquadFilter({ context: this.context, frequency: this.frequency.value, gain: this.gain.value, Q: this.Q.value, type: this._type, detune: this.detune.value, }); // start with all 1s const totalResponse = new Float32Array(len).map(() => 1); this._filters.forEach(() => { const response = filterClone.getFrequencyResponse(len); response.forEach((val, i) => (totalResponse[i] *= val)); }); filterClone.dispose(); return totalResponse; } /** * Clean up. */ dispose(): this { super.dispose(); this._filters.forEach((filter) => { filter.dispose(); }); writable(this, ["detune", "frequency", "gain", "Q"]); this.frequency.dispose(); this.Q.dispose(); this.detune.dispose(); this.gain.dispose(); return this; } } ================================================ FILE: Tone/component/filter/LowpassCombFilter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { LowpassCombFilter } from "./LowpassCombFilter.js"; describe("LowpassCombFilter", () => { BasicTests(LowpassCombFilter); context("Comb Filtering", () => { it("can be constructed with an object", () => { const lpcf = new LowpassCombFilter({ delayTime: 0.2, resonance: 0.3, dampening: 2400, }); expect(lpcf.delayTime.value).to.be.closeTo(0.2, 0.001); expect(lpcf.resonance.value).to.be.closeTo(0.3, 0.001); expect(lpcf.dampening).to.be.closeTo(2400, 0.001); lpcf.dispose(); }); it("can be get and set through object", () => { const lpcf = new LowpassCombFilter(); lpcf.set({ delayTime: 0.2, resonance: 0.3, dampening: 2000, }); expect(lpcf.get().delayTime).to.be.closeTo(0.2, 0.001); expect(lpcf.get().resonance).to.be.closeTo(0.3, 0.001); expect(lpcf.get().dampening).to.be.closeTo(2000, 0.001); lpcf.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const lpcf = new LowpassCombFilter(0).toDestination(); input.connect(lpcf); }); }); it("produces a decay signal at high resonance", async () => { const buffer = await Offline(() => { const lpcf = new LowpassCombFilter( 0.01, 0.9, 5000 ).toDestination(); const burst = new Oscillator(440).connect(lpcf); burst.start(0); burst.stop(0.1); }, 0.8); expect(buffer.getRmsAtTime(0.05)).to.be.within(0.2, 0.6); expect(buffer.getRmsAtTime(0.1)).to.be.within(0.2, 0.6); expect(buffer.getRmsAtTime(0.15)).to.be.within(0.15, 0.4); expect(buffer.getRmsAtTime(0.3)).to.be.within(0.01, 0.15); expect(buffer.getRmsAtTime(0.7)).to.be.below(0.01); }); it("produces a decay signal at moderate resonance", async () => { const buffer = await Offline(() => { const lpcf = new LowpassCombFilter(0.05, 0.5).toDestination(); const burst = new Oscillator(440).connect(lpcf); burst.start(0); burst.stop(0.1); }, 0.6); expect(buffer.getRmsAtTime(0.05)).to.be.closeTo(0.7, 0.1); expect(buffer.getRmsAtTime(0.1)).to.be.within(0.7, 1.1); expect(buffer.getRmsAtTime(0.2)).to.be.closeTo(0.25, 0.1); expect(buffer.getRmsAtTime(0.4)).to.be.closeTo(0.015, 0.01); }); }); }); ================================================ FILE: Tone/component/filter/LowpassCombFilter.ts ================================================ import { Param } from "../../core/context/Param.js"; import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Frequency, NormalRange, Time } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; import { RecursivePartial } from "../../core/util/Interface.js"; import { FeedbackCombFilter } from "./FeedbackCombFilter.js"; import { OnePoleFilter } from "./OnePoleFilter.js"; interface LowpassCombFilterOptions extends ToneAudioNodeOptions { delayTime: Time; resonance: NormalRange; dampening: Frequency; } /** * A lowpass feedback comb filter. It is similar to * {@link FeedbackCombFilter}, but includes a lowpass filter. * @category Component */ export class LowpassCombFilter extends ToneAudioNode { readonly name = "LowpassCombFilter"; /** * The delay node */ private _combFilter: FeedbackCombFilter; /** * The lowpass filter */ private _lowpass: OnePoleFilter; /** * The delayTime of the comb filter. */ readonly delayTime: Param<"time">; /** * The amount of feedback of the delayed signal. */ readonly resonance: Param<"normalRange">; readonly input: InputNode; readonly output: OutputNode; /** * @param delayTime The delay time of the comb filter * @param resonance The resonance (feedback) of the comb filter * @param dampening The cutoff of the lowpass filter dampens the signal as it is fed back. */ constructor( delayTime?: Time, resonance?: NormalRange, dampening?: Frequency ); constructor(options?: RecursivePartial); constructor() { const options = optionsFromArguments( LowpassCombFilter.getDefaults(), arguments, ["delayTime", "resonance", "dampening"] ); super(options); this._combFilter = this.output = new FeedbackCombFilter({ context: this.context, delayTime: options.delayTime, resonance: options.resonance, }); this.delayTime = this._combFilter.delayTime; this.resonance = this._combFilter.resonance; this._lowpass = this.input = new OnePoleFilter({ context: this.context, frequency: options.dampening, type: "lowpass", }); // connections this._lowpass.connect(this._combFilter); } static getDefaults(): LowpassCombFilterOptions { return Object.assign(ToneAudioNode.getDefaults(), { dampening: 3000, delayTime: 0.1, resonance: 0.5, }); } /** * The dampening control of the feedback */ get dampening(): Frequency { return this._lowpass.frequency; } set dampening(fq) { this._lowpass.frequency = fq; } dispose(): this { super.dispose(); this._combFilter.dispose(); this._lowpass.dispose(); return this; } } ================================================ FILE: Tone/component/filter/OnePoleFilter.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { CompareToFile } from "../../../test/helper/CompareToFile.js"; import { atTime, Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { OnePoleFilter } from "./OnePoleFilter.js"; describe("OnePoleFilter", () => { BasicTests(OnePoleFilter); it("matches a file when set to lowpass", () => { return CompareToFile( () => { const filter = new OnePoleFilter( 300, "lowpass" ).toDestination(); const osc = new Oscillator().connect(filter); osc.type = "square"; osc.start(0).stop(0.1); }, "onePoleLowpass.wav", 0.05 ); }); it("matches a file when set to highpass", () => { return CompareToFile( () => { const filter = new OnePoleFilter( 700, "highpass" ).toDestination(); const osc = new Oscillator().connect(filter); osc.type = "square"; osc.start(0).stop(0.1); }, "onePoleHighpass.wav", 0.05 ); }); context("Filtering", () => { it("can set the frequency more than once", () => { return Offline(() => { const filter = new OnePoleFilter(200); filter.frequency = 300; return atTime(0.1, () => { filter.frequency = 400; }); }, 1); }); it("can be constructed with an object", () => { const filter = new OnePoleFilter({ frequency: 400, type: "lowpass", }); expect(filter.frequency).to.be.closeTo(400, 0.1); expect(filter.type).to.equal("lowpass"); filter.dispose(); }); it("can be constructed with args", () => { const filter = new OnePoleFilter(120, "highpass"); expect(filter.frequency).to.be.closeTo(120, 0.1); expect(filter.type).to.equal("highpass"); filter.dispose(); }); it("can be get and set through object", () => { const filter = new OnePoleFilter(); filter.set({ frequency: 200, type: "highpass", }); expect(filter.get().type).to.equal("highpass"); expect(filter.get().frequency).to.be.closeTo(200, 0.1); filter.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const filter = new OnePoleFilter(5000).toDestination(); input.connect(filter); }); }); }); context("Response Curve", () => { it("can get the response curve", () => { const filter = new OnePoleFilter(); const response = filter.getFrequencyResponse(128); expect(response.length).to.equal(128); response.forEach((v) => expect(v).to.be.within(0, 1)); filter.dispose(); }); }); }); ================================================ FILE: Tone/component/filter/OnePoleFilter.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; import { Frequency } from "../../core/type/Units.js"; import { optionsFromArguments } from "../../core/util/Defaults.js"; export type OnePoleFilterType = "highpass" | "lowpass"; export interface OnePoleFilterOptions extends ToneAudioNodeOptions { frequency: Frequency; type: OnePoleFilterType; } /** * A one pole filter with 6db-per-octave rolloff. Either "highpass" or "lowpass". * Note that changing the type or frequency may result in a discontinuity which * can sound like a click or pop. * References: * * http://www.earlevel.com/main/2012/12/15/a-one-pole-filter/ * * http://www.dspguide.com/ch19/2.htm * * https://github.com/vitaliy-bobrov/js-rocks/blob/master/src/app/audio/effects/one-pole-filters.ts * @category Component */ export class OnePoleFilter extends ToneAudioNode { readonly name: string = "OnePoleFilter"; /** * Hold the current frequency */ private _frequency: Frequency; /** * the current one pole type */ private _type: OnePoleFilterType; /** * the current one pole filter */ private _filter!: IIRFilterNode; readonly input: Gain; readonly output: Gain; /** * @param frequency The frequency * @param type The filter type, either "lowpass" or "highpass" */ constructor(frequency?: Frequency, type?: OnePoleFilterType); constructor(options?: Partial); constructor() { const options = optionsFromArguments( OnePoleFilter.getDefaults(), arguments, ["frequency", "type"] ); super(options); this._frequency = options.frequency; this._type = options.type; this.input = new Gain({ context: this.context }); this.output = new Gain({ context: this.context }); this._createFilter(); } static getDefaults(): OnePoleFilterOptions { return Object.assign(ToneAudioNode.getDefaults(), { frequency: 880, type: "lowpass" as OnePoleFilterType, }); } /** * Create a filter and dispose the old one */ private _createFilter() { const oldFilter = this._filter; const freq = this.toFrequency(this._frequency); const t = 1 / (2 * Math.PI * freq); if (this._type === "lowpass") { const a0 = 1 / (t * this.context.sampleRate); const b1 = a0 - 1; this._filter = this.context.createIIRFilter([a0, 0], [1, b1]); } else { const b1 = 1 / (t * this.context.sampleRate) - 1; this._filter = this.context.createIIRFilter([1, -1], [1, b1]); } this.input.chain(this._filter, this.output); if (oldFilter) { // dispose it on the next block this.context.setTimeout(() => { if (!this.disposed) { this.input.disconnect(oldFilter); oldFilter.disconnect(); } }, this.blockTime); } } /** * The frequency value. */ get frequency(): Frequency { return this._frequency; } set frequency(fq) { this._frequency = fq; this._createFilter(); } /** * The OnePole Filter type, either "highpass" or "lowpass" */ get type(): OnePoleFilterType { return this._type; } set type(t) { this._type = t; this._createFilter(); } /** * Get the frequency response curve. This curve represents how the filter * responses to frequencies between 20hz-20khz. * @param len The number of values to return * @return The frequency response curve between 20-20kHz */ getFrequencyResponse(len = 128): Float32Array { const freqValues = new Float32Array(len); for (let i = 0; i < len; i++) { const norm = Math.pow(i / len, 2); const freq = norm * (20000 - 20) + 20; freqValues[i] = freq; } const magValues = new Float32Array(len); const phaseValues = new Float32Array(len); this._filter.getFrequencyResponse(freqValues, magValues, phaseValues); return magValues; } dispose(): this { super.dispose(); this.input.dispose(); this.output.dispose(); this._filter.disconnect(); return this; } } ================================================ FILE: Tone/component/filter/PhaseShiftAllpass.test.ts ================================================ import { BasicTests } from "../../../test/helper/Basic.js"; import { CompareToFile } from "../../../test/helper/CompareToFile.js"; import { connectTo } from "../../../test/helper/Connect.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { connect } from "../../core/context/ToneAudioNode.js"; import { Subtract } from "../../signal/Subtract.js"; import { PhaseShiftAllpass } from "./PhaseShiftAllpass.js"; describe("PhaseShiftAllpass", () => { BasicTests(PhaseShiftAllpass); context("PhaseShiftAllpass", () => { it("handles output connections", () => { const phaseShifter = new PhaseShiftAllpass(); phaseShifter.connect(connectTo()); phaseShifter.offset90.connect(connectTo()); phaseShifter.dispose(); }); it("passes the incoming signal through", () => { return PassAudio((input) => { const phaseShifter = new PhaseShiftAllpass().toDestination(); input.connect(phaseShifter); }); }); it("generates correct values with the phase shifted channel", () => { return CompareToFile( (context) => { // create impulse with 5 samples offset const constantNode = context.createConstantSource(); constantNode.start(0); const oneSampleDelay = context.createIIRFilter( [0.0, 1.0], [1.0, 0.0] ); const fiveSampleDelay = context.createIIRFilter( [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0] ); const sub = new Subtract(); connect(constantNode, oneSampleDelay); connect(constantNode, sub); connect(oneSampleDelay, sub.subtrahend); connect(sub, fiveSampleDelay); const phaseShifter = new PhaseShiftAllpass(); connect(fiveSampleDelay, phaseShifter); phaseShifter.toDestination(); }, "phaseShiftAllpass.wav", 0.001 ); }); it("generates correct values with the offset90 channel", () => { return CompareToFile( (context) => { // create impulse with 5 samples offset const constantNode = context.createConstantSource(); constantNode.start(0); const oneSampleDelay = context.createIIRFilter( [0.0, 1.0], [1.0, 0.0] ); const fiveSampleDelay = context.createIIRFilter( [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0] ); const sub = new Subtract(); connect(constantNode, oneSampleDelay); connect(constantNode, sub); connect(oneSampleDelay, sub.subtrahend); connect(sub, fiveSampleDelay); const phaseShifter = new PhaseShiftAllpass(); connect(fiveSampleDelay, phaseShifter); phaseShifter.offset90.toDestination(); }, "phaseShiftAllpass1.wav", 0.001 ); }); }); }); ================================================ FILE: Tone/component/filter/PhaseShiftAllpass.ts ================================================ import { Gain } from "../../core/context/Gain.js"; import { connectSeries, ToneAudioNode, ToneAudioNodeOptions, } from "../../core/context/ToneAudioNode.js"; /** * PhaseShiftAllpass is an very efficient implementation of a Hilbert Transform * using two Allpass filter banks whose outputs have a phase difference of 90°. * Here the `offset90` phase is offset by +90° in relation to `output`. * Coefficients and structure was developed by Olli Niemitalo. * For more details see: http://yehar.com/blog/?p=368 * @category Component */ export class PhaseShiftAllpass extends ToneAudioNode { readonly name: string = "PhaseShiftAllpass"; readonly input = new Gain({ context: this.context }); /** * The Allpass filter in the first bank */ private _bank0: IIRFilterNode[]; /** * The Allpass filter in the seconds bank */ private _bank1: IIRFilterNode[]; /** * A IIR filter implementing a delay by one sample used by the first bank */ private _oneSampleDelay: IIRFilterNode; /** * The phase shifted output */ readonly output = new Gain({ context: this.context }); /** * The PhaseShifted allpass output */ readonly offset90 = new Gain({ context: this.context }); constructor(options?: Partial) { super(options); const allpassBank1Values = [ 0.6923878, 0.9360654322959, 0.988229522686, 0.9987488452737, ]; const allpassBank2Values = [ 0.4021921162426, 0.856171088242, 0.9722909545651, 0.9952884791278, ]; this._bank0 = this._createAllPassFilterBank(allpassBank1Values); this._bank1 = this._createAllPassFilterBank(allpassBank2Values); this._oneSampleDelay = this.context.createIIRFilter( [0.0, 1.0], [1.0, 0.0] ); // connect Allpass filter banks connectSeries( this.input, ...this._bank0, this._oneSampleDelay, this.output ); connectSeries(this.input, ...this._bank1, this.offset90); } /** * Create all of the IIR filters from an array of values using the coefficient calculation. */ private _createAllPassFilterBank(bankValues: number[]): IIRFilterNode[] { const nodes: IIRFilterNode[] = bankValues.map((value) => { const coefficients = [ [value * value, 0, -1], [1, 0, -(value * value)], ]; return this.context.createIIRFilter( coefficients[0], coefficients[1] ); }); return nodes; } dispose(): this { super.dispose(); this.input.dispose(); this.output.dispose(); this.offset90.dispose(); this._bank0.forEach((f) => f.disconnect()); this._bank1.forEach((f) => f.disconnect()); this._oneSampleDelay.disconnect(); return this; } } ================================================ FILE: Tone/component/index.ts ================================================ export * from "./analysis/Analyser.js"; export * from "./analysis/DCMeter.js"; export * from "./analysis/FFT.js"; export * from "./analysis/Follower.js"; export * from "./analysis/Meter.js"; export * from "./analysis/Waveform.js"; export * from "./channel/Channel.js"; export * from "./channel/CrossFade.js"; export * from "./channel/Merge.js"; export * from "./channel/MidSideMerge.js"; export * from "./channel/MidSideSplit.js"; export * from "./channel/Mono.js"; export * from "./channel/MultibandSplit.js"; export * from "./channel/Panner.js"; export * from "./channel/Panner3D.js"; export * from "./channel/PanVol.js"; export * from "./channel/Recorder.js"; export * from "./channel/Solo.js"; export * from "./channel/Split.js"; export * from "./channel/Volume.js"; export * from "./dynamics/Compressor.js"; export * from "./dynamics/Gate.js"; export * from "./dynamics/Limiter.js"; export * from "./dynamics/MidSideCompressor.js"; export * from "./dynamics/MultibandCompressor.js"; export * from "./envelope/AmplitudeEnvelope.js"; export * from "./envelope/Envelope.js"; export * from "./envelope/FrequencyEnvelope.js"; export * from "./filter/BiquadFilter.js"; export * from "./filter/Convolver.js"; export * from "./filter/EQ3.js"; export * from "./filter/FeedbackCombFilter.js"; export * from "./filter/Filter.js"; export * from "./filter/LowpassCombFilter.js"; export * from "./filter/OnePoleFilter.js"; ================================================ FILE: Tone/core/Global.ts ================================================ import { version } from "../version.js"; import { AnyAudioContext, hasAudioContext, theWindow, } from "./context/AudioContext.js"; import { BaseContext } from "./context/BaseContext.js"; import { Context } from "./context/Context.js"; import { DummyContext } from "./context/DummyContext.js"; import { OfflineContext } from "./context/OfflineContext.js"; import { isAudioContext, isOfflineAudioContext, } from "./util/AdvancedTypeCheck.js"; /** * This dummy context is used to avoid throwing immediate errors when importing in Node.js */ const dummyContext = new DummyContext(); /** * The global audio context which is getable and assignable through * getContext and setContext */ let globalContext: BaseContext = dummyContext; /** * Returns the default system-wide {@link Context} * @category Core */ export function getContext(): BaseContext { if (globalContext === dummyContext && hasAudioContext) { setContext(new Context()); } return globalContext; } /** * Set the default audio context * @param context * @param disposeOld Pass `true` if you don't need the old context to dispose it. * @category Core */ export function setContext( context: BaseContext | AnyAudioContext, disposeOld = false ): void { if (disposeOld) { globalContext.dispose(); } if (isAudioContext(context)) { globalContext = new Context(context); } else if (isOfflineAudioContext(context)) { globalContext = new OfflineContext(context); } else { globalContext = context; } } /** * Most browsers will not play _any_ audio until a user * clicks something (like a play button). Invoke this method * on a click or keypress event handler to start the audio context. * More about the Autoplay policy * [here](https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio) * @example * document.querySelector("button").addEventListener("click", async () => { * await Tone.start(); * console.log("context started"); * }); * @category Core */ export function start(): Promise { return globalContext.resume(); } /** * Log Tone.js + version in the console. */ if (theWindow && !theWindow.TONE_SILENCE_LOGGING) { let prefix = "v"; if (version === "dev") { prefix = ""; } const printString = ` * Tone.js ${prefix}${version} * `; // eslint-disable-next-line no-console console.log(`%c${printString}`, "background: #000; color: #fff"); } ================================================ FILE: Tone/core/Tone.ts ================================================ /** * Tone.js * @author Yotam Mann * @license http://opensource.org/licenses/MIT MIT License * @copyright 2014-2024 Yotam Mann */ import { version } from "../version.js"; import { theWindow } from "./context/AudioContext.js"; import { log } from "./util/Debug.js"; //------------------------------------- // TONE //------------------------------------- export interface BaseToneOptions {} /** * Tone is the base class of all other classes. * * @category Core * @constructor */ export abstract class Tone { /** * The version number semver */ static version: string = version; /** * The name of the class */ protected abstract name: string; /** * Returns all of the default options belonging to the class. */ static getDefaults(): BaseToneOptions { return {}; } //------------------------------------- // DEBUGGING //------------------------------------- /** * Set this debug flag to log all events that happen in this class. */ debug = false; /** * Prints the outputs to the console log for debugging purposes. * Prints the contents only if either the object has a property * called `debug` set to true, or a variable called TONE_DEBUG_CLASS * is set to the name of the class. * @example * const osc = new Tone.Oscillator(); * // prints all logs originating from this oscillator * osc.debug = true; * // calls to start/stop will print in the console * osc.start(); */ protected log(...args: any[]): void { // if the object is either set to debug = true // or if there is a string on the Tone.global.with the class name if ( this.debug || (theWindow && this.toString() === theWindow.TONE_DEBUG_CLASS) ) { log(this, ...args); } } //------------------------------------- // DISPOSING //------------------------------------- /** * Indicates if the instance was disposed */ private _wasDisposed = false; /** * disconnect and dispose. */ dispose(): this { this._wasDisposed = true; return this; } /** * Indicates if the instance was disposed. 'Disposing' an * instance means that all of the Web Audio nodes that were * created for the instance are disconnected and freed for garbage collection. */ get disposed(): boolean { return this._wasDisposed; } /** * Convert the class to a string * @example * const osc = new Tone.Oscillator(); * console.log(osc.toString()); */ toString(): string { return this.name; } } ================================================ FILE: Tone/core/clock/Clock.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { atTime, Offline, whenBetween } from "../../../test/helper/Offline.js"; import { noOp } from "../util/Interface.js"; import { Clock } from "./Clock.js"; describe("Clock", () => { BasicTests(Clock); context("Get/Set values", () => { it("can get and set the frequency", () => { const clock = new Clock(noOp, 2); expect(clock.frequency.value).to.equal(2); clock.frequency.value = 0.2; expect(clock.frequency.value).to.be.closeTo(0.2, 0.001); clock.dispose(); }); it("invokes the callback when started", (done) => { const clock = new Clock((time) => { clock.dispose(); done(); }, 10).start(); }); it("can be constructed with an options object", (done) => { const clock = new Clock({ callback(): void { clock.dispose(); done(); }, frequency: 8, }).start(); expect(clock.frequency.value).to.equal(8); }); it("can get and set its values with the set/get", () => { const clock = new Clock(); clock.set({ frequency: 2, }); const gotValues = clock.get(); expect(gotValues.frequency).to.equal(2); clock.dispose(); }); }); context("State", () => { it("correctly returns the scheduled play state", () => { return Offline(() => { const clock = new Clock(); expect(clock.state).to.equal("stopped"); clock.start(0).stop(0.2); expect(clock.state).to.equal("started"); return (time) => { whenBetween(time, 0, 0.2, () => { expect(clock.state).to.equal("started"); }); whenBetween(time, 0.2, Infinity, () => { expect(clock.state).to.equal("stopped"); }); }; }, 0.3); }); it("can start, pause, and stop", () => { return Offline(() => { const clock = new Clock(); expect(clock.state).to.equal("stopped"); clock.start(0).pause(0.2).stop(0.4); expect(clock.state).to.equal("started"); return (time) => { whenBetween(time, 0, 0.2, () => { expect(clock.state).to.equal("started"); }); whenBetween(time, 0.2, 0.4, () => { expect(clock.state).to.equal("paused"); }); whenBetween(time, 0.4, Infinity, () => { expect(clock.state).to.equal("stopped"); }); }; }, 0.5); }); it("can schedule multiple start and stops", () => { return Offline(() => { const clock = new Clock(); expect(clock.state).to.equal("stopped"); clock.start(0).pause(0.1).stop(0.2).start(0.3).stop(0.4); expect(clock.state).to.equal("started"); return (time) => { whenBetween(time, 0.1, 0.2, () => { expect(clock.state).to.equal("paused"); expect(clock.ticks).to.be.greaterThan(0); }); whenBetween(time, 0.2, 0.3, () => { expect(clock.state).to.equal("stopped"); expect(clock.ticks).to.equal(0); }); whenBetween(time, 0.3, 0.4, () => { expect(clock.state).to.equal("started"); expect(clock.ticks).to.be.greaterThan(0); }); }; }, 0.5); }); it("stop and immediately start", () => { return Offline(() => { const clock = new Clock(); expect(clock.state).to.equal("stopped"); clock.start(0).stop(0.1).start(0.1); expect(clock.state).to.equal("started"); return (time) => { whenBetween(time, 0, 0.1, () => { expect(clock.state).to.equal("started"); }); whenBetween(time, 0.1, 0.5, () => { expect(clock.state).to.equal("started"); }); }; }, 0.5); }); }); context("Scheduling", () => { it("passes a time to the callback", (done) => { const clock = new Clock((time) => { expect(time).to.be.a("number"); clock.dispose(); done(); }, 10).start(); }); it("invokes the callback with a time great than now", (done) => { const clock = new Clock((time) => { clock.dispose(); expect(time).to.be.greaterThan(now); done(); }, 10); const now = clock.now(); const startTime = now + 0.1; clock.start(startTime); }); it("invokes the first callback at the given start time", (done) => { const clock = new Clock((time) => { clock.dispose(); expect(time).to.be.closeTo(startTime, 0.01); done(); }, 10); const startTime = clock.now() + 0.1; clock.start(startTime); }); it("can be scheduled to start in the future", async () => { let invocations = 0; await Offline(() => { const clock = new Clock((time) => { invocations++; }, 2).start(0.1); }, 0.4); expect(invocations).to.equal(1); }); it("invokes the right number of callbacks given the duration", async () => { let invocations = 0; await Offline(() => { new Clock((time) => { invocations++; }, 10) .start(0) .stop(0.45); }, 0.6); expect(invocations).to.equal(5); }); it("can schedule the frequency of the clock", async () => { let invocations = 0; await Offline(() => { const clock = new Clock((time, ticks) => { invocations++; }, 2); clock.start(0).stop(1.01); clock.frequency.setValueAtTime(4, 0.5); }, 2); expect(invocations).to.equal(4); }); }); context("Seconds", () => { it("can set the current seconds", () => { return Offline(() => { const clock = new Clock(noOp, 10); expect(clock.seconds).to.be.closeTo(0, 0.001); clock.seconds = 3; expect(clock.seconds).to.be.closeTo(3, 0.01); clock.dispose(); }); }); it("can get the seconds", () => { return Offline(() => { const clock = new Clock(noOp, 10); expect(clock.seconds).to.be.closeTo(0, 0.001); clock.start(0.05); return (time) => { if (time > 0.05) { expect(clock.seconds).to.be.closeTo(time - 0.05, 0.01); } }; }, 0.1); }); it("can get the seconds during a bpm ramp", () => { return Offline(() => { const clock = new Clock(noOp, 10); expect(clock.seconds).to.be.closeTo(0, 0.001); clock.start(0.05); clock.frequency.linearRampTo(60, 0.5, 0.5); return (time) => { if (time > 0.05) { expect(clock.seconds).to.be.closeTo(time - 0.05, 0.01); } }; }, 0.7); }); it("can set seconds during a bpm ramp", () => { return Offline(() => { const clock = new Clock(noOp, 10); expect(clock.seconds).to.be.closeTo(0, 0.001); clock.start(0.05); clock.frequency.linearRampTo(60, 0.5, 0.5); const changeSeconds = atTime(0.4, () => { clock.seconds = 0; }); return (time) => { changeSeconds(time); if (time > 0.05 && time < 0.4) { expect(clock.seconds).to.be.closeTo(time - 0.05, 0.01); } else if (time > 0.4) { expect(clock.seconds).to.be.closeTo(time - 0.4, 0.01); } }; }, 0.7); }); }); context("Ticks", () => { it("has 0 ticks when first created", () => { const clock = new Clock(); expect(clock.ticks).to.equal(0); clock.dispose(); }); it("can set the ticks", () => { const clock = new Clock(); expect(clock.ticks).to.equal(0); clock.ticks = 10; expect(clock.ticks).to.equal(10); clock.dispose(); }); it("increments 1 tick per callback", () => { return Offline(() => { let ticks = 0; const clock = new Clock(() => { ticks++; }, 2).start(); return atTime(0.59, () => { expect(ticks).to.equal(clock.ticks); }); }, 0.6); }); it("resets ticks on stop", () => { return Offline(() => { const clock = new Clock(noOp, 20).start(0).stop(0.1); return (time) => { whenBetween(time, 0.01, 0.09, () => { expect(clock.ticks).to.be.greaterThan(0); }); whenBetween(time, 0.1, Infinity, () => { expect(clock.ticks).to.equal(0); }); }; }, 0.2); }); it("does not reset ticks on pause but stops incrementing", () => { return Offline(() => { const clock = new Clock(noOp, 20).start(0).pause(0.1); let pausedTicks = 0; return (time) => { whenBetween(time, 0.01, 0.1, () => { expect(clock.ticks).to.be.greaterThan(0); pausedTicks = clock.ticks; }); whenBetween(time, 0.1, Infinity, () => { expect(clock.ticks).to.equal(pausedTicks); }); }; }, 0.2); }); it("starts incrementing where it left off after pause", () => { return Offline(() => { const clock = new Clock(noOp, 20) .start(0) .pause(0.1) .start(0.2); let pausedTicks = 0; let tested = false; return (time) => { whenBetween(time, 0.01, 0.1, () => { expect(clock.ticks).to.be.greaterThan(0); pausedTicks = clock.ticks; }); whenBetween(time, 0.1, 0.19, () => { expect(clock.ticks).to.equal(pausedTicks); }); whenBetween(time, 0.21, Infinity, () => { if (!tested) { tested = true; expect(clock.ticks).to.equal(pausedTicks + 1); } }); }; }, 0.3); }); it("can start with a tick offset", () => { return Offline(() => { let tested = false; const clock = new Clock((time, ticks) => { if (!tested) { tested = true; expect(ticks).to.equal(4); } }, 10); expect(clock.ticks).to.equal(0); clock.start(0, 4); }); }); }); context("Events", () => { it("triggers the start event on start", (done) => { Offline(() => { const clock = new Clock(noOp, 20); const startTime = 0.3; clock.on("start", (time, offset) => { expect(time).to.be.closeTo(startTime, 0.05); expect(offset).to.equal(0); done(); }); clock.start(startTime); }, 0.4); }); it("triggers the start event with an offset", (done) => { Offline(() => { const clock = new Clock(noOp, 20); const startTime = 0.3; clock.on("start", (time, offset) => { expect(time).to.be.closeTo(startTime, 0.05); expect(offset).to.equal(2); done(); }); clock.start(startTime, 2); }, 0.4); }); it("triggers stop event", (done) => { Offline(() => { const clock = new Clock(noOp, 20); const stopTime = 0.3; clock.on("stop", (time) => { expect(time).to.be.closeTo(stopTime, 0.05); done(); }); clock.start().stop(stopTime); }, 0.4); }); it("triggers pause stop event", (done) => { Offline(() => { const clock = new Clock(noOp, 20); clock .on("pause", (time) => { expect(time).to.be.closeTo(0.1, 0.05); }) .on("stop", (time) => { expect(time).to.be.closeTo(0.2, 0.05); done(); }); clock.start().pause(0.1).stop(0.2); }, 0.4); }); it("triggers events even in close proximity", (done) => { Offline(() => { const clock = new Clock(noOp, 20); let invokedStartEvent = false; clock.on("start", () => { invokedStartEvent = true; }); clock.on("stop", () => { expect(invokedStartEvent).to.equal(true); done(); }); clock.start(0.09999).stop(0.1); }, 0.4); }); it("triggers 'start' event when time is in the past", (done) => { const clock = new Clock(noOp, 20); clock.on("start", () => { done(); clock.dispose(); }); setTimeout(() => { clock.start(0); }, 100); }); it("triggers 'stop' event when time is in the past", (done) => { const clock = new Clock(noOp, 20); clock.on("stop", () => { done(); clock.dispose(); }); setTimeout(() => { clock.start(0); }, 100); setTimeout(() => { clock.stop(0); }, 200); }); it("triggers 'pause' event when time is in the past", (done) => { const clock = new Clock(noOp, 20); clock.on("pause", () => { done(); clock.dispose(); }); setTimeout(() => { clock.start(0); }, 100); setTimeout(() => { clock.pause(0); }, 200); }); }); context("[get/set]Ticks", () => { it("always reports 0 if not started", () => { return Offline(() => { const clock = new Clock(noOp, 20); expect(clock.getTicksAtTime(0)).to.equal(0); expect(clock.getTicksAtTime(1)).to.equal(0); expect(clock.getTicksAtTime(2)).to.equal(0); clock.dispose(); }); }); it("can get ticks in the future", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(1); expect(clock.getTicksAtTime(1)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(1.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(20, 0.01); clock.dispose(); }); }); it("pauses on last ticks", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0).pause(1); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(20, 0.01); clock.dispose(); }); }); it("resumes from paused position", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0).pause(1).start(2); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(40, 0.01); expect(clock.getTicksAtTime(3.5)).to.be.closeTo(50, 0.01); clock.dispose(); }); }); it("can get tick position after multiple pauses", () => { return Offline(() => { const clock = new Clock(noOp, 10); clock.start(0).pause(1).start(2).pause(3).start(4); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(5, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(4)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(5)).to.be.closeTo(30, 0.01); clock.dispose(); }); }); it("can get tick position after multiple pauses and tempo scheduling", () => { return Offline(() => { const clock = new Clock(noOp, 10); clock.frequency.setValueAtTime(100, 3.5); clock.start(0).pause(1).start(2).pause(3).start(4); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(5, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(4)).to.be.closeTo(20, 0.01); expect(clock.getTicksAtTime(5)).to.be.closeTo(120, 0.01); clock.dispose(); }); }); it("can get tick position after multiple pauses and setting ticks", () => { return Offline(() => { const clock = new Clock(noOp, 10); clock.start(0).pause(1).start(2).pause(3).start(4); clock.setTicksAtTime(10, 2.5); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(5, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(15, 0.01); expect(clock.getTicksAtTime(4)).to.be.closeTo(15, 0.01); expect(clock.getTicksAtTime(5)).to.be.closeTo(25, 0.01); clock.dispose(); }); }); it("resumes from paused position with tempo scheduling", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0).pause(1).start(2); clock.frequency.setValueAtTime(20, 0); clock.frequency.setValueAtTime(10, 0.5); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(15, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(15, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(25, 0.01); expect(clock.getTicksAtTime(3.5)).to.be.closeTo(30, 0.01); clock.dispose(); }); }); it("can set a tick value at the given time", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0); clock.setTicksAtTime(0, 1); clock.setTicksAtTime(0, 2); expect(clock.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(1.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(2.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(20, 0.01); clock.dispose(); }); }); it("can get a tick position while the frequency is scheduled with setValueAtTime", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0); clock.frequency.setValueAtTime(2, 1); clock.setTicksAtTime(0, 1); clock.setTicksAtTime(0, 2); expect(clock.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(1.5)).to.be.closeTo(1, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(2.5)).to.be.closeTo(1, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(2, 0.01); clock.dispose(); }); }); it("can get a tick position while the frequency is scheduled with linearRampTo", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0); clock.frequency.linearRampTo(2, 1, 1); clock.setTicksAtTime(0, 1); clock.setTicksAtTime(10, 2); expect(clock.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(1.5)).to.be.closeTo(7.75, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2.5)).to.be.closeTo(11, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(12, 0.01); clock.dispose(); }); }); it("can get a tick position while the frequency is scheduled with exponentialRampTo", () => { return Offline(() => { const clock = new Clock(noOp, 20); clock.start(0); clock.frequency.exponentialRampTo(2, 1, 1); clock.setTicksAtTime(0, 1); clock.setTicksAtTime(10, 2); expect(clock.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(1)).to.be.closeTo(0, 0.01); expect(clock.getTicksAtTime(1.5)).to.be.closeTo(5.96, 0.01); expect(clock.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(clock.getTicksAtTime(2.5)).to.be.closeTo(11, 0.01); expect(clock.getTicksAtTime(3)).to.be.closeTo(12, 0.01); clock.dispose(); }); }); }); }); ================================================ FILE: Tone/core/clock/Clock.ts ================================================ import { ToneWithContext, ToneWithContextOptions, } from "../context/ToneWithContext.js"; import { Frequency, Hertz, Seconds, Ticks, Time } from "../type/Units.js"; import { assertContextRunning } from "../util/Debug.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { Emitter } from "../util/Emitter.js"; import { noOp, readOnly } from "../util/Interface.js"; import { PlaybackState, StateTimeline } from "../util/StateTimeline.js"; import { TickSignal } from "./TickSignal.js"; import { TickSource } from "./TickSource.js"; type ClockCallback = (time: Seconds, ticks?: Ticks) => void; interface ClockOptions extends ToneWithContextOptions { frequency: Hertz; callback: ClockCallback; units: "hertz" | "bpm"; } type ClockEvent = "start" | "stop" | "pause"; /** * A sample-accurate clock that provides a callback at a given rate. * * While the callback is not sample-accurate (it is susceptible to * loose JavaScript timing), the time passed to the callback is precise. * * For most applications, it is better to use {@link Transport} instead of the * Clock by itself, since you can synchronize multiple callbacks. * * @example * // The callback will be invoked approximately once a second, * // and it will print the time exactly one second apart. * const clock = new Tone.Clock(time => { * console.log(time); * }, 1); * clock.start(); * @category Core */ export class Clock extends ToneWithContext implements Emitter { readonly name: string = "Clock"; /** * The callback function to invoke at the scheduled tick. */ callback: ClockCallback = noOp; /** * The tick counter */ private _tickSource: TickSource; /** * The last time the loop callback was invoked */ private _lastUpdate = 0; /** * Keep track of the playback state */ private _state: StateTimeline = new StateTimeline("stopped"); /** * Context bound reference to the _loop method * This is necessary to remove the event in the end. */ private _boundLoop: () => void = this._loop.bind(this); /** * The rate the callback function should be invoked. */ frequency: TickSignal; /** * @param callback The callback to be invoked with the time of the audio event * @param frequency The rate of the callback */ constructor(callback?: ClockCallback, frequency?: Frequency); constructor(options: Partial); constructor() { const options = optionsFromArguments(Clock.getDefaults(), arguments, [ "callback", "frequency", ]); super(options); this.callback = options.callback; this._tickSource = new TickSource({ context: this.context, frequency: options.frequency, units: options.units, }); this._lastUpdate = 0; this.frequency = this._tickSource.frequency; readOnly(this, "frequency"); // add an initial state this._state.setStateAtTime("stopped", 0); // bind a callback to the worker thread this.context.on("tick", this._boundLoop); } static getDefaults(): ClockOptions { return Object.assign(ToneWithContext.getDefaults(), { callback: noOp as ClockCallback, frequency: 1, units: "hertz", }) as ClockOptions; } /** * The playback state of the clock, either "started", "stopped", or "paused". */ get state(): PlaybackState { return this._state.getValueAtTime(this.now()); } /** * Start the clock at the given time. * @param time The time the clock should start. * @param offset The number of ticks to start the clock from. */ start(time?: Time, offset?: Ticks): this { // make sure the context is running assertContextRunning(this.context); // start the loop const computedTime = this.toSeconds(time); this.log("start", computedTime); if (this._state.getValueAtTime(computedTime) !== "started") { this._state.setStateAtTime("started", computedTime); this._tickSource.start(computedTime, offset); if (computedTime < this._lastUpdate) { this.emit("start", computedTime, offset); } } return this; } /** * Stop the clock. Stopping the clock resets the tick counter to 0. * @param time The time when the clock should stop. * @example * const clock = new Tone.Clock(time => { * console.log(time); * }, 1); * clock.start(); * // Stop the clock after 10 seconds. * clock.stop("+10"); */ stop(time?: Time): this { const computedTime = this.toSeconds(time); this.log("stop", computedTime); this._state.cancel(computedTime); this._state.setStateAtTime("stopped", computedTime); this._tickSource.stop(computedTime); if (computedTime < this._lastUpdate) { this.emit("stop", computedTime); } return this; } /** * Pause the clock. Pausing does not reset the tick counter. * @param time The time when the clock should pause. */ pause(time?: Time): this { const computedTime = this.toSeconds(time); if (this._state.getValueAtTime(computedTime) === "started") { this._state.setStateAtTime("paused", computedTime); this._tickSource.pause(computedTime); if (computedTime < this._lastUpdate) { this.emit("pause", computedTime); } } return this; } /** * The number of times the callback has been invoked. * * Starts counting at 0 and increments after the callback is invoked. */ get ticks(): Ticks { return Math.ceil(this.getTicksAtTime(this.now())); } set ticks(t: Ticks) { this._tickSource.ticks = t; } /** * The time since ticks=0 that the clock has been running. * * Accounts for tempo curves. */ get seconds(): Seconds { return this._tickSource.seconds; } set seconds(s: Seconds) { this._tickSource.seconds = s; } /** * Return the elapsed seconds at the given time. * @param time When to get the elapsed seconds. * @return The number of elapsed seconds. */ getSecondsAtTime(time: Time): Seconds { return this._tickSource.getSecondsAtTime(time); } /** * Set the clock's ticks at the given time. * @param ticks The tick value to set. * @param time When to set the tick value. */ setTicksAtTime(ticks: Ticks, time: Time): this { this._tickSource.setTicksAtTime(ticks, time); return this; } /** * Get the time of the given tick. * * The second argument is when to test before. Since ticks can be set * (with {@link setTicksAtTime}), there may be multiple times for a given * tick value. * * @param tick The tick number. * @param before When to measure the tick value from. * @return The time of the tick */ getTimeOfTick(tick: Ticks, before = this.now()): Seconds { return this._tickSource.getTimeOfTick(tick, before); } /** * Get the clock's ticks at the given time. * @param time When to get the tick value. * @return The tick value at the given time. */ getTicksAtTime(time?: Time): Ticks { return this._tickSource.getTicksAtTime(time); } /** * Get the time of the next tick. * @param offset The tick number. */ nextTickTime(offset: Ticks, when: Time): Seconds { const computedTime = this.toSeconds(when); const currentTick = this.getTicksAtTime(computedTime); return this._tickSource.getTimeOfTick( currentTick + offset, computedTime ); } /** * The scheduling loop. */ private _loop(): void { const startTime = this._lastUpdate; const endTime = this.now(); this._lastUpdate = endTime; this.log("loop", startTime, endTime); if (startTime !== endTime) { // the state change events this._state.forEachBetween(startTime, endTime, (e) => { switch (e.state) { case "started": const offset = this._tickSource.getTicksAtTime(e.time); this.emit("start", e.time, offset); break; case "stopped": if (e.time !== 0) { this.emit("stop", e.time); } break; case "paused": this.emit("pause", e.time); break; } }); // the tick callbacks this._tickSource.forEachTickBetween( startTime, endTime, (time, ticks) => { this.callback(time, ticks); } ); } } /** * Returns the scheduled state at the given time. * @param time The time to query. * @return The name of the state input in setStateAtTime. * @example * const clock = new Tone.Clock(); * clock.start("+0.1"); * clock.getStateAtTime("+0.1"); // returns "started" */ getStateAtTime(time: Time): PlaybackState { const computedTime = this.toSeconds(time); return this._state.getValueAtTime(computedTime); } /** * Clean up */ dispose(): this { super.dispose(); this.context.off("tick", this._boundLoop); this._tickSource.dispose(); this._state.dispose(); return this; } //------------------------------------- // EMITTER MIXIN TO SATISFY COMPILER //------------------------------------- on!: (event: ClockEvent, callback: (...args: any[]) => void) => this; once!: (event: ClockEvent, callback: (...args: any[]) => void) => this; off!: ( event: ClockEvent, callback?: ((...args: any[]) => void) | undefined ) => this; emit!: (event: any, ...args: any[]) => this; } Emitter.mixin(Clock); ================================================ FILE: Tone/core/clock/TickParam.test.ts ================================================ import { BasicTests, testAudioContext } from "../../../test/helper/Basic.js"; // import { atTime, Offline } from "../../../test/helper/Offline"; import { TickParam } from "./TickParam.js"; describe("TickParam", () => { // sanity checks BasicTests(TickParam, { context: testAudioContext, param: testAudioContext.createOscillator().frequency, }); }); ================================================ FILE: Tone/core/clock/TickParam.ts ================================================ import { AutomationEvent, Param, ParamOptions } from "../context/Param.js"; import { Seconds, Ticks, Time, UnitMap, UnitName } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { Timeline } from "../util/Timeline.js"; import { isUndef } from "../util/TypeCheck.js"; type TickAutomationEvent = AutomationEvent & { ticks: number; }; interface TickParamOptions extends ParamOptions { multiplier: number; } /** * A Param class just for computing ticks. Similar to the {@link Param} class, * but offers conversion to BPM values as well as ability to compute tick * duration and elapsed ticks */ export class TickParam< TypeName extends "hertz" | "bpm", > extends Param { readonly name: string = "TickParam"; /** * The timeline which tracks all of the automations. */ protected _events: Timeline = new Timeline(Infinity); /** * The internal holder for the multiplier value */ private _multiplier = 1; /** * @param param The AudioParam to wrap * @param units The unit name * @param convert Whether or not to convert the value to the target units */ /** * @param value The initial value of the signal */ constructor(value?: number); constructor(options: Partial>); constructor() { const options = optionsFromArguments( TickParam.getDefaults(), arguments, ["value"] ); super(options); // set the multiplier this._multiplier = options.multiplier; // clear the ticks from the beginning this._events.cancel(0); // set an initial event this._events.add({ ticks: 0, time: 0, type: "setValueAtTime", value: this._fromType(options.value), }); this.setValueAtTime(options.value, 0); } static getDefaults(): TickParamOptions { return Object.assign(Param.getDefaults(), { multiplier: 1, units: "hertz", value: 1, }); } setTargetAtTime( value: UnitMap[TypeName], time: Time, constant: number ): this { // approximate it with multiple linear ramps time = this.toSeconds(time); this.setRampPoint(time); const computedValue = this._fromType(value); // start from previously scheduled value const prevEvent = this._events.get(time) as TickAutomationEvent; const segments = Math.round(Math.max(1 / constant, 1)); for (let i = 0; i <= segments; i++) { const segTime = constant * i + time; const rampVal = this._exponentialApproach( prevEvent.time, prevEvent.value, computedValue, constant, segTime ); this.linearRampToValueAtTime(this._toType(rampVal), segTime); } return this; } setValueAtTime(value: UnitMap[TypeName], time: Time): this { const computedTime = this.toSeconds(time); super.setValueAtTime(value, time); const event = this._events.get(computedTime) as TickAutomationEvent; const previousEvent = this._events.previousEvent(event); const ticksUntilTime = this._getTicksUntilEvent( previousEvent, computedTime ); event.ticks = Math.max(ticksUntilTime, 0); return this; } linearRampToValueAtTime(value: UnitMap[TypeName], time: Time): this { const computedTime = this.toSeconds(time); super.linearRampToValueAtTime(value, time); const event = this._events.get(computedTime) as TickAutomationEvent; const previousEvent = this._events.previousEvent(event); const ticksUntilTime = this._getTicksUntilEvent( previousEvent, computedTime ); event.ticks = Math.max(ticksUntilTime, 0); return this; } exponentialRampToValueAtTime(value: UnitMap[TypeName], time: Time): this { // approximate it with multiple linear ramps time = this.toSeconds(time); const computedVal = this._fromType(value); // start from previously scheduled value const prevEvent = this._events.get(time) as TickAutomationEvent; // approx 10 segments per second const segments = Math.round(Math.max((time - prevEvent.time) * 10, 1)); const segmentDur = (time - prevEvent.time) / segments; for (let i = 0; i <= segments; i++) { const segTime = segmentDur * i + prevEvent.time; const rampVal = this._exponentialInterpolate( prevEvent.time, prevEvent.value, time, computedVal, segTime ); this.linearRampToValueAtTime(this._toType(rampVal), segTime); } return this; } /** * Returns the tick value at the time. Takes into account * any automation curves scheduled on the signal. * @param event The time to get the tick count at * @return The number of ticks which have elapsed at the time given any automations. */ private _getTicksUntilEvent( event: TickAutomationEvent | null, time: number ): Ticks { if (event === null) { event = { ticks: 0, time: 0, type: "setValueAtTime", value: 0, }; } else if (isUndef(event.ticks)) { const previousEvent = this._events.previousEvent(event); event.ticks = this._getTicksUntilEvent(previousEvent, event.time); } const val0 = this._fromType(this.getValueAtTime(event.time)); let val1 = this._fromType(this.getValueAtTime(time)); // if it's right on the line, take the previous value const onTheLineEvent = this._events.get(time); if ( onTheLineEvent && onTheLineEvent.time === time && onTheLineEvent.type === "setValueAtTime" ) { val1 = this._fromType(this.getValueAtTime(time - this.sampleTime)); } return 0.5 * (time - event.time) * (val0 + val1) + event.ticks; } /** * Returns the tick value at the time. Takes into account * any automation curves scheduled on the signal. * @param time The time to get the tick count at * @return The number of ticks which have elapsed at the time given any automations. */ getTicksAtTime(time: Time): Ticks { const computedTime = this.toSeconds(time); const event = this._events.get(computedTime); return Math.max(this._getTicksUntilEvent(event, computedTime), 0); } /** * Return the elapsed time of the number of ticks from the given time * @param ticks The number of ticks to calculate * @param time The time to get the next tick from * @return The duration of the number of ticks from the given time in seconds */ getDurationOfTicks(ticks: Ticks, time: Time): Seconds { const computedTime = this.toSeconds(time); const currentTick = this.getTicksAtTime(time); return this.getTimeOfTick(currentTick + ticks) - computedTime; } /** * Given a tick, returns the time that tick occurs at. * @return The time that the tick occurs. */ getTimeOfTick(tick: Ticks): Seconds { const before = this._events.get(tick, "ticks"); const after = this._events.getAfter(tick, "ticks"); if (before && before.ticks === tick) { return before.time; } else if ( before && after && after.type === "linearRampToValueAtTime" && before.value !== after.value ) { const val0 = this._fromType(this.getValueAtTime(before.time)); const val1 = this._fromType(this.getValueAtTime(after.time)); const delta = (val1 - val0) / (after.time - before.time); const k = Math.sqrt( Math.pow(val0, 2) - 2 * delta * (before.ticks - tick) ); const sol1 = (-val0 + k) / delta; const sol2 = (-val0 - k) / delta; return (sol1 > 0 ? sol1 : sol2) + before.time; } else if (before) { if (before.value === 0) { return Infinity; } else { return before.time + (tick - before.ticks) / before.value; } } else { return tick / this._initialValue; } } /** * Convert some number of ticks their the duration in seconds accounting * for any automation curves starting at the given time. * @param ticks The number of ticks to convert to seconds. * @param when When along the automation timeline to convert the ticks. * @return The duration in seconds of the ticks. */ ticksToTime(ticks: Ticks, when: Time): Seconds { return this.getDurationOfTicks(ticks, when); } /** * The inverse of {@link ticksToTime}. Convert a duration in * seconds to the corresponding number of ticks accounting for any * automation curves starting at the given time. * @param duration The time interval to convert to ticks. * @param when When along the automation timeline to convert the ticks. * @return The duration in ticks. */ timeToTicks(duration: Time, when: Time): Ticks { const computedTime = this.toSeconds(when); const computedDuration = this.toSeconds(duration); const startTicks = this.getTicksAtTime(computedTime); const endTicks = this.getTicksAtTime(computedTime + computedDuration); return endTicks - startTicks; } /** * Convert from the type when the unit value is BPM */ protected _fromType(val: UnitMap[TypeName]): number { if (this.units === "bpm" && this.multiplier) { return 1 / (60 / val / this.multiplier); } else { return super._fromType(val); } } /** * Special case of type conversion where the units === "bpm" */ protected _toType(val: number): UnitMap[TypeName] { if (this.units === "bpm" && this.multiplier) { return ((val / this.multiplier) * 60) as UnitMap[TypeName]; } else { return super._toType(val); } } /** * A multiplier on the bpm value. Useful for setting a PPQ relative to the base frequency value. */ get multiplier(): number { return this._multiplier; } set multiplier(m: number) { // get and reset the current value with the new multiplier // might be necessary to clear all the previous values const currentVal = this.value; this._multiplier = m; this.cancelScheduledValues(0); this.setValueAtTime(currentVal, 0); } } ================================================ FILE: Tone/core/clock/TickSignal.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { TickSignal } from "./TickSignal.js"; describe("TickSignal", () => { BasicTests(TickSignal); it("can be created and disposed", () => { const tickSignal = new TickSignal(); tickSignal.dispose(); }); it("can schedule a change in the future", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 0.2); tickSignal.dispose(); }); it("can schedule a ramp in the future", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 0); tickSignal.linearRampToValueAtTime(0.1, 0.2); tickSignal.exponentialRampToValueAtTime(1, 0.4); tickSignal.dispose(); }); it("calculates the ticks when no changes are scheduled", () => { const tickSignal0 = new TickSignal(2); expect(tickSignal0.getTicksAtTime(1)).to.be.closeTo(2, 0.01); expect(tickSignal0.getTicksAtTime(2)).to.be.closeTo(4, 0.01); expect(tickSignal0.getTimeOfTick(4)).to.be.closeTo(2, 0.01); tickSignal0.dispose(); const tickSignal1 = new TickSignal(1); expect(tickSignal1.getTicksAtTime(1)).to.be.closeTo(1, 0.01); expect(tickSignal1.getTicksAtTime(2)).to.be.closeTo(2, 0.01); expect(tickSignal1.getTimeOfTick(2)).to.be.closeTo(2, 0.01); tickSignal1.dispose(); }); it("calculates the ticks in the future when a setValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 0.5); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.5, 0.01); expect(tickSignal.getTicksAtTime(0.75)).to.be.closeTo(1, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1.5, 0.01); expect(tickSignal.getTimeOfTick(1.5)).to.be.closeTo(1, 0.01); tickSignal.dispose(); }); it("calculates the ticks in the future when multiple setValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 1); tickSignal.setValueAtTime(4, 2); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.5, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1, 0.01); expect(tickSignal.getTicksAtTime(1.5)).to.be.closeTo(2, 0.01); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(3, 0.01); expect(tickSignal.getTicksAtTime(2.5)).to.be.closeTo(5, 0.01); expect(tickSignal.getTicksAtTime(3)).to.be.closeTo(7, 0.01); expect(tickSignal.getTimeOfTick(7)).to.be.closeTo(3, 0.01); tickSignal.dispose(); }); it("if ticks are 0, getTicksAtTime will return 0", () => { const tickSignal = new TickSignal(0); tickSignal.setValueAtTime(0, 1); tickSignal.linearRampToValueAtTime(0, 2); expect(tickSignal.getTicksAtTime(0)).to.equal(0); expect(tickSignal.getTicksAtTime(1)).to.equal(0); expect(tickSignal.getTicksAtTime(2)).to.equal(0); expect(tickSignal.getTicksAtTime(3)).to.equal(0); tickSignal.dispose(); }); it("calculates the ticks in the future when a linearRampToValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.linearRampToValueAtTime(2, 1); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.62, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1.5, 0.01); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(3.5, 0.01); tickSignal.dispose(); }); it("calculates the ticks in the future when multiple linearRampToValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.linearRampToValueAtTime(2, 1); tickSignal.linearRampToValueAtTime(0, 2); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.62, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1.5, 0.01); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(2.5, 0.01); expect(tickSignal.getTicksAtTime(3)).to.be.closeTo(2.5, 0.01); tickSignal.dispose(); }); it("calculates the ticks in the future when a exponentialRampToValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.exponentialRampToValueAtTime(2, 1); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.6, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1.5, 0.1); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(3.5, 0.1); expect(tickSignal.getTicksAtTime(3)).to.be.closeTo(5.5, 0.1); tickSignal.dispose(); }); it("calculates the ticks in the future when multiple exponentialRampToValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.exponentialRampToValueAtTime(2, 1); tickSignal.exponentialRampToValueAtTime(0, 2); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(0.6, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(1.5, 0.1); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(1.54, 0.1); expect(tickSignal.getTicksAtTime(3)).to.be.closeTo(1.54, 0.1); tickSignal.dispose(); }); it("computes the time of a given tick when setTargetAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setTargetAtTime(0.5, 0, 0.1); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(1.89, 0.01); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(3.89, 0.01); tickSignal.dispose(); }); it("computes the time of a given tick when multiple setTargetAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setTargetAtTime(0.5, 0, 0.1); tickSignal.setTargetAtTime(2, 1, 1); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(1.5, 0.1); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(2.28, 0.1); tickSignal.dispose(); }); it("computes the time of a given tick when nothing is scheduled", () => { const tickSignal0 = new TickSignal(1); expect(tickSignal0.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal0.getTimeOfTick(1)).to.be.closeTo(1, 0.01); expect(tickSignal0.getTimeOfTick(2)).to.be.closeTo(2, 0.01); expect(tickSignal0.getTimeOfTick(3)).to.be.closeTo(3, 0.01); tickSignal0.dispose(); const tickSignal = new TickSignal(2); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(0.5, 0.01); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(1, 0.01); expect(tickSignal.getTimeOfTick(3)).to.be.closeTo(1.5, 0.01); tickSignal.dispose(); }); it("computes the time of a given tick when setValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(0.5, 1); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(1, 0.01); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(3, 0.01); expect(tickSignal.getTimeOfTick(3)).to.be.closeTo(5, 0.01); tickSignal.dispose(); }); it("returns Infinity if the tick interval is 0", () => { const tickSignal = new TickSignal(0); expect(tickSignal.getTimeOfTick(1)).to.equal(Infinity); tickSignal.dispose(); }); it("computes the time of a given tick when multiple setValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(0.5, 1); tickSignal.setValueAtTime(0, 2); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(1, 0.01); expect(tickSignal.getTimeOfTick(1.499)).to.be.closeTo(2, 0.01); expect(tickSignal.getTimeOfTick(2)).to.equal(Infinity); tickSignal.dispose(); }); it("computes the time of a given tick when a linearRampToValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.linearRampToValueAtTime(2, 1); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(0.75, 0.1); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(1.25, 0.1); expect(tickSignal.getTimeOfTick(3)).to.be.closeTo(1.75, 0.1); tickSignal.dispose(); }); it("computes the time of a given tick when multiple linearRampToValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.linearRampToValueAtTime(2, 1); tickSignal.linearRampToValueAtTime(0, 2); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.1); expect(tickSignal.getTimeOfTick(1)).to.be.closeTo(0.75, 0.1); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(1.25, 0.1); expect(tickSignal.getTimeOfTick(3)).to.equal(Infinity); tickSignal.dispose(); }); it("computes the time of a given tick when a exponentialRampToValueAtTime is scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.exponentialRampToValueAtTime(2, 1); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(2)).to.be.closeTo(1.25, 0.1); expect(tickSignal.getTimeOfTick(3)).to.be.closeTo(1.75, 0.1); tickSignal.dispose(); }); it("computes the time of a given tick when multiple exponentialRampToValueAtTime are scheduled", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.exponentialRampToValueAtTime(2, 1); tickSignal.exponentialRampToValueAtTime(0, 2); expect(tickSignal.getTimeOfTick(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTimeOfTick(0.5)).to.be.closeTo(0.5, 0.1); expect(tickSignal.getTimeOfTick(1.5)).to.be.closeTo(1, 0.1); expect(tickSignal.getTimeOfTick(3)).to.equal(Infinity); tickSignal.dispose(); }); it("can schedule multiple types of curves", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(1, 0); tickSignal.exponentialRampToValueAtTime(4, 1); tickSignal.linearRampToValueAtTime(0.2, 2); tickSignal.setValueAtTime(2, 3); tickSignal.linearRampToValueAtTime(2, 4); tickSignal.setTargetAtTime(8, 5, 0.2); for (let time = 0; time < 5; time += 0.2) { const tick = tickSignal.getTicksAtTime(time); expect(tickSignal.getTimeOfTick(tick)).to.be.closeTo(time, 0.1); } tickSignal.dispose(); }); it("can get the duration of a tick at any point in time", () => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 1); tickSignal.setValueAtTime(10, 2); expect(tickSignal.getDurationOfTicks(1, 0)).to.be.closeTo(1, 0.01); expect(tickSignal.getDurationOfTicks(1, 1)).to.be.closeTo(0.5, 0.01); expect(tickSignal.getDurationOfTicks(1, 2)).to.be.closeTo(0.1, 0.01); expect(tickSignal.getDurationOfTicks(2, 1.5)).to.be.closeTo(0.6, 0.01); }); context("BPM / PPQ", () => { it("can be set as PPQ", () => { const tickSignal = new TickSignal({ multiplier: 10, units: "bpm", value: 120, }); expect(tickSignal.multiplier).to.equal(10); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(20, 0.01); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(40, 0.01); expect(tickSignal.getTimeOfTick(40)).to.be.closeTo(2, 0.01); tickSignal.dispose(); }); it("calculates the ticks in the future when multiple setValueAtTime are scheduled", () => { const tickSignal = new TickSignal({ multiplier: 20, units: "bpm", value: 60, }); tickSignal.setValueAtTime(120, 1); tickSignal.setValueAtTime(180, 2); expect(tickSignal.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(tickSignal.getTicksAtTime(0.5)).to.be.closeTo(10, 0.01); expect(tickSignal.getTicksAtTime(1)).to.be.closeTo(20, 0.01); expect(tickSignal.getTicksAtTime(1.5)).to.be.closeTo(40, 0.01); expect(tickSignal.getTicksAtTime(2)).to.be.closeTo(60, 0.01); expect(tickSignal.getTicksAtTime(2.5)).to.be.closeTo(90, 0.01); expect(tickSignal.getTicksAtTime(3)).to.be.closeTo(120, 0.01); expect(tickSignal.getTimeOfTick(120)).to.be.closeTo(3, 0.01); tickSignal.dispose(); }); }); it("outputs a signal", async () => { const buffer = await Offline((context) => { const sched = new TickSignal(1).connect(context.destination); sched.linearRampTo(3, 1, 0); }, 1.01); expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(2, 0.01); expect(buffer.getValueAtTime(1)).to.be.closeTo(3, 0.01); }); it("outputs a signal with bpm units", async () => { const buffer = await Offline((context) => { const sched = new TickSignal({ units: "bpm", value: 120, }).connect(context.destination); sched.linearRampTo(60, 1, 0); }, 1.01); expect(buffer.getValueAtTime(0)).to.be.closeTo(2, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(1.5, 0.01); expect(buffer.getValueAtTime(1)).to.be.closeTo(1, 0.01); }); it("outputs a signal with bpm units and a multiplier", async () => { const buffer = await Offline((context) => { const sched = new TickSignal({ multiplier: 10, units: "bpm", value: 60, }).connect(context.destination); sched.linearRampTo(120, 1, 0); }, 1.01); expect(buffer.getValueAtTime(0)).to.be.closeTo(10, 0.01); expect(buffer.getValueAtTime(0.5)).to.be.closeTo(15, 0.01); expect(buffer.getValueAtTime(1)).to.be.closeTo(20, 0.01); }); context("Ticks <-> Time", () => { it("converts from time to ticks", () => { return Offline(() => { const tickSignal = new TickSignal(20); expect(tickSignal.ticksToTime(20, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.ticksToTime(10, 0).valueOf()).to.be.closeTo( 0.5, 0.01 ); expect(tickSignal.ticksToTime(10, 10).valueOf()).to.be.closeTo( 0.5, 0.01 ); tickSignal.dispose(); }); }); it("converts from time to ticks with a linear ramp on the tempo", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.linearRampTo(2, 2, 1); expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo( 0.82, 0.01 ); expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo( 1.82, 0.01 ); expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo( 0.5, 0.01 ); tickSignal.dispose(); }); }); it("converts from time to ticks with a setValueAtTime", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 1); expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo( 0.5, 0.01 ); expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo( 1.5, 0.01 ); expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo( 0.5, 0.01 ); expect(tickSignal.ticksToTime(1, 0.5).valueOf()).to.be.closeTo( 0.75, 0.01 ); tickSignal.dispose(); }); }); it("converts from time to ticks with an exponential ramp", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.exponentialRampTo(2, 1, 1); expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo( 0.75, 0.01 ); expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo( 1.75, 0.01 ); expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo( 0.5, 0.01 ); tickSignal.dispose(); }); }); it("converts from time to ticks with a setTargetAtTime", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.setTargetAtTime(2, 1, 1); expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo( 0.79, 0.01 ); expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo( 1.79, 0.01 ); expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo( 0.61, 0.01 ); tickSignal.dispose(); }); }); it("converts from ticks to time", () => { return Offline(() => { const tickSignal = new TickSignal(20); expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo( 20, 0.01 ); expect(tickSignal.timeToTicks(0.5, 0).valueOf()).to.be.closeTo( 10, 0.01 ); expect(tickSignal.timeToTicks(0.5, 2).valueOf()).to.be.closeTo( 10, 0.01 ); tickSignal.dispose(); }); }); it("converts from ticks to time with a setValueAtTime", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.setValueAtTime(2, 1); expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo( 2, 0.01 ); expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo( 2, 0.01 ); expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo( 1.5, 0.01 ); tickSignal.dispose(); }); }); it("converts from ticks to time with a linear ramp", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.linearRampTo(2, 1, 1); expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo( 1.5, 0.01 ); expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo( 2, 0.01 ); expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo( 1.12, 0.01 ); tickSignal.dispose(); }); }); it("converts from ticks to time with an exponential ramp", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.exponentialRampTo(2, 1, 1); expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo( 1.44, 0.01 ); expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo( 2, 0.01 ); expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo( 1.09, 0.01 ); tickSignal.dispose(); }); }); it("converts from ticks to time with a setTargetAtTime", () => { return Offline(() => { const tickSignal = new TickSignal(1); tickSignal.setTargetAtTime(2, 1, 1); expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo( 1, 0.01 ); expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo( 1.31, 0.01 ); expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo( 1.63, 0.01 ); expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo( 1.07, 0.01 ); tickSignal.dispose(); }); }); }); }); ================================================ FILE: Tone/core/clock/TickSignal.ts ================================================ import { Signal, SignalOptions } from "../../signal/Signal.js"; import { InputNode } from "../context/ToneAudioNode.js"; import { Seconds, Ticks, Time, UnitMap, UnitName } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { TickParam } from "./TickParam.js"; interface TickSignalOptions extends SignalOptions { value: UnitMap[TypeName]; multiplier: number; } /** * TickSignal extends Tone.Signal, but adds the capability * to calculate the number of elapsed ticks. exponential and target curves * are approximated with multiple linear ramps. * * Thank you Bruno Dias, H. Sofia Pinto, and David M. Matos, * for your [WAC paper](https://smartech.gatech.edu/bitstream/handle/1853/54588/WAC2016-49.pdf) * describing integrating timing functions for tempo calculations. */ export class TickSignal< TypeName extends "hertz" | "bpm", > extends Signal { readonly name: string = "TickSignal"; /** * The param which controls the output signal value */ protected _param: TickParam; readonly input: InputNode; /** * @param value The initial value of the signal */ constructor(value?: UnitMap[TypeName]); constructor(options: Partial>); constructor() { const options = optionsFromArguments( TickSignal.getDefaults(), arguments, ["value"] ); super(options); this.input = this._param = new TickParam({ context: this.context, convert: options.convert, multiplier: options.multiplier, param: this._constantSource.offset, units: options.units, value: options.value, }); } static getDefaults(): TickSignalOptions { return Object.assign(Signal.getDefaults(), { multiplier: 1, units: "hertz", value: 1, }); } ticksToTime(ticks: Ticks, when: Time): Seconds { return this._param.ticksToTime(ticks, when); } timeToTicks(duration: Time, when: Time): Ticks { return this._param.timeToTicks(duration, when); } getTimeOfTick(tick: Ticks): Seconds { return this._param.getTimeOfTick(tick); } getDurationOfTicks(ticks: Ticks, time: Time): Seconds { return this._param.getDurationOfTicks(ticks, time); } getTicksAtTime(time: Time): Ticks { return this._param.getTicksAtTime(time); } /** * A multiplier on the bpm value. Useful for setting a PPQ relative to the base frequency value. */ get multiplier(): number { return this._param.multiplier; } set multiplier(m: number) { this._param.multiplier = m; } dispose(): this { super.dispose(); this._param.dispose(); return this; } } ================================================ FILE: Tone/core/clock/TickSource.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { TickSource } from "./TickSource.js"; describe("TickSource", () => { BasicTests(TickSource); context("Constructor", () => { it("can pass in the frequency", () => { const source = new TickSource(2); expect(source.frequency.value).to.equal(2); source.dispose(); }); it("initially returns stop", () => { const source = new TickSource(2); expect(source.state).to.equal("stopped"); source.dispose(); }); }); context("Ticks", () => { it("ticks are 0 before started", () => { const source = new TickSource(); expect(source.ticks).to.equal(0); source.dispose(); }); it("can set ticks", () => { const source = new TickSource(); expect(source.ticks).to.equal(0); source.ticks = 10; expect(source.ticks).to.equal(10); source.dispose(); }); it("ticks increment at the rate of the frequency after started", () => { return Offline(() => { const source = new TickSource(); source.start(0); return (time) => { expect(source.ticks).to.be.closeTo(time, 0.1); }; }, 0.5); }); it("ticks return to 0 after stopped", () => { return Offline(() => { const source = new TickSource(2); source.start(0).stop(0.4); return (time) => { if (time < 0.399) { expect(source.ticks).to.be.closeTo(2 * time, 0.01); } else if (time > 0.4) { expect(source.ticks).to.be.equal(0); } }; }, 0.5); }); it("returns the paused ticks when paused", () => { return Offline(() => { const source = new TickSource(2); source.start(0).pause(0.4); let pausedTicks = -1; return (time) => { if (time < 0.4) { pausedTicks = source.ticks; expect(source.ticks).to.be.closeTo(2 * time, 0.01); } else { expect(source.ticks).to.be.closeTo(pausedTicks, 0.01); } }; }, 0.5); }); it("ticks restart at 0 when started after stop", () => { const source = new TickSource(3); source.start(0).stop(1).start(2); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(1.5, 0.01); 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(3, 0.01); source.dispose(); }); it("ticks remain the same after paused", () => { const source = new TickSource(3); source.start(0).pause(1); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(1.5, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(3, 0.01); source.dispose(); }); it("ticks resume where they were paused", () => { const source = new TickSource(2); source.start(0).pause(1).start(2); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(6, 0.01); source.dispose(); }); it("ticks return to 0 after pause then stopped", () => { const source = new TickSource(2); source.start(0).pause(1).start(2).stop(3); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(0, 0.01); source.dispose(); }); it("handles multiple starts/stops", () => { const source = new TickSource(1); source.start(0).stop(0.3).start(0.4).stop(0.5).start(0.6); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.1)).to.be.closeTo(0.1, 0.01); expect(source.getTicksAtTime(0.2)).to.be.closeTo(0.2, 0.01); expect(source.getTicksAtTime(0.3)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.4)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.6)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.7)).to.be.closeTo(0.1, 0.01); expect(source.getTicksAtTime(0.8)).to.be.closeTo(0.2, 0.01); source.dispose(); }); it("can get ticks when started with an offset", () => { const source = new TickSource(1); source.start(0, 2).stop(3).start(5, 1); expect(source.getTicksAtTime(0)).to.be.closeTo(2, 0.01); 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(0, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(6)).to.be.closeTo(2, 0.01); source.dispose(); }); it("can invoke stop multiple times, takes the last invocation", () => { const source = new TickSource(1); source.start(0).stop(3).stop(2).stop(4); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(6)).to.be.closeTo(0, 0.01); source.dispose(); }); it("can set multiple setTicksAtTime", () => { const source = new TickSource(1); source.start(0, 1).pause(3); source.setTicksAtTime(1, 4); source.stop(5).start(6); source.setTicksAtTime(2, 7); expect(source.getTicksAtTime(0)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(3.5)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(6)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(6.5)).to.be.closeTo(0.5, 0.01); expect(source.getTicksAtTime(7)).to.be.closeTo(2, 0.01); source.dispose(); }); it("can pass start offset", () => { const source = new TickSource(2); source.start(0, 2).pause(1).start(2, 1); expect(source.getTicksAtTime(0)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(5, 0.01); source.dispose(); }); it("can set ticks at any point", () => { const source = new TickSource(2); source.start(0, 2).pause(1).start(2); source.setTicksAtTime(10, 1.5); source.setTicksAtTime(2, 3.5); expect(source.getTicksAtTime(0)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(10, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(12, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(5, 0.01); source.dispose(); }); it("get the time of the ticks", () => { const source = new TickSource(2); source.start(0, 2).pause(1).start(2); source.setTicksAtTime(10, 1.5); source.setTicksAtTime(2, 3.5); expect(source.getTimeOfTick(2, 0.9)).to.be.closeTo(0, 0.01); expect(source.getTimeOfTick(4, 0.9)).to.be.closeTo(1, 0.01); expect(source.getTimeOfTick(10, 3)).to.be.closeTo(2, 0.01); expect(source.getTimeOfTick(12, 3)).to.be.closeTo(3, 0.01); expect(source.getTimeOfTick(3, 4)).to.be.closeTo(4, 0.01); expect(source.getTimeOfTick(5, 4)).to.be.closeTo(5, 0.01); source.dispose(); }); it("can cancel scheduled events", () => { const source = new TickSource(1); source.start(0).stop(3); source.setTicksAtTime(10, 2); source.cancel(1); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(1)).to.be.closeTo(1, 0.01); expect(source.getTicksAtTime(2)).to.be.closeTo(2, 0.01); expect(source.getTicksAtTime(3)).to.be.closeTo(3, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(4, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(5, 0.01); 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", () => { it("invokes a callback function when started", () => { const source = new TickSource(1); source.start(0); let wasCalled = false; source.forEachTickBetween(0, 2, () => { wasCalled = true; }); expect(wasCalled).to.equal(true); source.dispose(); }); it("does not invoke callback when not overlapping with tick", () => { const source = new TickSource(1); source.start(0); let wasCalled = false; source.forEachTickBetween(1.1, 2, () => { wasCalled = true; }); expect(wasCalled).to.equal(false); source.dispose(); }); it("iterates only at times when the state is 'started'", () => { const source = new TickSource(4); source.start(0.2).pause(2).start(3.5).stop(5); let iterations = 0; const expectedTimes = [ 1.2, 1.45, 1.7, 1.95, 3.5, 3.75, 4, 4.25, 4.5, 4.75, ]; const expectedTicks = [4, 5, 6, 7, 7, 8, 9, 10, 11, 12]; source.forEachTickBetween(1, 7, (time, ticks) => { expect(time).to.be.closeTo(expectedTimes[iterations], 0.001); expect(ticks).to.equal(expectedTicks[iterations]); iterations++; }); expect(iterations).to.equal(expectedTimes.length); source.dispose(); }); it("can start at time = 0", () => { const source = new TickSource(1); source.start(0); let iterations = 0; source.forEachTickBetween(0, 0.1, () => { iterations++; }); expect(iterations).to.equal(1); source.dispose(); }); it("can throw an error in the callback but still invokes all loops", () => { const source = new TickSource(1); source.start(0); expect(() => { source.forEachTickBetween(0, 3, () => { throw new Error("should throw"); }); }).throws(Error); source.dispose(); }); it("iterates once per tick", () => { const source = new TickSource(1); source.start(0.5); let iterations = 0; source.forEachTickBetween(0, 2, () => { iterations++; }); expect(iterations).to.equal(2); source.dispose(); }); it("passes time and tick into the callback", () => { const source = new TickSource(2); source.start(0.5); let iterations = 0; const times = [0.5, 1.0, 1.5]; source.forEachTickBetween(0, 2, (time, ticks) => { expect(times[ticks]).to.be.closeTo(time, 0.001); iterations++; }); expect(iterations).to.equal(3); source.dispose(); }); it("ticks = 0 when restarted", () => { const source = new TickSource(1); source.start(0.5).stop(1).start(2); let iterations = 0; const expectedTicks = [0, 0, 1]; const expectedTimes = [0.5, 2, 3]; source.forEachTickBetween(0, 3.1, (time, ticks) => { expect(time).to.be.closeTo(expectedTimes[iterations], 0.001); expect(ticks).to.equal(expectedTicks[iterations]); iterations++; }); expect(iterations).to.equal(expectedTicks.length); source.dispose(); }); it("ticks resume after pause when restarted", () => { const source = new TickSource(1); source.start(0.5).pause(2).start(3); let iterations = 0; const expectedTicks = [0, 1, 2]; const expectedTimes = [0.5, 1.5, 3]; source.forEachTickBetween(0, 3.1, (time, ticks) => { expect(time).to.be.closeTo(expectedTimes[iterations], 0.001); expect(ticks).to.equal(expectedTicks[iterations]); iterations++; }); expect(iterations).to.equal(expectedTicks.length); source.dispose(); }); it("handles start and stop", () => { const source = new TickSource(2); source.start(0.5).stop(2); let iterations = 0; const times = [0.5, 1.0, 1.5]; source.forEachTickBetween(0, 3, (time, ticks) => { expect(times[ticks]).to.be.closeTo(time, 0.001); iterations++; }); expect(iterations).to.equal(3); source.dispose(); }); it("handles multiple start and stop", () => { const source = new TickSource(2); source.start(0.5).stop(2).start(2.5).stop(4.1); let iterations = 0; const times = [0.5, 1.0, 1.5, 2.5, 3, 3.5, 4]; source.forEachTickBetween(0, 10, (time) => { expect(times[iterations]).to.be.closeTo(time, 0.001); iterations++; }); expect(iterations).to.equal(times.length); source.dispose(); }); it("works with a frequency ramp", () => { const source = new TickSource(1); source.frequency.setValueAtTime(1, 0); source.frequency.linearRampToValueAtTime(4, 1); source.start(0.5); let iterations = 0; const times = [0.5, 0.833, 1.094, 1.344, 1.594, 1.844]; source.forEachTickBetween(0, 2, (time, ticks) => { expect(time).to.be.closeTo(times[ticks], 0.001); iterations++; }); expect(iterations).to.equal(times.length); source.dispose(); }); it("can start with a tick offset", () => { const source = new TickSource(10); source.start(0.5, 5); let iterations = 0; source.forEachTickBetween(0, 2, (time, ticks) => { expect(ticks).to.be.gte(5); iterations++; }); expect(iterations).to.equal(15); source.dispose(); }); it("can handle multiple starts with tick offsets", () => { const source = new TickSource(1); source.start(0.5, 10).stop(2).start(3, 1); let iterations = 0; const expectedTimes = [0.5, 1.5, 3]; const expectedTicks = [10, 11, 1]; source.forEachTickBetween(0, 4, (time, ticks) => { expect(time).to.be.closeTo(expectedTimes[iterations], 0.001); expect(ticks).to.equal(expectedTicks[iterations]); iterations++; }); expect(iterations).to.equal(expectedTicks.length); source.dispose(); }); it("can set ticks after start", () => { const source = new TickSource(1); source.start(0.4, 3); source.setTicksAtTime(1, 1.4); source.setTicksAtTime(10, 3); let iterations = 0; const expectedTicks = [3, 1, 2, 10]; source.forEachTickBetween(0, 4, (time, ticks) => { expect(ticks).to.equal(expectedTicks[iterations]); iterations++; }); expect(iterations).to.equal(expectedTicks.length); source.dispose(); }); it("can pass in the frequency", () => { const source = new TickSource(20); source.start(0.5); let iterations = 0; let lastTime = 0.5; source.forEachTickBetween(0.51, 2.01, (time) => { expect(time - lastTime).to.be.closeTo(0.05, 0.001); lastTime = time; iterations++; }); expect(iterations).to.equal(30); source.dispose(); }); it("can iterate from later in the timeline", () => { const source = new TickSource(1); source.start(0.2); let iterations = 0; source.forEachTickBetween(100, 101, (time, ticks) => { expect(ticks).to.equal(100); expect(time).to.be.closeTo(100.2, 0.001); iterations++; }); expect(iterations).to.equal(1); source.dispose(); }); it("always increments by 1 at a fixed rate", () => { const source = new TickSource(960); source.start(0); let previousTick = -1; let previousTime = -1; source.forEachTickBetween(1000, 1010, (time, ticks) => { expect(time).to.be.gt(previousTime); if (previousTick !== -1) { expect(ticks - previousTick).to.equal(1); } previousTick = ticks; previousTime = time; }); source.dispose(); }); it("always increments by 1 when linearly changing rate", () => { const source = new TickSource(200); source.frequency.setValueAtTime(200, 0); source.frequency.linearRampToValueAtTime(1000, 100); source.start(10); let previousTick = -1; let previousTime = -1; source.forEachTickBetween(10, 30, (time, ticks) => { expect(time).to.be.gt(previousTime); expect(ticks - previousTick).to.equal(1); previousTick = ticks; previousTime = time; }); source.dispose(); }); it("always increments by 1 when setting values", () => { const source = new TickSource(200); source.frequency.setValueAtTime(300, 0); source.frequency.setValueAtTime(3, 0.1); source.frequency.setValueAtTime(100, 0.2); source.frequency.setValueAtTime(10, 0.3); source.frequency.setValueAtTime(1000, 0.4); source.frequency.setValueAtTime(1, 0.5); source.frequency.setValueAtTime(50, 0.6); source.start(0); let previousTick = -1; let previousTime = -1; source.forEachTickBetween(0, 10, (time, ticks) => { expect(time).to.be.gt(previousTime); expect(ticks - previousTick).to.equal(1); previousTick = ticks; previousTime = time; }); source.dispose(); }); }); context("Seconds", () => { it("get the elapsed time in seconds", () => { return Offline(() => { const source = new TickSource(1).start(0); return (time) => { expect(source.seconds).to.be.closeTo(time, 0.01); }; }, 2); }); it("seconds is 0 before starting", () => { const source = new TickSource(1); expect(source.seconds).to.be.closeTo(0, 0.001); source.dispose(); }); it("can set the seconds", () => { const source = new TickSource(1); expect(source.seconds).to.be.closeTo(0, 0.001); source.dispose(); }); it("seconds pauses at last second count", () => { const source = new TickSource(1); source.start(0).pause(1); expect(source.getSecondsAtTime(0)).to.be.closeTo(0, 0.001); expect(source.getSecondsAtTime(1)).to.be.closeTo(1, 0.001); expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.001); source.dispose(); }); it("can handle multiple pauses", () => { const source = new TickSource(1); source.start(0).pause(1).start(2).pause(3).start(4).stop(6); expect(source.getSecondsAtTime(0)).to.be.closeTo(0, 0.001); expect(source.getSecondsAtTime(1)).to.be.closeTo(1, 0.001); expect(source.getSecondsAtTime(2)).to.be.closeTo(1, 0.001); expect(source.getSecondsAtTime(2.5)).to.be.closeTo(1.5, 0.001); expect(source.getSecondsAtTime(3)).to.be.closeTo(2, 0.001); expect(source.getSecondsAtTime(4.5)).to.be.closeTo(2.5, 0.001); expect(source.getSecondsAtTime(5)).to.be.closeTo(3, 0.001); expect(source.getSecondsAtTime(6)).to.be.closeTo(0, 0.001); source.dispose(); }); it("get the elapsed time in seconds when starting in the future", () => { return Offline(() => { const source = new TickSource(1).start(0.1); return (time) => { if (time < 0.1) { expect(source.seconds).to.be.closeTo(0, 0.001); } else { expect(source.seconds).to.be.closeTo(time - 0.1, 0.01); } }; }, 2); }); it("handles multiple starts and stops", () => { const source = new TickSource(1) .start(0) .stop(0.5) .start(1) .stop(1.5); expect(source.getSecondsAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getSecondsAtTime(0.4)).to.be.closeTo(0.4, 0.01); expect(source.getSecondsAtTime(0.5)).to.be.closeTo(0, 0.01); expect(source.getSecondsAtTime(0.9)).to.be.closeTo(0, 0.01); expect(source.getSecondsAtTime(1)).to.be.closeTo(0, 0.01); expect(source.getSecondsAtTime(1.4)).to.be.closeTo(0.4, 0.01); 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", () => { it("can automate frequency with setValueAtTime", () => { const source = new TickSource(1); source.start(0).stop(0.3).start(0.4).stop(0.5).start(0.6); source.frequency.setValueAtTime(2, 0.3); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.1)).to.be.closeTo(0.1, 0.01); expect(source.getTicksAtTime(0.2)).to.be.closeTo(0.2, 0.01); expect(source.getTicksAtTime(0.3)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.4)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.45)).to.be.closeTo(0.1, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.6)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.7)).to.be.closeTo(0.2, 0.01); expect(source.getTicksAtTime(0.8)).to.be.closeTo(0.4, 0.01); source.dispose(); }); it("can automate frequency with linearRampToValueAtTime", () => { const source = new TickSource(1); source.start(0).stop(1).start(2); source.frequency.linearRampToValueAtTime(2, 2); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(0.56, 0.01); 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(2, 0.01); source.dispose(); }); it("can automate frequency with exponentialRampToValueAtTime", () => { const source = new TickSource(1); source.start(0).stop(1).start(2).stop(5); source.frequency.exponentialRampToValueAtTime(4, 2); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(0.6, 0.01); 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(4, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(8, 0.01); source.dispose(); }); it("can automate frequency with setTargetAtTime", () => { const source = new TickSource(1); source.start(0).stop(1).start(2).stop(5); source.frequency.setTargetAtTime(2, 1, 0.5); expect(source.getTicksAtTime(0)).to.be.closeTo(0, 0.01); expect(source.getTicksAtTime(0.5)).to.be.closeTo(0.5, 0.01); 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(1.86, 0.01); expect(source.getTicksAtTime(4)).to.be.closeTo(3.73, 0.01); expect(source.getTicksAtTime(5)).to.be.closeTo(0, 0.01); source.dispose(); }); }); }); ================================================ FILE: Tone/core/clock/TickSource.ts ================================================ import { ToneWithContext, ToneWithContextOptions, } from "../context/ToneWithContext.js"; import { Seconds, Ticks, Time } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { readOnly } from "../util/Interface.js"; import { EQ } from "../util/Math.js"; import { PlaybackState, StateTimeline, StateTimelineEvent, } from "../util/StateTimeline.js"; import { Timeline, TimelineEvent } from "../util/Timeline.js"; import { isDefined } from "../util/TypeCheck.js"; import { TickSignal } from "./TickSignal.js"; interface TickSourceOptions extends ToneWithContextOptions { frequency: number; units: "bpm" | "hertz"; } 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. */ export class TickSource< TypeName extends "bpm" | "hertz", > extends ToneWithContext { readonly name: string = "TickSource"; /** * The frequency the callback function should be invoked. */ readonly frequency: TickSignal; /** * The state timeline */ private _state: StateTimeline = new StateTimeline(); /** * The offset values of the ticks */ 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 */ constructor(frequency?: number); constructor(options?: Partial); constructor() { const options = optionsFromArguments( TickSource.getDefaults(), arguments, ["frequency"] ); super(options); this.frequency = new TickSignal({ context: this.context, units: options.units as TypeName, value: options.frequency, }); readOnly(this, "frequency"); // set the initial state this._state.setStateAtTime("stopped", 0); // add the first event this.setTicksAtTime(0, 0); } static getDefaults(): TickSourceOptions { return Object.assign( { frequency: 1, units: "hertz" as const, }, ToneWithContext.getDefaults() ); } /** * Returns the playback state of the source, either "started", "stopped" or "paused". */ get state(): PlaybackState { return this.getStateAtTime(this.now()); } /** * Start the clock at the given time. Optionally pass in an offset * of where to start the tick counter from. * @param time The time the clock should start * @param offset The number of ticks to start the source at */ start(time: Time, offset?: Ticks): this { const computedTime = this.toSeconds(time); if (this._state.getValueAtTime(computedTime) !== "started") { this._state.setStateAtTime("started", computedTime); if (isDefined(offset)) { this.setTicksAtTime(offset, computedTime); } this._ticksAtTime.cancel(computedTime); this._secondsAtTime.cancel(computedTime); } return this; } /** * Stop the clock. Stopping the clock resets the tick counter to 0. * @param time The time when the clock should stop. */ stop(time: Time): this { const computedTime = this.toSeconds(time); // cancel the previous stop if (this._state.getValueAtTime(computedTime) === "stopped") { const event = this._state.get(computedTime); if (event && event.time > 0) { this._tickOffset.cancel(event.time); this._state.cancel(event.time); } } this._state.cancel(computedTime); this._state.setStateAtTime("stopped", computedTime); this.setTicksAtTime(0, computedTime); this._ticksAtTime.cancel(computedTime); this._secondsAtTime.cancel(computedTime); return this; } /** * Pause the clock. Pausing does not reset the tick counter. * @param time The time when the clock should stop. */ pause(time: Time): this { 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; } /** * Cancel start/stop/pause and setTickAtTime events scheduled after the given time. * @param time When to clear the events after */ cancel(time: Time): this { time = this.toSeconds(time); this._state.cancel(time); this._tickOffset.cancel(time); this._ticksAtTime.cancel(time); this._secondsAtTime.cancel(time); return this; } /** * Get the elapsed ticks at the given time * @param time When to get the tick value * @return The number of ticks */ 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 = 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( 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); if (offsetEvent && offsetEvent.time >= lastState.time) { elapsedTicks = offsetEvent.ticks; periodStartTime = offsetEvent.time; } 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; } ); // 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; } /** * The number of times the callback was invoked. Starts counting at 0 * and increments after the callback was invoked. Returns -1 when stopped. */ get ticks(): Ticks { return this.getTicksAtTime(this.now()); } set ticks(t: Ticks) { this.setTicksAtTime(t, this.now()); } /** * The time since ticks=0 that the TickSource has been running. Accounts * for tempo curves */ get seconds(): Seconds { return this.getSecondsAtTime(this.now()); } set seconds(s: Seconds) { const now = this.now(); const ticks = this.frequency.timeToTicks(s, now); this.setTicksAtTime(ticks, now); } /** * Return the elapsed seconds at the given time. * @param time When to get the elapsed seconds * @return The number of elapsed seconds */ getSecondsAtTime(time: Time): Seconds { time = this.toSeconds(time); const stopEvent = this._state.getLastState( "stopped", time ) as StateTimelineEvent; // this event allows forEachBetween to iterate until the current time 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 = 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( 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); if (offsetEvent && offsetEvent.time >= lastState.time) { elapsedSeconds = offsetEvent.seconds; periodStartTime = offsetEvent.time; } 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; } ); // remove the temporary event this._state.remove(tmpEvent); // memoize the seconds at the most recent event with state other than "started" if (eventToMemoize) { this._secondsAtTime.add(eventToMemoize); } // return the seconds return elapsedSeconds; } /** * Set the clock's ticks at the given time. * @param ticks The tick value to set * @param time When to set the tick value */ setTicksAtTime(ticks: Ticks, time: Time): this { time = this.toSeconds(time); this._tickOffset.cancel(time); this._tickOffset.add({ seconds: this.frequency.getDurationOfTicks(ticks, time), ticks, time, }); this._ticksAtTime.cancel(time); this._secondsAtTime.cancel(time); return this; } /** * Returns the scheduled state at the given time. * @param time The time to query. */ getStateAtTime(time: Time): PlaybackState { time = this.toSeconds(time); return this._state.getValueAtTime(time); } /** * Get the time of the given tick. The second argument * is when to test before. Since ticks can be set (with setTicksAtTime) * there may be multiple times for a given tick value. * @param tick The tick number. * @param before When to measure the tick value from. * @return The time of the tick */ getTimeOfTick(tick: Ticks, before = this.now()): Seconds { const offset = this._tickOffset.get(before) as TickSourceOffsetEvent; const event = this._state.get(before) as StateTimelineEvent; const startTime = Math.max(offset.time, event.time); const absoluteTicks = this.frequency.getTicksAtTime(startTime) + tick - offset.ticks; return this.frequency.getTimeOfTick(absoluteTicks); } /** * Invoke the callback event at all scheduled ticks between the * start time and the end time * @param startTime The beginning of the search range * @param endTime The end of the search range * @param callback The callback to invoke with each tick */ forEachTickBetween( startTime: number, endTime: number, callback: (when: Seconds, ticks: Ticks) => void ): this { // only iterate through the sections where it is "started" let lastStateEvent = this._state.get(startTime); this._state.forEachBetween(startTime, endTime, (event) => { if ( lastStateEvent && lastStateEvent.state === "started" && event.state !== "started" ) { this.forEachTickBetween( Math.max(lastStateEvent.time, startTime), event.time - this.sampleTime, callback ); } lastStateEvent = event; }); let error: Error | null = null; if (lastStateEvent && lastStateEvent.state === "started") { const maxStartTime = Math.max(lastStateEvent.time, startTime); // figure out the difference between the frequency ticks and the const startTicks = this.frequency.getTicksAtTime(maxStartTime); const ticksAtStart = this.frequency.getTicksAtTime( lastStateEvent.time ); const diff = startTicks - ticksAtStart; let offset = Math.ceil(diff) - diff; // guard against floating point issues offset = EQ(offset, 1) ? 0 : offset; let nextTickTime = this.frequency.getTimeOfTick( startTicks + offset ); while (nextTickTime < endTime) { try { callback( nextTickTime, Math.round(this.getTicksAtTime(nextTickTime)) ); } catch (e) { error = e; break; } nextTickTime += this.frequency.getDurationOfTicks( 1, nextTickTime ); } } if (error) { throw error; } return this; } /** * Clean up */ dispose(): this { super.dispose(); this._state.dispose(); this._tickOffset.dispose(); this._ticksAtTime.dispose(); this._secondsAtTime.dispose(); this.frequency.dispose(); return this; } } ================================================ FILE: Tone/core/clock/Ticker.test.ts ================================================ import { expect } from "chai"; import { Ticker } from "./Ticker.js"; describe("Ticker", () => { function empty(): void { // do nothing } it("can be created and disposed", () => { const ticker = new Ticker(empty, "offline", 1); ticker.dispose(); }); it("can adjust the type", () => { const ticker = new Ticker(empty, "worker", 0.1); expect(ticker.type).to.equal("worker"); ticker.type = "timeout"; expect(ticker.type).to.equal("timeout"); ticker.type = "offline"; expect(ticker.type).to.equal("offline"); ticker.dispose(); }); it("can get/set the updateInterval", () => { const ticker = new Ticker(empty, "worker", 0.1); expect(ticker.updateInterval).to.equal(0.1); ticker.updateInterval = 0.5; expect(ticker.updateInterval).to.equal(0.5); ticker.dispose(); }); context("timeout", () => { it("provides a callback when set to timeout", (done) => { const ticker = new Ticker( () => { ticker.dispose(); done(); }, "timeout", 0.01 ); }); it("can adjust the interval when set to timeout", (done) => { const ticker = new Ticker( () => { ticker.dispose(); done(); }, "timeout", 0.01 ); ticker.updateInterval = 0.1; }); }); context("worker", () => { it("provides a callback when set to worker", (done) => { const ticker = new Ticker( () => { ticker.dispose(); done(); }, "worker", 0.01 ); }); it("falls back to timeout if the constructor throws an error", (done) => { const URL = window.URL; // @ts-ignore window.URL = null; const ticker = new Ticker( () => { expect(ticker.type).to.equal("timeout"); ticker.dispose(); window.URL = URL; done(); }, "worker", 0.01 ); }); it("can adjust the interval when set to worker", (done) => { const ticker = new Ticker( () => { ticker.dispose(); done(); }, "worker", 0.01 ); ticker.updateInterval = 0.1; }); }); }); ================================================ FILE: Tone/core/clock/Ticker.ts ================================================ import { Seconds } from "../type/Units.js"; export type TickerClockSource = "worker" | "timeout" | "offline"; /** * A class which provides a reliable callback using either * a Web Worker, or if that isn't supported, falls back to setTimeout. */ export class Ticker { /** * Either "worker" or "timeout" or "offline" */ private _type: TickerClockSource; /** * The update interval of the worker */ private _updateInterval!: Seconds; /** * The lowest allowable interval, preferably calculated from context sampleRate */ private _minimumUpdateInterval: Seconds; /** * The callback to invoke at regular intervals */ private _callback: () => void; /** * track the callback interval */ private _timeout!: ReturnType; /** * private reference to the worker */ private _worker!: Worker; constructor( callback: () => void, type: TickerClockSource, updateInterval: Seconds, contextSampleRate?: number ) { this._callback = callback; this._type = type; this._minimumUpdateInterval = Math.max( 128 / (contextSampleRate || 44100), 0.001 ); this.updateInterval = updateInterval; // create the clock source for the first time this._createClock(); } /** * Generate a web worker */ private _createWorker(): void { const blob = new Blob( [ /* javascript */ ` // the initial timeout time let timeoutTime = ${(this._updateInterval * 1000).toFixed(1)}; // onmessage callback self.onmessage = function(msg){ timeoutTime = parseInt(msg.data); }; // the tick function which posts a message // and schedules a new tick function tick(){ setTimeout(tick, timeoutTime); self.postMessage('tick'); } // call tick initially tick(); `, ], { type: "text/javascript" } ); const blobUrl = URL.createObjectURL(blob); const worker = new Worker(blobUrl); worker.onmessage = this._callback.bind(this); this._worker = worker; } /** * Create a timeout loop */ private _createTimeout(): void { this._timeout = setTimeout(() => { this._createTimeout(); this._callback(); }, this._updateInterval * 1000); } /** * Create the clock source. */ private _createClock(): void { if (this._type === "worker") { try { this._createWorker(); // eslint-disable-next-line unused-imports/no-unused-vars } catch (e) { // workers not supported, fallback to timeout this._type = "timeout"; this._createClock(); } } else if (this._type === "timeout") { this._createTimeout(); } } /** * Clean up the current clock source */ private _disposeClock(): void { if (this._timeout) { clearTimeout(this._timeout); } if (this._worker) { this._worker.terminate(); this._worker.onmessage = null; } } /** * The rate in seconds the ticker will update */ get updateInterval(): Seconds { return this._updateInterval; } set updateInterval(interval: Seconds) { this._updateInterval = Math.max(interval, this._minimumUpdateInterval); if (this._type === "worker") { this._worker?.postMessage(this._updateInterval * 1000); } } /** * The type of the ticker, either a worker or a timeout */ get type(): TickerClockSource { return this._type; } set type(type: TickerClockSource) { this._disposeClock(); this._type = type; this._createClock(); } /** * Clean up */ dispose(): void { this._disposeClock(); } } ================================================ FILE: Tone/core/clock/Transport.test.ts ================================================ // importing for side affects import "../context/Destination.js"; import { expect } from "chai"; import { warns } from "../../../test/helper/Basic.js"; import { atTime, Offline, whenBetween } from "../../../test/helper/Offline.js"; import { Synth } from "../../instrument/Synth.js"; import { Signal } from "../../signal/Signal.js"; import { Time } from "../type/Time.js"; import { TransportTime } from "../type/TransportTime.js"; import { noOp } from "../util/Interface.js"; import { TransportInstance } from "./Transport.js"; describe("Transport", () => { context("BPM and timeSignature", () => { it("can get and set bpm", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.bpm.value = 125; expect(transport.bpm.value).to.be.closeTo(125, 0.001); transport.bpm.value = 120; expect(transport.bpm.value).to.equal(120); }); }); it("can get and set timeSignature as both an array or number", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.timeSignature = [6, 8]; expect(transport.timeSignature).to.equal(3); transport.timeSignature = 5; expect(transport.timeSignature).to.equal(5); }); }); it("can get and set timeSignature as both an array or number", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.timeSignature = [6, 8]; expect(transport.timeSignature).to.equal(3); transport.timeSignature = 5; expect(transport.timeSignature).to.equal(5); }); }); }); context("looping", () => { it("can get and set loop points", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.loopStart = 0.2; transport.loopEnd = 0.4; expect(transport.loopStart).to.be.closeTo(0.2, 0.01); expect(transport.loopEnd).to.be.closeTo(0.4, 0.01); transport.setLoopPoints(0, "1m"); expect(transport.loopStart).to.be.closeTo(0, 0.01); expect(transport.loopEnd).to.be.closeTo( transport.toSeconds("1m"), 0.01 ); }); }); it("can loop events scheduled on the transport", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.schedule((time) => { invocations++; }, 0); transport.setLoopPoints(0, 0.1).start(0); transport.loop = true; }, 0.41); expect(invocations).to.equal(5); }); it("jumps to the loopStart after the loopEnd point", async () => { let looped = false; await Offline((context) => { const transport = new TransportInstance({ context }); transport.on("loop", () => { looped = true; }); transport.loop = true; transport.loopEnd = 1; transport.seconds = 2; transport.start(); }, 0.4); expect(looped).to.equal(true); }); }); context("nextSubdivision", () => { it("returns 0 if the transports not started", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.nextSubdivision()).to.equal(0); }); }); it("can get the next subdivision of the transport", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0); return (time) => { whenBetween(time, 0.05, 0.07, () => { expect(transport.nextSubdivision(0.5)).to.be.closeTo( 0.5, 0.01 ); expect(transport.nextSubdivision(0.04)).to.be.closeTo( 0.08, 0.01 ); expect(transport.nextSubdivision(2)).to.be.closeTo( 2, 0.01 ); }); whenBetween(time, 0.09, 0.1, () => { expect(transport.nextSubdivision(0.04)).to.be.closeTo( 0.12, 0.01 ); expect(transport.nextSubdivision("8n")).to.be.closeTo( 0.25, 0.01 ); }); }; }, 0.1); }); }); context("PPQ", () => { it("can get and set pulses per quarter", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.PPQ = 96; expect(transport.PPQ).to.equal(96); }); }); it("schedules a quarter note at the same time with a different PPQ", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.PPQ = 1; const id = transport.schedule((time) => { expect(time).to.be.closeTo(transport.toSeconds("4n"), 0.1); transport.clear(id); }, "4n"); transport.start(); }); }); it("invokes the right number of ticks with a different PPQ", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.bpm.value = 120; const ppq = 20; transport.PPQ = ppq; transport.start(); return (time) => { if (time > 0.5) { expect(transport.ticks).to.be.within(ppq, ppq * 1.2); } }; }, 0.55); }); }); context("position", () => { it("can jump to a specific tick number", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.ticks = 200; expect(transport.ticks).to.equal(200); transport.start(0); let tested = false; return () => { if (!tested) { expect(transport.ticks).to.at.least(200); tested = true; } }; }, 0.1); }); it("can get the current position in BarsBeatsSixteenths", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.position).to.equal("0:0:0"); transport.start(0); return atTime(0.05, () => { expect(transport.position).to.not.equal("0:0:0"); }); }, 0.1); }); it("can get the current position in seconds", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.seconds).to.equal(0); transport.start(0.05); return (time) => { if (time > 0.05) { expect(transport.seconds).to.be.closeTo( time - 0.05, 0.01 ); } }; }, 0.1); }); it("can get the current position in seconds during a bpm ramp", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.seconds).to.equal(0); transport.start(0.05); transport.bpm.linearRampTo(60, 0.5, 0.5); return (time) => { if (time > 0.05) { expect(transport.seconds).to.be.closeTo( time - 0.05, 0.01 ); } }; }, 0.7); }); it("can set the current position in seconds", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.seconds).to.equal(0); transport.seconds = 3; expect(transport.seconds).to.be.closeTo(3, 0.01); }); }); it("can schedule the current seconds position", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.start(); let scheduled = false; return (time) => { if (time > 0.5 && !scheduled) { scheduled = true; transport.setSecondsAtTime(3, 0.5); } whenBetween(time, 0, 0.5, () => { expect(transport.seconds).to.be.closeTo(time, 0.01); }); whenBetween(time, 0.5, 1, () => { expect(transport.seconds).to.be.closeTo( 2.5 + time, 0.01 ); }); }; }, 1); }); it("can set the current position in BarsBeatsSixteenths", () => { return Offline((context) => { const transport = new TransportInstance({ context }); expect(transport.position).to.equal("0:0:0"); transport.position = "3:0"; expect(transport.position).to.equal("3:0:0"); transport.position = "0:0"; expect(transport.position).to.equal("0:0:0"); }); }); it("can get the progress of the loop", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.setLoopPoints(0, "1m").start(); transport.loop = true; expect(transport.progress).to.be.equal(0); transport.position = "2n"; expect(transport.progress).to.be.closeTo(0.5, 0.001); transport.position = Time("2n").valueOf() + Time("4n").valueOf(); expect(transport.progress).to.be.closeTo(0.75, 0.001); }); }); it("progress is always 0 when not looping", () => { return Offline(({ transport }) => { transport.loop = false; transport.start(); return atTime(0.1, () => { expect(transport.progress).to.be.equal(0); }); }, 0.2); }); it("invokes the first callback time when the scheduled time is a non-integer tick time", async () => { let wasCalled = false; await Offline(({ transport }) => { // choose a value which is not cleanly representable as ticks const problemValue = Time(100, "i").toSeconds() + 0.01; transport.seconds = problemValue; transport.schedule(() => { wasCalled = true; }, problemValue); transport.start(); }, 0.2); expect(wasCalled).to.be.true; }); it("setting the same ticks value twice does not emit twice", async () => { await Offline(({ transport }) => { let callCount = 0; transport.on("ticks", () => { callCount++; }); transport.ticks = 100; expect(transport.ticks).to.equal(100); expect(callCount).to.equal(1); // set it to the same value again has no change transport.ticks = 100; expect(callCount).to.equal(1); }, 0.1); }); }); context("state", () => { it("can start, pause, and restart", async () => { const buffer = await Offline(({ transport }) => { transport.start(0).pause(0.2).start(0.4); const pulse = new Signal(0).toDestination(); transport.schedule((time) => { pulse.setValueAtTime(1, time); pulse.setValueAtTime(0, time + 0.1); }, 0); transport.schedule((time) => { pulse.setValueAtTime(1, time); pulse.setValueAtTime(0, time + 0.1); }, 0.3); return (time) => { whenBetween(time, 0, 0.2, () => { expect(transport.state).to.equal("started"); }); whenBetween(time, 0.2, 0.4, () => { expect(transport.state).to.equal("paused"); }); whenBetween(time, 0.4, Infinity, () => { expect(transport.state).to.equal("started"); }); }; }, 0.6); buffer.forEach((sample, time) => { whenBetween(time, 0, 0.01, () => { expect(sample).to.equal(1); }); whenBetween(time, 0.1, 0.11, () => { expect(sample).to.equal(0); }); whenBetween(time, 0.502, 0.51, () => { expect(sample).to.equal(1); }); }); }); }); context("ticks", () => { it("resets ticks on stop but not on pause", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0).pause(0.1).stop(0.2); expect(transport.getTicksAtTime(0)).to.be.equal( Math.floor(transport.PPQ * 0) ); expect(transport.getTicksAtTime(0.05)).to.be.closeTo( Math.floor(transport.PPQ * 0.1), 0.5 ); expect(transport.getTicksAtTime(0.1)).to.be.closeTo( Math.floor(transport.PPQ * 0.2), 0.5 ); expect(transport.getTicksAtTime(0.15)).to.be.closeTo( Math.floor(transport.PPQ * 0.2), 0.5 ); expect(transport.getTicksAtTime(0.2)).to.be.equal(0); }, 0.3); }); it("tracks ticks after start", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.bpm.value = 120; const ppq = transport.PPQ; transport.start(); return (time) => { if (time > 0.5) { expect(transport.ticks).to.at.least(ppq); } }; }, 0.6); }); it("can start with a tick offset", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0, "200i"); return (time) => { if (time < 0.01) { expect(transport.ticks).to.at.least(200); } }; }, 0.1); }); it("can toggle the state of the transport", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); transport.toggle(0); transport.toggle(0.2); return (time) => { whenBetween(time, 0, 0.2, () => { expect(transport.state).to.equal("started"); }); whenBetween(time, 0.2, Infinity, () => { expect(transport.state).to.equal("stopped"); }); }; }, 0.1); }); it("tracks ticks correctly with a different PPQ and BPM", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); transport.PPQ = 96; transport.bpm.value = 90; transport.start(); return (time) => { if (time > 0.5) { expect(transport.ticks).to.at.least(72); } }; }, 0.6); }); it("can set the ticks while started", async () => { let invocations = 0; const times = [0, 1.5]; await Offline(({ transport }) => { transport.PPQ = 1; transport.schedule((time) => { expect(time).to.be.closeTo(times[invocations], 0.01); invocations++; }, 0); transport.start(0); return atTime(1.1, () => { transport.ticks = 0; }); }, 2.5); expect(invocations).to.equal(2); }); it("can schedule the ticks", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.start(); let scheduled = false; return (time) => { if (time > 0.5 && !scheduled) { scheduled = true; transport.setTicksAtTime(0, 0.5); } whenBetween(time, 0, 0.5, () => { expect(transport.ticks).to.be.closeTo( transport.toTicks(time), 1 ); }); whenBetween(time, 0.5, 1, () => { expect(transport.ticks).to.be.closeTo( transport.toTicks(time - 0.5), 1 ); }); }; }, 1); }); }); context("schedule", () => { it("can schedule an event on the timeline", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.schedule(() => {}, 0); expect(eventID).to.be.a("number"); }); }); it("scheduled event gets invoked with the time of the event", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; transport.schedule((time) => { expect(time).to.be.closeTo(startTime, 0.01); wasCalled = true; }, 0); transport.start(startTime); }, 0.2); expect(wasCalled).to.equal(true); }); it("can schedule events with TransportTime", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eighth = transport.toSeconds("8n"); transport.schedule((time) => { expect(time).to.be.closeTo(startTime + eighth, 0.01); wasCalled = true; }, TransportTime("8n")); transport.start(startTime); }, 0.5); expect(wasCalled).to.be.true; }); it("can clear a scheduled event", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.schedule(() => { throw new Error("should not call this function"); }, 0); transport.clear(eventID); transport.start(); }); }); it("can cancel the timeline of scheduled object", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.schedule(() => { throw new Error("should not call this"); }, 0); transport.cancel(0); transport.start(0); }); }); it("can cancel the timeline of scheduleOnce object", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.scheduleOnce(() => { throw new Error("should not call this"); }, 0); transport.cancel(0); transport.start(0); }); }); it("scheduled event anywhere along the timeline", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = transport.now(); transport.schedule((time) => { expect(time).to.be.closeTo(startTime + 0.5, 0.001); wasCalled = true; }, 0.5); transport.start(startTime); }, 0.6); expect(wasCalled).to.equal(true); }); it("can schedule multiple events and invoke them in the right order", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); let first = false; transport.schedule(() => { first = true; }, 0.1); transport.schedule(() => { expect(first).to.equal(true); wasCalled = true; }, 0.11); transport.start(); }, 0.2); expect(wasCalled).to.equal(true); }); it("invokes the event again if the timeline is restarted", async () => { let iterations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.schedule(() => { iterations++; }, 0.05); transport.start(0).stop(0.1).start(0.2); }, 0.3); expect(iterations).to.be.equal(2); }); it("can add an event after the Transport is started", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0); let wasScheduled = false; return (time) => { if (time > 0.1 && !wasScheduled) { wasScheduled = true; transport.schedule(() => { wasCalled = true; }, 0.15); } }; }, 0.3); expect(wasCalled).to.equal(true); }); it("warns if the scheduled time was not used in the callback", async () => { await Offline(({ transport }) => { const synth = new Synth(); transport.schedule(() => { warns(() => { synth.triggerAttackRelease("C2", 0.1); }); }, 0); transport.start(0); }, 0.3); }); }); context("scheduleRepeat", () => { it("can schedule a repeated event", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.scheduleRepeat(noOp, 1); expect(eventID).to.be.a("number"); }); }); it("scheduled event gets invoked with the time of the event", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eventID = transport.scheduleRepeat( (time) => { expect(time).to.be.closeTo(startTime, 0.01); invoked = true; transport.clear(eventID); }, 1, 0 ); transport.start(startTime); }, 0.3); expect(invoked).to.equal(true); }); it("can cancel the timeline of scheduleRepeat", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.scheduleRepeat( () => { throw new Error("should not call this"); }, 0.01, 0 ); transport.cancel(0); transport.start(0); }); }); it("can schedule events with TransportTime", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eighth = transport.toSeconds("8n"); transport.scheduleRepeat( (time) => { expect(time).to.be.closeTo(startTime + eighth, 0.01); invoked = true; }, "1n", TransportTime("8n") ); transport.start(startTime); }, 0.4); expect(invoked).to.equal(true); }); it("can clear a scheduled event", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.scheduleRepeat( () => { throw new Error("should not call this function"); }, 1, 0 ); transport.clear(eventID); transport.stop(); }); }); it("can be scheduled in the future", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eventID = transport.scheduleRepeat( (time) => { transport.clear(eventID); expect(time).to.be.closeTo(startTime + 0.2, 0.01); invoked = true; }, 1, 0.2 ); transport.start(startTime); }, 0.5); expect(invoked).to.equal(true); }); it("repeats a repeat event", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.scheduleRepeat( () => { invocations++; }, 0.1, 0 ); transport.start(); }, 0.51); expect(invocations).to.equal(6); }); it("repeats at the repeat interval", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); let repeatTime = -1; transport.scheduleRepeat( (time) => { if (repeatTime !== -1) { expect(time - repeatTime).to.be.closeTo(0.1, 0.01); } repeatTime = time; wasCalled = true; }, 0.1, 0 ); transport.start(); }, 0.5); expect(wasCalled).to.equal(true); }); it("can schedule multiple events and invoke them in the right order", async () => { let first = false; let second = false; await Offline((context) => { const transport = new TransportInstance({ context }); const firstID = transport.scheduleRepeat( () => { first = true; transport.clear(firstID); }, 1, 0.1 ); const secondID = transport.scheduleRepeat( () => { transport.clear(secondID); expect(first).to.equal(true); second = true; }, 1, 0.11 ); transport.start(); }, 0.3); expect(first); expect(second); }); it("repeats for the given interval", async () => { let repeatCount = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.scheduleRepeat( (time) => { repeatCount++; }, 0.1, 0, 0.5 ); transport.start(); }, 0.61); expect(repeatCount).to.equal(5); }); it("can add an event after the Transport is started", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0); let wasScheduled = false; const times = [0.15, 0.3]; return (time) => { if (time > 0.1 && !wasScheduled) { wasScheduled = true; transport.scheduleRepeat( (repeatedTime) => { expect(repeatedTime).to.be.closeTo( times[invocations], 0.01 ); invocations++; }, 0.15, 0.15 ); } }; }, 0.31); expect(invocations).to.equal(2); }); it("can add an event to the past after the Transport is started", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.start(0); let wasScheduled = false; const times = [0.15, 0.25]; return (time) => { if (time >= 0.12 && !wasScheduled) { wasScheduled = true; transport.scheduleRepeat( (repeatedTime) => { expect(repeatedTime).to.be.closeTo( times[invocations], 0.01 ); invocations++; }, 0.1, 0.05 ); } }; }, 0.3); expect(invocations).to.equal(2); }); }); context("scheduleOnce", () => { it("can schedule a single event on the timeline", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.scheduleOnce(() => {}, 0); expect(eventID).to.be.a("number"); }); }); it("scheduled event gets invoked with the time of the event", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eventID = transport.scheduleOnce((time) => { invoked = true; transport.clear(eventID); expect(time).to.be.closeTo(startTime, 0.01); }, 0); transport.start(startTime); }, 0.2); expect(invoked).to.equal(true); }); it("can schedule events with TransportTime", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = 0.1; const eighth = transport.toSeconds("8n"); transport.scheduleOnce((time) => { expect(time).to.be.closeTo(startTime + eighth, 0.01); invoked = true; }, TransportTime("8n")); transport.start(startTime); }, 0.5); expect(invoked).to.equal(true); }); it("can clear a scheduled event", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const eventID = transport.scheduleOnce(() => { throw new Error("should not call this function"); }, 0); transport.clear(eventID); transport.start(); }); }); it("can be scheduled in the future", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const startTime = transport.now() + 0.1; const eventID = transport.scheduleOnce((time) => { transport.clear(eventID); expect(time).to.be.closeTo(startTime + 0.3, 0.01); invoked = true; }, 0.3); transport.start(startTime); }, 0.5); expect(invoked).to.equal(true); }); it("the event is removed after is is invoked", async () => { let iterations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.scheduleOnce(() => { iterations++; }, 0); transport.start().stop("+0.1").start("+0.2"); }, 0.5); expect(iterations).to.be.lessThan(2); }); }); context("events", () => { it("invokes start/stop/pause events", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.on("start", () => { invocations++; }); transport.on("stop", () => { invocations++; }); transport.on("pause", () => { invocations++; }); transport.start().stop(0.1).start(0.2); }, 0.5); expect(invocations).to.equal(3); }); it("invokes start event with correct offset", async () => { let wasCalled = false; await Offline((context) => { const transport = new TransportInstance({ context }); transport.on("start", (time, offset) => { expect(time).to.be.closeTo(0.2, 0.01); expect(offset).to.be.closeTo(0.5, 0.001); wasCalled = true; }); transport.start(0.2, "4n"); }, 0.3); expect(wasCalled).to.equal(true); }); it("invokes the event just before the scheduled time", async () => { let invoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); transport.on("start", (time, offset) => { expect(time - transport.context.currentTime).to.be.closeTo( 0, 0.01 ); expect(offset).to.equal(0); invoked = true; }); transport.start(0.2); }, 0.3); expect(invoked).to.equal(true); }); it("passes in the time argument to the events", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); const now = transport.now(); transport.on("start", (time) => { invocations++; expect(time).to.be.closeTo(now + 0.1, 0.01); }); transport.on("stop", (time) => { invocations++; expect(time).to.be.closeTo(now + 0.2, 0.01); }); transport.start("+0.1").stop("+0.2"); }, 0.3); expect(invocations).to.equal(2); }); it("invokes the 'loop' method on loop", async () => { let loops = 0; await Offline((context) => { const transport = new TransportInstance({ context }); const sixteenth = transport.toSeconds("16n"); transport.setLoopPoints(0, sixteenth); transport.loop = true; let lastLoop = -1; transport.on("loop", (time) => { loops++; if (lastLoop !== -1) { expect(time - lastLoop).to.be.closeTo(sixteenth, 0.001); } lastLoop = time; }); transport.start(0).stop(sixteenth * 5.1); }, 0.7); expect(loops).to.equal(5); }); }); context("swing", () => { it("can get/set the swing subdivision", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.swingSubdivision = "8n"; expect(transport.swingSubdivision).to.equal("8n"); transport.swingSubdivision = "4n"; expect(transport.swingSubdivision).to.equal("4n"); }); }); it("can get/set the swing amount", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.swing = 0.5; expect(transport.swing).to.equal(0.5); transport.swing = 0; expect(transport.swing).to.equal(0); }); }); it("can swing", async () => { let invocations = 0; await Offline((context) => { const transport = new TransportInstance({ context }); transport.swing = 1; transport.swingSubdivision = "8n"; const eightNote = transport.toSeconds("8n"); // downbeat, no swing transport.schedule((time) => { invocations++; expect(time).is.closeTo(0, 0.001); }, 0); // eighth note has swing transport.schedule((time) => { invocations++; expect(time).is.closeTo((eightNote * 5) / 3, 0.001); }, "8n"); // sixteenth note is also swung transport.schedule((time) => { invocations++; expect(time).is.closeTo(eightNote, 0.05); }, "16n"); // no swing on the quarter transport.schedule((time) => { invocations++; expect(time).is.closeTo(eightNote * 2, 0.001); }, "4n"); transport.start(0).stop(0.7); }, 0.7); expect(invocations).to.equal(4); }); }); }); ================================================ FILE: Tone/core/clock/Transport.ts ================================================ import { ToneAudioNode } from "../../core/context/ToneAudioNode.js"; import { TimeClass } from "../../core/type/Time.js"; import { PlaybackState } from "../../core/util/StateTimeline.js"; import { TimelineValue } from "../../core/util/TimelineValue.js"; import { Pow } from "../../signal/Pow.js"; import { Signal } from "../../signal/Signal.js"; import { onContextClose, onContextInit, } from "../context/ContextInitialization.js"; import { Gain } from "../context/Gain.js"; import { ToneWithContext, ToneWithContextOptions, } from "../context/ToneWithContext.js"; import { TicksClass } from "../type/Ticks.js"; import { TransportTimeClass } from "../type/TransportTime.js"; import { BarsBeatsSixteenths, BPM, NormalRange, Seconds, Subdivision, Ticks, Time, TimeSignature, TransportTime, } from "../type/Units.js"; import { enterScheduledCallback } from "../util/Debug.js"; import { assertUsedScheduleTime } from "../util/Debug.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { Emitter } from "../util/Emitter.js"; import { readOnly, writable } from "../util/Interface.js"; import { IntervalTimeline } from "../util/IntervalTimeline.js"; import { Timeline } from "../util/Timeline.js"; import { isArray, isDefined } from "../util/TypeCheck.js"; import { Clock } from "./Clock.js"; import { TickParam } from "./TickParam.js"; import { TransportEvent } from "./TransportEvent.js"; import { TransportRepeatEvent } from "./TransportRepeatEvent.js"; interface TransportOptions extends ToneWithContextOptions { bpm: BPM; swing: NormalRange; swingSubdivision: Subdivision; timeSignature: number; loopStart: Time; loopEnd: Time; ppq: number; } type TransportEventNames = | "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart" | "ticks"; interface SyncedSignalEvent { signal: Signal; initial: number; nodes: ToneAudioNode[]; } type TransportCallback = (time: Seconds) => void; /** * Transport for timing musical events. * * Supports tempo curves and time changes. * * Unlike browser-based timing (setInterval, requestAnimationFrame), * Transport timing events pass in the exact time of the scheduled event * in the argument of the callback function. * * A single transport is created for you when the library is initialized. * * The transport emits "start", "stop", "pause", and "loop" events. * * @example * const osc = new Tone.Oscillator().toDestination(); * // Repeated event every 8th note. * Tone.getTransport().scheduleRepeat((time) => { * // Use the callback time to schedule events. * osc.start(time).stop(time + 0.1); * }, "8n"); * // Transport must be started before it starts invoking events. * Tone.getTransport().start(); * @category Core */ export class TransportInstance extends ToneWithContext implements Emitter { readonly name: string = "Transport"; //------------------------------------- // LOOPING //------------------------------------- /** * If the transport loops or not. */ private _loop: TimelineValue = new TimelineValue(false); /** * The loop start position in ticks */ private _loopStart: Ticks = 0; /** * The loop end position in ticks */ private _loopEnd: Ticks = 0; //------------------------------------- // CLOCK/TEMPO //------------------------------------- /** * Pulses per quarter is the number of ticks per quarter note. */ private _ppq: number; /** * watches the main oscillator for timing ticks * initially starts at 120bpm */ private _clock: Clock<"bpm">; /** * The Beats Per Minute of the Transport. * @example * const osc = new Tone.Oscillator().toDestination(); * Tone.getTransport().bpm.value = 80; * // start/stop the oscillator every quarter note * Tone.getTransport().scheduleRepeat(time => { * osc.start(time).stop(time + 0.1); * }, "4n"); * Tone.getTransport().start(); * // ramp the bpm to 120 over 10 seconds * Tone.getTransport().bpm.rampTo(120, 10); */ bpm: TickParam<"bpm">; /** * The time signature, or more accurately the numerator * of the time signature over a denominator of 4. */ private _timeSignature: number; //------------------------------------- // TIMELINE EVENTS //------------------------------------- /** * All the events in an object to keep track by ID */ private _scheduledEvents = {}; /** * The scheduled events. */ private _timeline: Timeline = new Timeline(); /** * Repeated events */ private _repeatedEvents: IntervalTimeline = new IntervalTimeline(); /** * All of the synced Signals */ private _syncedSignals: SyncedSignalEvent[] = []; //------------------------------------- // SWING //------------------------------------- /** * The subdivision of the swing */ private _swingTicks: Ticks; /** * The swing amount */ private _swingAmount: NormalRange = 0; constructor(options?: Partial); constructor() { const options = optionsFromArguments( TransportInstance.getDefaults(), arguments ); super(options); // CLOCK/TEMPO this._ppq = options.ppq; this._clock = new Clock({ callback: this._processTick.bind(this), context: this.context, frequency: 0, units: "bpm", }); this._bindClockEvents(); this.bpm = this._clock.frequency as unknown as TickParam<"bpm">; this._clock.frequency.multiplier = options.ppq; this.bpm.setValueAtTime(options.bpm, 0); readOnly(this, "bpm"); this._timeSignature = options.timeSignature; // SWING this._swingTicks = options.ppq / 2; // 8n } static getDefaults(): TransportOptions { return Object.assign(ToneWithContext.getDefaults(), { bpm: 120, loopEnd: "4m" as Subdivision, loopStart: 0, ppq: 192, swing: 0, swingSubdivision: "8n" as Subdivision, timeSignature: 4, }); } //------------------------------------- // TICKS //------------------------------------- /** * called on every tick * @param tickTime clock relative tick time */ private _processTick(tickTime: Seconds, ticks: Ticks): void { // do the loop test if (this._loop.get(tickTime)) { if (ticks >= this._loopEnd) { this.emit("loopEnd", tickTime); this._clock.setTicksAtTime(this._loopStart, tickTime); ticks = this._loopStart; this.emit( "loopStart", tickTime, this._clock.getSecondsAtTime(tickTime) ); this.emit("loop", tickTime); } } // handle swing if ( this._swingAmount > 0 && ticks % this._ppq !== 0 && // not on a downbeat 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; } // invoke the timeline events scheduled on this tick enterScheduledCallback(true); this._timeline.forEachAtTime(ticks, (event) => event.invoke(tickTime)); enterScheduledCallback(false); } //------------------------------------- // SCHEDULABLE EVENTS //------------------------------------- /** * Schedule an event to be invoked at a specific time. * @param callback The callback to invoke at the given time. * @param time The time to invoke the callback at. * @return The ID of the event, which can be used to cancel the event. * @example * // Schedule an event on the 16th measure. * Tone.getTransport().schedule((time) => { * // Invoked on measure 16. * console.log("measure 16!"); * }, "16:0:0"); */ schedule( callback: TransportCallback, time: TransportTime | TransportTimeClass ): number { const event = new TransportEvent(this, { callback, time: new TransportTimeClass(this.context, time).toTicks(), }); return this._addEvent(event, this._timeline); } /** * Schedule a repeated event. * * The event will fire at the `interval` starting at the `startTime` and for the specified `duration`. * * @param callback The callback to invoke. * @param interval The duration between successive callbacks. * @param startTime When the event should start. * @param duration How long the event should repeat. * @return The ID of the scheduled event. Use this to cancel the event. * @example * const osc = new Tone.Oscillator().toDestination().start(); * // A callback invoked every eighth note after the first measure. * Tone.getTransport().scheduleRepeat((time) => { * osc.start(time).stop(time + 0.1); * }, "8n", "1m"); */ scheduleRepeat( callback: TransportCallback, interval: Time | TimeClass, startTime?: TransportTime | TransportTimeClass, duration: Time = Infinity ): number { const event = new TransportRepeatEvent(this, { callback, duration: new TimeClass(this.context, duration).toTicks(), interval: new TimeClass(this.context, interval).toTicks(), time: new TransportTimeClass(this.context, startTime).toTicks(), }); // kick it off if the Transport is started // @ts-ignore return this._addEvent(event, this._repeatedEvents); } /** * Schedule an event that will be removed after it is invoked. * @param callback The callback to invoke once. * @param time The time the callback should be invoked. * @returns The ID of the scheduled event. */ scheduleOnce( callback: TransportCallback, time: TransportTime | TransportTimeClass ): number { const event = new TransportEvent(this, { callback, once: true, time: new TransportTimeClass(this.context, time).toTicks(), }); return this._addEvent(event, this._timeline); } /** * Clear the passed in event id from the timeline * @param eventId The id of the event. */ clear(eventId: number): this { if (this._scheduledEvents.hasOwnProperty(eventId)) { const item = this._scheduledEvents[eventId.toString()]; item.timeline.remove(item.event); item.event.dispose(); delete this._scheduledEvents[eventId.toString()]; } return this; } /** * Add an event to the correct timeline. Keep track of the * timeline it was added to. * @returns the event id which was just added */ private _addEvent( event: TransportEvent, timeline: Timeline ): number { this._scheduledEvents[event.id.toString()] = { event, timeline, }; timeline.add(event); return event.id; } /** * Remove scheduled events from the timeline after * the given time. Repeated events will be removed * if their startTime is after the given time * @param after Clear all events after this time. */ 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) ); return this; } //------------------------------------- // START/STOP/PAUSE //------------------------------------- /** * Bind start/stop/pause events from the clock and emit them. */ private _bindClockEvents(): void { this._clock.on("start", (time, offset) => { offset = new TicksClass(this.context, offset).toSeconds(); this.emit("start", time, offset); }); this._clock.on("stop", (time) => { this.emit("stop", time); }); this._clock.on("pause", (time) => { this.emit("pause", time); }); } /** * The playback state of the transport, either "started", "stopped", or "paused". */ get state(): PlaybackState { return this._clock.getStateAtTime(this.now()); } /** * Start the transport and all sources synced to the transport. * @param time The time when the transport should start. * @param offset The timeline offset to start the transport from. * @example * // Start the transport in one second, beginning at the start of the 5th measure. * Tone.getTransport().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); } // start the clock this._clock.start(time, offsetTicks); return this; } /** * Stop the transport and all sources synced to the transport. * @param time The time when the transport should stop. * @example * Tone.getTransport().stop(); */ stop(time?: Time): this { this._clock.stop(time); return this; } /** * Pause the transport and all sources synced to the transport. */ pause(time?: Time): this { this._clock.pause(time); return this; } /** * Toggle the current state of the transport. * * If it is started, it will stop it. If it is stopped, it will start it. * * @param time The time of the event. */ toggle(time?: Time): this { time = this.toSeconds(time); if (this._clock.getStateAtTime(time) !== "started") { this.start(time); } else { this.stop(time); } return this; } //------------------------------------- // SETTERS/GETTERS //------------------------------------- /** * The time signature as just the numerator over 4. * For example 4/4 would be just 4 and 6/8 would be 3. * @example * // common time * Tone.getTransport().timeSignature = 4; * // 7/8 * Tone.getTransport().timeSignature = [7, 8]; * // this will be reduced to a single number * Tone.getTransport().timeSignature; // returns 3.5 */ get timeSignature(): TimeSignature { return this._timeSignature; } set timeSignature(timeSig: TimeSignature) { if (isArray(timeSig)) { timeSig = (timeSig[0] / timeSig[1]) * 4; } this._timeSignature = timeSig; } /** * When the Transport.loop = true, this is the starting position of the loop. */ get loopStart(): Time { return new TimeClass(this.context, this._loopStart, "i").toSeconds(); } set loopStart(startPosition: Time) { this._loopStart = this.toTicks(startPosition); } /** * When the Transport.loop = true, this is the ending position of the loop. */ get loopEnd(): Time { return new TimeClass(this.context, this._loopEnd, "i").toSeconds(); } set loopEnd(endPosition: Time) { this._loopEnd = this.toTicks(endPosition); } /** * If the transport loops or not. */ get loop(): boolean { return this._loop.get(this.now()); } set loop(loop) { this._loop.set(loop, this.now()); } /** * Set the loop start and stop at the same time. * @example * // loop over the first measure * Tone.getTransport().setLoopPoints(0, "1m"); * Tone.getTransport().loop = true; */ setLoopPoints( startPosition: TransportTime, endPosition: TransportTime ): this { this.loopStart = startPosition; this.loopEnd = endPosition; return this; } /** * The swing value. Between 0-1 where 1 equal to the note + half the subdivision. */ get swing(): NormalRange { return this._swingAmount; } set swing(amount: NormalRange) { // scale the values to a normal range this._swingAmount = amount; } /** * Set the subdivision which the swing will be applied to. * The default value is an 8th note. Value must be less * than a quarter note. */ get swingSubdivision(): Subdivision { return new TicksClass(this.context, this._swingTicks).toNotation(); } set swingSubdivision(subdivision: Subdivision) { this._swingTicks = this.toTicks(subdivision); } /** * The Transport's position in Bars:Beats:Sixteenths. * Setting the value will jump to that position right away. */ get position(): BarsBeatsSixteenths | Time { const now = this.now(); const ticks = this._clock.getTicksAtTime(now); return new TicksClass(this.context, ticks).toBarsBeatsSixteenths(); } set position(progress: Time) { const ticks = this.toTicks(progress); this.ticks = ticks; } /** * The Transport's position in seconds. * Setting the value will jump to that position right away. */ get seconds(): Seconds { return this._clock.seconds; } set seconds(s: Seconds) { const now = this.now(); const ticks = this._clock.frequency.timeToTicks(s, now); this.ticks = ticks; } /** * The Transport's loop position as a normalized value. Always * 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) ); } else { return 0; } } /** * The Transport's current tick position. */ get ticks(): Ticks { return this._clock.ticks; } set ticks(t: Ticks) { assertUsedScheduleTime(); // "floor" ensures that any events scheduled on this tick will be called. t = Math.floor(t); if (this._clock.ticks === t) { return; } const now = this.now(); // stop everything synced to the transport if (this.state === "started") { const ticks = this._clock.getTicksAtTime(now); // schedule to start on the next tick, #573 const remainingTick = this._clock.frequency.getDurationOfTicks( Math.ceil(ticks) - ticks, now ); const time = now + remainingTick; this.emit("stop", time); this._clock.setTicksAtTime(t, time); // restart it with the new time this.emit("start", time, this._clock.getSecondsAtTime(time)); } else { this.emit("ticks", now); this._clock.setTicksAtTime(t, now); } } /** * Get the clock's ticks at the given time. * @param time When to get the tick value * @return The tick value at the given time. */ getTicksAtTime(time?: Time): Ticks { return this._clock.getTicksAtTime(time); } /** * Set the Transport's {@link ticks} value at the given time * @param ticks The tick value to set * @param time The Context time at which to set the seconds value */ setTicksAtTime(ticks: Ticks, time: Time): this { this._clock.setTicksAtTime(ticks, time); return this; } /** * Return the elapsed seconds at the given time. * @param time When to get the elapsed seconds * @return The number of elapsed seconds */ getSecondsAtTime(time: Time): Seconds { return this._clock.getSecondsAtTime(time); } /** * Set the Transport's {@link seconds} value at the given time. * @param seconds The seconds value to set * @param time The Context time at which to set the seconds value */ setSecondsAtTime(seconds: Seconds, time: Time): this { this.setTicksAtTime(this.toTicks(seconds), time); return this; } /** * Pulses Per Quarter note. This is the smallest resolution * the Transport timing supports. This should be set once * on initialization and not set again. Changing this value * after other objects have been created can cause problems. */ get PPQ(): number { return this._clock.frequency.multiplier; } set PPQ(ppq: number) { this._clock.frequency.multiplier = ppq; } //------------------------------------- // SYNCING //------------------------------------- /** * Returns the time aligned to the next subdivision * of the Transport. If the Transport is not started, * it will return 0. * Note: this will not work precisely during tempo ramps. * @param subdivision The subdivision to quantize to * @return The context time of the next subdivision. * @example * // the transport must be started, otherwise returns 0 * Tone.getTransport().start(); * Tone.getTransport().nextSubdivision("4n"); */ nextSubdivision(subdivision?: Time): Seconds { subdivision = this.toTicks(subdivision); if (this.state !== "started") { // if the transport's not started, return 0 return 0; } else { const now = this.now(); // the remainder of the current ticks and the subdivision const transportPos = this.getTicksAtTime(now); const remainingTicks = subdivision - (transportPos % subdivision); return this._clock.nextTickTime(remainingTicks, now); } } /** * Attaches the signal to the tempo control signal so that * any changes in the tempo will change the signal in the same * ratio. * * @param signal * @param ratio Optionally pass in the ratio between the two signals. * 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 if (signal.getValueAtTime(now) !== 0) { ratio = signal.getValueAtTime(now) / sourceValue; } else { ratio = 0; } } const ratioSignal = new Gain(ratio); // @ts-ignore source.connect(ratioSignal); // @ts-ignore ratioSignal.connect(signal._param); nodes.push(ratioSignal); this._syncedSignals.push({ initial: signal.value, nodes: nodes, signal, }); signal.value = 0; return this; } /** * Unsyncs a previously synced signal from the transport's control. * @see {@link syncSignal}. */ unsyncSignal(signal: Signal): this { for (let i = this._syncedSignals.length - 1; i >= 0; i--) { const syncedSignal = this._syncedSignals[i]; if (syncedSignal.signal === signal) { syncedSignal.nodes.forEach((node) => node.dispose()); syncedSignal.signal.value = syncedSignal.initial; this._syncedSignals.splice(i, 1); } } return this; } /** * Clean up. */ dispose(): this { super.dispose(); this._clock.dispose(); writable(this, "bpm"); this._timeline.dispose(); this._repeatedEvents.dispose(); return this; } //------------------------------------- // 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; emit!: (event: any, ...args: any[]) => this; } Emitter.mixin(TransportInstance); //------------------------------------- // INITIALIZATION //------------------------------------- onContextInit((context) => { context.transport = new TransportInstance({ context }); }); onContextClose((context) => { context.transport.dispose(); }); ================================================ FILE: Tone/core/clock/TransportEvent.test.ts ================================================ import { expect } from "chai"; import { Offline } from "../../../test/helper/Offline.js"; import { TransportInstance } from "./Transport.js"; import { TransportEvent } from "./TransportEvent.js"; describe("TransportEvent", () => { it("can be created and disposed", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportEvent(transport, { time: 0, }); event.dispose(); }); }); it("has a unique id", () => { return Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportEvent(transport, { time: 0, }); expect(event.id).to.be.a("number"); event.dispose(); }); }); it("can invoke the callback", async () => { let wasInvoked = false; await Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportEvent(transport, { callback: (time) => { expect(time).to.equal(100); wasInvoked = true; }, time: 0, }); event.invoke(100); }); expect(wasInvoked).to.equal(true); }); }); ================================================ FILE: Tone/core/clock/TransportEvent.ts ================================================ import { Seconds, Ticks } from "../type/Units.js"; import { noOp } from "../util/Interface.js"; import type { TransportInstance as Transport } from "./Transport.js"; export interface TransportEventOptions { callback: (time: number) => void; once: boolean; time: Ticks; } /** * TransportEvent is an internal class used by {@link TransportInstance} * to schedule events. Do no invoke this class directly, it is * handled from within Tone.Transport. */ export class TransportEvent { /** * Reference to the Transport that created it */ protected transport: Transport; /** * The unique id of the event */ id: number = TransportEvent._eventId++; /** * The time the event starts */ time: Ticks; /** * The callback to invoke */ private callback?: (time: Seconds) => void; /** * If the event should be removed after being invoked. */ 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 */ constructor(transport: Transport, opts: Partial) { const options: TransportEventOptions = Object.assign( TransportEvent.getDefaults(), opts ); this.transport = transport; this.callback = options.callback; this._once = options.once; this.time = Math.floor(options.time); this._remainderTime = options.time - this.time; } static getDefaults(): TransportEventOptions { return { callback: noOp, once: false, time: 0, }; } /** * Current ID counter */ 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) { const tickDuration = this.transport.bpm.getDurationOfTicks(1, time); this.callback(time + this._remainderTime * tickDuration); if (this._once) { this.transport.clear(this.id); } } } /** * Clean up */ dispose(): this { this.callback = undefined; return this; } } ================================================ FILE: Tone/core/clock/TransportRepeatEvent.test.ts ================================================ import { expect } from "chai"; import { Offline } from "../../../test/helper/Offline.js"; import { TransportInstance } from "./Transport.js"; import { TransportRepeatEvent } from "./TransportRepeatEvent.js"; describe("TransportRepeatEvent", () => { it("can be created and disposed", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportRepeatEvent(transport, { duration: 100, interval: 4, time: 0, }); event.dispose(); }); }); it("generates a unique event ID", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportRepeatEvent(transport, { time: 0, }); expect(event.id).to.be.a("number"); event.dispose(); }); }); it("is removed from the Transport when disposed", async () => { await Offline((context) => { const transport = new TransportInstance({ context }); const event = new TransportRepeatEvent(transport, { time: 0, }); event.dispose(); // @ts-ignore expect(transport._timeline.length).to.equal(0); }); }); }); ================================================ FILE: Tone/core/clock/TransportRepeatEvent.ts ================================================ import { BaseContext } from "../context/BaseContext.js"; import { TicksClass } from "../type/Ticks.js"; import { Seconds, Ticks, Time } from "../type/Units.js"; import { GT, LT } from "../util/Math.js"; import type { TransportInstance as Transport } from "./Transport.js"; import { TransportEvent, TransportEventOptions } from "./TransportEvent.js"; interface TransportRepeatEventOptions extends TransportEventOptions { interval: Ticks; duration: Ticks; } /** * TransportRepeatEvent is an internal class used by Tone.Transport * to schedule repeat events. This class should not be instantiated directly. */ export class TransportRepeatEvent extends TransportEvent { /** * When the event should stop repeating */ private duration: Ticks; /** * The interval of the repeated event */ private _interval: Ticks; /** * The ID of the current timeline event */ private _currentId = -1; /** * The ID of the next timeline event */ private _nextId = -1; /** * The time of the next event */ private _nextTick = this.time; /** * a reference to the bound start method */ private _boundRestart = this._restart.bind(this); /** * The audio context belonging to this event */ protected context: BaseContext; /** * @param transport The transport object which the event belongs to */ constructor( transport: Transport, opts: Partial ) { super(transport, opts); const options = Object.assign(TransportRepeatEvent.getDefaults(), opts); 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(); } static getDefaults(): TransportRepeatEventOptions { return Object.assign({}, TransportEvent.getDefaults(), { duration: Infinity, interval: 1, once: false, }); } /** * Invoke the callback. Returns the tick time which * the next event should be scheduled at. * @param time The AudioContext time in seconds of the event */ invoke(time: Seconds): void { // create more events if necessary this._createEvents(); // call the super class 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(): void { // 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), new TicksClass(this.context, this._nextTick).toSeconds() ); } } /** * 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); // start at the first event this._nextTick = this.floatTime; const ticks = this.transport.getTicksAtTime(time); 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._createEvent(); this._nextTick += this._interval; this._nextId = this._createEvent(); } /** * Clean up */ dispose(): this { super.dispose(); this.transport.clear(this._currentId); 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; } } ================================================ FILE: Tone/core/context/AbstractParam.ts ================================================ import { Time, UnitMap, UnitName } from "../type/Units.js"; /** * Abstract base class for {@link Param} and {@link Signal} */ export abstract class AbstractParam { /** * Schedules a parameter value change at the given time. * @param value The value to set the signal. * @param time The time when the change should occur. * @example * return Tone.Offline(() => { * const osc = new Tone.Oscillator(20).toDestination().start(); * // set the frequency to 40 at exactly 0.25 seconds * osc.frequency.setValueAtTime(40, 0.25); * }, 0.5, 1); */ abstract setValueAtTime(value: UnitMap[TypeName], time: Time): this; /** * Get the signals value at the given time. Subsequent scheduling * may invalidate the returned value. * @param time When to get the value * @example * const signal = new Tone.Signal().toDestination(); * // ramp up to '8' over 3 seconds * signal.rampTo(8, 3); * // ramp back down to '0' over 3 seconds * signal.rampTo(0, 3, "+3"); * setInterval(() => { * // check the value every 100 ms * console.log(signal.getValueAtTime(Tone.now())); * }, 100); */ abstract getValueAtTime(time: Time): UnitMap[TypeName]; /** * Creates a schedule point with the current value at the current time. * Automation methods like {@link linearRampToValueAtTime} and {@link exponentialRampToValueAtTime} * require a starting automation value usually set by {@link setValueAtTime}. This method * is useful since it will do a `setValueAtTime` with whatever the currently computed * value at the given time is. * @param time When to add a ramp point. * @example * const osc = new Tone.Oscillator().toDestination().start(); * // set the frequency to "G4" in exactly 1 second from now. * osc.frequency.setRampPoint("+1"); * osc.frequency.linearRampToValueAtTime("C1", "+2"); */ abstract setRampPoint(time: Time): this; /** * Schedules a linear continuous change in parameter value from the * previous scheduled parameter value to the given value. * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(0).toDestination(); * // the ramp starts from the previously scheduled value * signal.setValueAtTime(0, 0.1); * signal.linearRampToValueAtTime(1, 0.4); * }, 0.5, 1); */ abstract linearRampToValueAtTime( value: UnitMap[TypeName], time: Time ): this; /** * Schedules an exponential continuous change in parameter value from * the previous scheduled parameter value to the given value. * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(1).toDestination(); * // the ramp starts from the previously scheduled value, which must be positive * signal.setValueAtTime(1, 0.1); * signal.exponentialRampToValueAtTime(0, 0.4); * }, 0.5, 1); */ abstract exponentialRampToValueAtTime( value: UnitMap[TypeName], time: Time ): this; /** * Schedules an exponential continuous change in parameter value from * the current time and current value to the given value over the * duration of the rampTime. * @param value The value to ramp to. * @param rampTime the time that it takes the * value to ramp from its current value * @param startTime When the ramp should start. * @example * const delay = new Tone.FeedbackDelay(0.5, 0.98).toDestination(); * // a short burst of noise through the feedback delay * const noise = new Tone.Noise().connect(delay).start().stop("+0.1"); * // making the delay time shorter over time will also make the pitch rise * delay.delayTime.exponentialRampTo(0.01, 20); * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(.1).toDestination(); * signal.exponentialRampTo(5, 0.3, 0.1); * }, 0.5, 1); */ abstract exponentialRampTo( value: UnitMap[TypeName], rampTime: Time, startTime?: Time ): this; /** * Schedules an linear continuous change in parameter value from * the current time and current value to the given value over the * duration of the rampTime. * * @param value The value to ramp to. * @param rampTime the time that it takes the * value to ramp from its current value * @param startTime When the ramp should start. * @returns {Param} this * @example * const delay = new Tone.FeedbackDelay(0.5, 0.98).toDestination(); * // a short burst of noise through the feedback delay * const noise = new Tone.Noise().connect(delay).start().stop("+0.1"); * // making the delay time shorter over time will also make the pitch rise * delay.delayTime.linearRampTo(0.01, 20); * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(1).toDestination(); * signal.linearRampTo(0, 0.3, 0.1); * }, 0.5, 1); */ abstract linearRampTo( value: UnitMap[TypeName], rampTime: Time, startTime?: Time ): this; /** * Start exponentially approaching the target value at the given time. Since it * is an exponential approach it will continue approaching after the ramp duration. The * rampTime is the time that it takes to reach over 99% of the way towards the value. * @param value The value to ramp to. * @param rampTime the time that it takes the * value to ramp from its current value * @param startTime When the ramp should start. * @example * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(1).toDestination(); * signal.targetRampTo(0, 0.3, 0.1); * }, 0.5, 1); */ abstract targetRampTo( value: UnitMap[TypeName], rampTime: Time, startTime?: Time ): this; /** * Start exponentially approaching the target value at the given time. Since it * is an exponential approach it will continue approaching after the ramp duration. The * rampTime is the time that it takes to reach over 99% of the way towards the value. This methods * is similar to setTargetAtTime except the third argument is a time instead of a 'timeConstant' * @param value The value to ramp to. * @param time When the ramp should start. * @param rampTime the time that it takes the value to ramp from its current value * @example * const osc = new Tone.Oscillator().toDestination().start(); * // exponential approach over 4 seconds starting in 1 second * osc.frequency.exponentialApproachValueAtTime("C4", "+1", 4); */ abstract exponentialApproachValueAtTime( value: UnitMap[TypeName], time: Time, rampTime: Time ): this; /** * Start exponentially approaching the target value at the given time with * a rate having the given time constant. * @param value * @param startTime * @param timeConstant */ abstract setTargetAtTime( value: UnitMap[TypeName], startTime: Time, timeConstant: number ): this; /** * Sets an array of arbitrary parameter values starting at the given time * for the given duration. * * @param values * @param startTime * @param duration * @param scaling If the values in the curve should be scaled by some value * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(1).toDestination(); * signal.setValueCurveAtTime([1, 0.2, 0.8, 0.1, 0], 0.2, 0.3); * }, 0.5, 1); */ abstract setValueCurveAtTime( values: UnitMap[TypeName][], startTime: Time, duration: Time, scaling?: number ): this; /** * Cancels all scheduled parameter changes with times greater than or * equal to startTime. * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(0).toDestination(); * signal.setValueAtTime(0.1, 0.1); * signal.setValueAtTime(0.2, 0.2); * signal.setValueAtTime(0.3, 0.3); * signal.setValueAtTime(0.4, 0.4); * // cancels the last two scheduled changes * signal.cancelScheduledValues(0.3); * }, 0.5, 1); */ abstract cancelScheduledValues(time: Time): this; /** * This is similar to {@link cancelScheduledValues} except * it holds the automated value at time until the next automated event. * @example * return Tone.Offline(() => { * const signal = new Tone.Signal(0).toDestination(); * signal.linearRampTo(1, 0.5, 0); * signal.cancelAndHoldAtTime(0.3); * }, 0.5, 1); */ abstract cancelAndHoldAtTime(time: Time): this; /** * Ramps to the given value over the duration of the rampTime. * Automatically selects the best ramp type (exponential or linear) * depending on the `units` of the signal * * @param value * @param rampTime The time that it takes the value to ramp from its current value * @param startTime When the ramp should start. * @example * const osc = new Tone.Oscillator().toDestination().start(); * // schedule it to ramp either linearly or exponentially depending on the units * osc.frequency.rampTo("A2", 10); * @example * const osc = new Tone.Oscillator().toDestination().start(); * // schedule it to ramp starting at a specific time * osc.frequency.rampTo("A2", 10, "+2"); */ abstract rampTo( value: UnitMap[TypeName], rampTime: Time, startTime?: Time ): this; /** * The current value of the parameter. Setting this value * is equivalent to setValueAtTime(value, context.currentTime) */ abstract value: UnitMap[TypeName]; /** * If the value should be converted or not */ abstract convert: boolean; /** * The unit type */ abstract readonly units: UnitName; /** * True if the signal value is being overridden by * a connected signal. Internal use only. */ abstract overridden: boolean; /** * The minimum value of the output given the units */ abstract readonly minValue: number; /** * The maximum value of the output given the units */ abstract readonly maxValue: number; } ================================================ FILE: Tone/core/context/AudioContext.ts ================================================ import { AudioContext as stdAudioContext, AudioWorkletNode as stdAudioWorkletNode, OfflineAudioContext as stdOfflineAudioContext, } from "standardized-audio-context"; import { assert } from "../util/Debug.js"; import { isDefined } from "../util/TypeCheck.js"; /** * Create a new AudioContext */ export function createAudioContext( options?: AudioContextOptions ): AudioContext { return new stdAudioContext(options) as unknown as AudioContext; } /** * Create a new OfflineAudioContext */ export function createOfflineAudioContext( channels: number, length: number, sampleRate: number ): OfflineAudioContext { return new stdOfflineAudioContext( channels, length, sampleRate ) as unknown as OfflineAudioContext; } /** * Either the online or offline audio context */ export type AnyAudioContext = AudioContext | OfflineAudioContext; /** * Interface for things that Tone.js adds to the window */ interface ToneWindow extends Window { TONE_SILENCE_LOGGING?: boolean; TONE_DEBUG_CLASS?: string; BaseAudioContext: any; AudioWorkletNode: any; } /** * A reference to the window object * @hidden */ export const theWindow: ToneWindow | null = typeof self === "object" ? self : null; /** * If the browser has a window object which has an AudioContext * @hidden */ export const hasAudioContext = theWindow && (theWindow.hasOwnProperty("AudioContext") || theWindow.hasOwnProperty("webkitAudioContext")); export function createAudioWorkletNode( context: AnyAudioContext, name: string, options?: Partial ): AudioWorkletNode { assert( isDefined(stdAudioWorkletNode), "AudioWorkletNode only works in a secure context (https or localhost)" ); return new ( context instanceof theWindow?.BaseAudioContext ? theWindow?.AudioWorkletNode : stdAudioWorkletNode )(context, name, options); } /** * This promise resolves to a boolean which indicates if the * functionality is supported within the currently used browse. * Taken from [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context#issupported) */ export { isSupported as supported } from "standardized-audio-context"; ================================================ FILE: Tone/core/context/BaseContext.ts ================================================ import type { TransportInstance as Transport } from "../clock/Transport.js"; import { Seconds } from "../type/Units.js"; import type { DrawInstance as Draw } from "../util/Draw.js"; import { Emitter } from "../util/Emitter.js"; import { AnyAudioContext } from "./AudioContext.js"; import type { DestinationInstance as Destination } from "./Destination.js"; import type { ListenerInstance as Listener } from "./Listener.js"; // these are either not used in Tone.js or deprecated and not implemented. export type ExcludedFromBaseAudioContext = | "onstatechange" | "addEventListener" | "removeEventListener" | "listener" | "dispatchEvent" | "audioWorklet" | "destination" | "createScriptProcessor"; // the subset of the BaseAudioContext which Tone.Context implements. export type BaseAudioContextSubset = Omit< BaseAudioContext, ExcludedFromBaseAudioContext >; export type ContextLatencyHint = AudioContextLatencyCategory; /** * Shared class for both Offline and Online Audio Context's */ export abstract class BaseContext extends Emitter<"statechange" | "tick"> implements BaseAudioContextSubset { //--------------------------- // BASE AUDIO CONTEXT METHODS //--------------------------- abstract createAnalyser(): AnalyserNode; abstract createOscillator(): OscillatorNode; abstract createBufferSource(): AudioBufferSourceNode; abstract createBiquadFilter(): BiquadFilterNode; abstract createBuffer( _numberOfChannels: number, _length: number, _sampleRate: number ): AudioBuffer; abstract createChannelMerger( _numberOfInputs?: number | undefined ): ChannelMergerNode; abstract createChannelSplitter( _numberOfOutputs?: number | undefined ): ChannelSplitterNode; abstract createConstantSource(): ConstantSourceNode; abstract createConvolver(): ConvolverNode; abstract createDelay(_maxDelayTime?: number | undefined): DelayNode; abstract createDynamicsCompressor(): DynamicsCompressorNode; abstract createGain(): GainNode; abstract createIIRFilter( _feedForward: number[] | Float32Array, _feedback: number[] | Float32Array ): IIRFilterNode; abstract createPanner(): PannerNode; abstract createPeriodicWave( _real: number[] | Float32Array, _imag: number[] | Float32Array, _constraints?: PeriodicWaveConstraints | undefined ): PeriodicWave; abstract createStereoPanner(): StereoPannerNode; abstract createWaveShaper(): WaveShaperNode; abstract createMediaStreamSource( _stream: MediaStream ): MediaStreamAudioSourceNode; abstract createMediaElementSource( _element: HTMLMediaElement ): MediaElementAudioSourceNode; abstract createMediaStreamDestination(): MediaStreamAudioDestinationNode; abstract decodeAudioData(_audioData: ArrayBuffer): Promise; //--------------------------- // TONE AUDIO CONTEXT METHODS //--------------------------- abstract createAudioWorkletNode( _name: string, _options?: Partial ): AudioWorkletNode; abstract get rawContext(): AnyAudioContext; abstract addAudioWorkletModule(_url: string): Promise; abstract lookAhead: number; abstract latencyHint: ContextLatencyHint | Seconds; abstract resume(): Promise; abstract setTimeout( _fn: (...args: any[]) => void, _timeout: Seconds ): number; abstract clearTimeout(_id: number): this; abstract setInterval( _fn: (...args: any[]) => void, _interval: Seconds ): number; abstract clearInterval(_id: number): this; /** * @deprecated use ToneConstantSource instead */ abstract getConstant(_val: number): AudioBufferSourceNode; abstract get currentTime(): Seconds; abstract get state(): AudioContextState; abstract get sampleRate(): number; abstract get listener(): Listener; abstract get transport(): Transport; abstract get draw(): Draw; abstract get destination(): Destination; abstract now(): Seconds; abstract immediate(): Seconds; /* * This is a placeholder so that JSON.stringify does not throw an error * This matches what JSON.stringify(audioContext) returns on a native * audioContext instance. */ toJSON(): Record { return {}; } readonly isOffline: boolean = false; } ================================================ FILE: Tone/core/context/Context.test.ts ================================================ import { expect } from "chai"; import { ConstantOutput } from "../../../test/helper/ConstantOutput.js"; import { Offline } from "../../../test/helper/Offline.js"; import { TransportInstance } from "../clock/Transport.js"; import { getContext } from "../Global.js"; import { DrawInstance } from "../util/Draw.js"; import { createAudioContext } from "./AudioContext.js"; import { Context } from "./Context.js"; import { DestinationInstance } from "./Destination.js"; import { ListenerInstance } from "./Listener.js"; import { connect } from "./ToneAudioNode.js"; describe("Context", () => { it("creates and disposes the classes attached to the context", async () => { const ac = createAudioContext(); const context = new Context(ac); const ctxDest = context.destination; const ctxDraw = context.draw; const ctxTransport = context.transport; const ctxListener = context.listener; expect(context.destination).is.instanceOf(DestinationInstance); expect(context.draw).is.instanceOf(DrawInstance); expect(context.listener).is.instanceOf(ListenerInstance); await context.close(); expect(ctxDest.disposed).to.be.true; expect(ctxDraw.disposed).to.be.true; expect(ctxTransport.disposed).to.be.true; expect(ctxListener.disposed).to.be.true; context.dispose(); }); context("AudioContext", () => { it("extends the AudioContext methods", () => { const ctx = new Context(createAudioContext()); expect(ctx).to.have.property("createGain"); expect(ctx.createGain()).to.have.property("gain"); expect(ctx).to.have.property("createOscillator"); expect(ctx.createOscillator()).to.be.have.property("frequency"); expect(ctx).to.have.property("createDelay"); expect(ctx.createDelay()).to.be.have.property("delayTime"); expect(ctx).to.have.property("createConstantSource"); ctx.dispose(); return ctx.close(); }); it("can be stringified", () => { const ctx = new Context(createAudioContext()); expect(JSON.stringify(ctx)).to.equal("{}"); ctx.dispose(); return ctx.close(); }); it("clock is running", (done) => { const interval = setInterval(() => { if (getContext().currentTime > 0.5) { clearInterval(interval); done(); } }, 20); }); it("has a rawContext", () => { const ctx = new Context(createAudioContext()); expect(ctx.rawContext).has.property("destination"); expect(ctx.rawContext).has.property("sampleRate"); ctx.dispose(); return ctx.close(); }); it("can be constructed with an options object", () => { const ctx = new Context({ clockSource: "timeout", latencyHint: "playback", lookAhead: 0.2, updateInterval: 0.1, sampleRate: 32000, }); 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"); expect(ctx.sampleRate).to.equal(32000); ctx.dispose(); return ctx.close(); }); it("returns 'now' and 'immediate' time", () => { const ctx = new Context(); expect(ctx.now()).to.be.a("number"); expect(ctx.immediate()).to.be.a("number"); ctx.dispose(); return ctx.close(); }); }); context("state", () => { it("can suspend and resume the state", async () => { const ac = createAudioContext(); const context = new Context(ac); expect(context.rawContext).to.equal(ac); await ac.suspend(); expect(context.state).to.equal("suspended"); await context.resume(); expect(context.state).to.equal("running"); context.dispose(); return context.close(); }); it("invokes the statechange event", async () => { const ac = createAudioContext(); const context = new Context(ac); let triggerChange = false; context.on("statechange", (state) => { if (!triggerChange) { triggerChange = true; expect(state).to.equal("running"); } }); await context.resume(); await new Promise((done) => setTimeout(() => done(), 10)); expect(triggerChange).to.equal(true); return context.dispose(); }); }); context("clockSource", () => { let ctx; beforeEach(() => { ctx = new Context(); return ctx.resume(); }); afterEach(() => { ctx.dispose(); return ctx.close(); }); it("defaults to 'worker'", () => { expect(ctx.clockSource).to.equal("worker"); }); it("provides callback", (done) => { expect(ctx.clockSource).to.equal("worker"); ctx.setTimeout(() => { done(); }, 0.1); }); it("can be set to 'timeout'", (done) => { ctx.clockSource = "timeout"; expect(ctx.clockSource).to.equal("timeout"); ctx.setTimeout(() => { done(); }, 0.1); }); it("can be set to 'offline'", (done) => { ctx.clockSource = "offline"; expect(ctx.clockSource).to.equal("offline"); // provides no callback ctx.setTimeout(() => { throw new Error("shouldn't be called"); }, 0.1); setTimeout(() => { done(); }, 200); }); }); context("setTimeout", () => { let ctx; beforeEach(() => { ctx = new Context(); return ctx.resume(); }); afterEach(() => { ctx.dispose(); return ctx.close(); }); it("can set a timeout", (done) => { ctx.setTimeout(() => { done(); }, 0.1); }); it("returns an id", () => { expect(ctx.setTimeout(() => {}, 0.1)).to.be.a("number"); // try clearing a random ID, shouldn't cause any errors ctx.clearTimeout(-2); }); it("timeout is not invoked when cancelled", (done) => { const id = ctx.setTimeout(() => { throw new Error("shouldn't be invoked"); }, 0.01); ctx.clearTimeout(id); ctx.setTimeout(() => { done(); }, 0.02); }); it("order is maintained", (done) => { let wasInvoked = false; ctx.setTimeout(() => { expect(wasInvoked).to.equal(true); done(); }, 0.02); ctx.setTimeout(() => { wasInvoked = true; }, 0.01); }); it("is invoked in the offline context", () => { return Offline((context) => { const transport = new TransportInstance({ context }); transport.context.setTimeout(() => { expect(transport.now()).to.be.closeTo(0.01, 0.005); }, 0.01); }, 0.05); }); it("is robust against altering the timeline within the callback fn", (done) => { let invokeCount = 0; function checkDone(id: number) { // clearing the current event alters the timeline and should not cause an issue ctx.clearTimeout(id); invokeCount++; if (invokeCount === 3) { done(); } } const id0 = ctx.setTimeout(() => checkDone(id0), 0.01); const id1 = ctx.setTimeout(() => checkDone(id1), 0.01); const id2 = ctx.setTimeout(() => checkDone(id2), 0.01); }); }); context("setInterval", () => { let ctx; beforeEach(() => { ctx = new Context(); return ctx.resume(); }); afterEach(() => { ctx.dispose(); return ctx.close(); }); it("can set an interval", (done) => { ctx.setInterval(() => { done(); }, 0.1); }); it("returns an id", () => { expect(ctx.setInterval(() => {}, 0.1)).to.be.a("number"); // try clearing a random ID, shouldn't cause any errors ctx.clearInterval(-2); }); it("timeout is not invoked when cancelled", (done) => { const id = ctx.setInterval(() => { throw new Error("shouldn't be invoked"); }, 0.01); ctx.clearInterval(id); ctx.setInterval(() => { done(); }, 0.02); }); it("order is maintained", (done) => { let wasInvoked = false; ctx.setInterval(() => { expect(wasInvoked).to.equal(true); done(); }, 0.02); ctx.setInterval(() => { wasInvoked = true; }, 0.01); }); it("is invoked in the offline context", async () => { let invocationCount = 0; await Offline((context) => { context.setInterval(() => { invocationCount++; }, 0.01); }, 0.051); expect(invocationCount).to.equal(4); }); it("is invoked with the right interval", async () => { let numberOfInvocations = 0; await Offline((context) => { let intervalTime = context.now(); context.setInterval(() => { expect(context.now() - intervalTime).to.be.closeTo( 0.01, 0.005 ); intervalTime = context.now(); numberOfInvocations++; }, 0.01); }, 0.051); expect(numberOfInvocations).to.equal(4); }); }); context("get/set", () => { let ctx; beforeEach(() => { ctx = new Context(); return ctx.resume(); }); afterEach(() => { ctx.dispose(); return ctx.close(); }); it("can set the lookAhead", () => { ctx.lookAhead = 0.05; expect(ctx.lookAhead).to.equal(0.05); }); it("can set the updateInterval", () => { ctx.updateInterval = 0.05; expect(ctx.updateInterval).to.equal(0.05); }); it("gets a constant signal", () => { return ConstantOutput((context) => { const bufferSrc = context.getConstant(1); connect(bufferSrc, context.destination); }, 1); }); it("multiple calls return the same buffer source", () => { const bufferA = ctx.getConstant(2); const bufferB = ctx.getConstant(2); expect(bufferA).to.equal(bufferB); }); }); context("Methods", () => { let ctx; beforeEach(() => { ctx = new Context(); return ctx.resume(); }); afterEach(() => { ctx.dispose(); return ctx.close(); }); it("can create a MediaElementAudioSourceNode", () => { const audioNode = document.createElement("audio"); const node = ctx.createMediaElementSource(audioNode); expect(node).is.not.undefined; }); }); }); ================================================ FILE: Tone/core/context/Context.ts ================================================ import { Ticker, TickerClockSource } from "../clock/Ticker.js"; import type { TransportInstance as Transport } from "../clock/Transport.js"; import { Seconds } from "../type/Units.js"; import { isAudioContext } from "../util/AdvancedTypeCheck.js"; import { assert } from "../util/Debug.js"; import { optionsFromArguments } from "../util/Defaults.js"; import type { DrawInstance as Draw } from "../util/Draw.js"; import { Timeline } from "../util/Timeline.js"; import { isDefined } from "../util/TypeCheck.js"; import { AnyAudioContext, createAudioContext, createAudioWorkletNode, } from "./AudioContext.js"; import { BaseContext, ContextLatencyHint } from "./BaseContext.js"; import { closeContext, initializeContext } from "./ContextInitialization.js"; import type { DestinationInstance as Destination } from "./Destination.js"; import type { ListenerInstance as Listener } from "./Listener.js"; export interface ContextOptions { clockSource: TickerClockSource; latencyHint: ContextLatencyHint; lookAhead: Seconds; updateInterval: Seconds; context: AnyAudioContext; sampleRate: number; } export interface ContextTimeoutEvent { callback: (...args: any[]) => void; id: number; time: Seconds; } /** * Wraps the native AudioContext. * @category Core */ export class Context extends BaseContext { readonly name: string = "Context"; /** * A private reference to the BaseAudioContext. */ protected readonly _context: AnyAudioContext; /** * A reliable callback method. */ private readonly _ticker: Ticker; /** * The default latency hint. */ private _latencyHint!: ContextLatencyHint | Seconds; /** * An object containing all of the AudioBufferSourceNodes with constant values. */ private _constants = new Map(); /** * All of the setTimeout events. */ private _timeouts: Timeline = new Timeline(); /** * The timeout id counter */ private _timeoutIds = 0; /** * A reference the Transport singleton belonging to this context */ private _transport!: Transport; /** * A reference the Listener singleton belonging to this context */ private _listener!: Listener; /** * A reference the Destination singleton belonging to this context */ private _destination!: Destination; /** * A reference the Transport singleton belonging to this context */ private _draw!: Draw; /** * Private indicator if the context has been initialized */ 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 */ readonly isOffline: boolean = false; constructor(context?: AnyAudioContext); constructor(options?: Partial); constructor() { super(); const options = optionsFromArguments(Context.getDefaults(), arguments, [ "context", ]); 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( options.sampleRate ? { latencyHint: options.latencyHint, sampleRate: options.sampleRate, } : { latencyHint: options.latencyHint, } ); this._latencyHint = options.latencyHint; } this._ticker = new Ticker( this.emit.bind(this, "tick"), options.clockSource, options.updateInterval, this._context.sampleRate ); this.on("tick", this._timeoutLoop.bind(this)); // fwd events from the context this._context.onstatechange = () => { this.emit("statechange", this.state); }; // if no custom updateInterval provided, updateInterval will be derived by lookAhead setter this[ arguments[0]?.hasOwnProperty("updateInterval") ? "_lookAhead" : "lookAhead" ] = options.lookAhead; } static getDefaults(): ContextOptions { return { clockSource: "worker", latencyHint: "interactive", lookAhead: 0.1, updateInterval: 0.05, } as ContextOptions; } /** * Finish setting up the context. **You usually do not need to do this manually.** */ private initialize(): this { if (!this._initialized) { // add any additional modules initializeContext(this); this._initialized = true; } return this; } //--------------------------- // BASE AUDIO CONTEXT METHODS //--------------------------- createAnalyser(): AnalyserNode { return this._context.createAnalyser(); } createOscillator(): OscillatorNode { return this._context.createOscillator(); } createBufferSource(): AudioBufferSourceNode { return this._context.createBufferSource(); } createBiquadFilter(): BiquadFilterNode { return this._context.createBiquadFilter(); } createBuffer( numberOfChannels: number, length: number, sampleRate: number ): AudioBuffer { return this._context.createBuffer(numberOfChannels, length, sampleRate); } createChannelMerger( numberOfInputs?: number | undefined ): ChannelMergerNode { return this._context.createChannelMerger(numberOfInputs); } createChannelSplitter( numberOfOutputs?: number | undefined ): ChannelSplitterNode { return this._context.createChannelSplitter(numberOfOutputs); } createConstantSource(): ConstantSourceNode { return this._context.createConstantSource(); } createConvolver(): ConvolverNode { return this._context.createConvolver(); } createDelay(maxDelayTime?: number | undefined): DelayNode { return this._context.createDelay(maxDelayTime); } createDynamicsCompressor(): DynamicsCompressorNode { return this._context.createDynamicsCompressor(); } createGain(): GainNode { return this._context.createGain(); } createIIRFilter( feedForward: number[] | Float32Array, feedback: number[] | Float32Array ): IIRFilterNode { // @ts-ignore return this._context.createIIRFilter(feedForward, feedback); } createPanner(): PannerNode { return this._context.createPanner(); } createPeriodicWave( real: number[] | Float32Array, imag: number[] | Float32Array, constraints?: PeriodicWaveConstraints | undefined ): PeriodicWave { return this._context.createPeriodicWave(real, imag, constraints); } createStereoPanner(): StereoPannerNode { return this._context.createStereoPanner(); } createWaveShaper(): WaveShaperNode { return this._context.createWaveShaper(); } createMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode { assert( isAudioContext(this._context), "Not available if OfflineAudioContext" ); const context = this._context as AudioContext; return context.createMediaStreamSource(stream); } createMediaElementSource( element: HTMLMediaElement ): MediaElementAudioSourceNode { assert( isAudioContext(this._context), "Not available if OfflineAudioContext" ); const context = this._context as AudioContext; return context.createMediaElementSource(element); } createMediaStreamDestination(): MediaStreamAudioDestinationNode { assert( isAudioContext(this._context), "Not available if OfflineAudioContext" ); const context = this._context as AudioContext; return context.createMediaStreamDestination(); } decodeAudioData(audioData: ArrayBuffer): Promise { return this._context.decodeAudioData(audioData); } /** * The current time in seconds of the AudioContext. */ get currentTime(): Seconds { return this._context.currentTime; } /** * The current time in seconds of the AudioContext. */ get state(): AudioContextState { return this._context.state; } /** * The current time in seconds of the AudioContext. */ get sampleRate(): number { return this._context.sampleRate; } /** * The listener */ get listener(): Listener { this.initialize(); return this._listener; } set listener(l) { assert( !this._initialized, "The listener cannot be set after initialization." ); this._listener = l; } /** * There is only one Transport per Context. It is created on initialization. */ get transport(): Transport { this.initialize(); return this._transport; } set transport(t: Transport) { assert( !this._initialized, "The transport cannot be set after initialization." ); this._transport = t; } /** * This is the Draw object for the context which is useful for synchronizing the draw frame with the Tone.js clock. */ get draw(): Draw { this.initialize(); return this._draw; } set draw(d) { assert(!this._initialized, "Draw cannot be set after initialization."); this._draw = d; } /** * A reference to the Context's destination node. */ get destination(): Destination { this.initialize(); return this._destination; } set destination(d: Destination) { assert( !this._initialized, "The destination cannot be set after initialization." ); this._destination = d; } //-------------------------------------------- // AUDIO WORKLET //-------------------------------------------- /** * A set of unsettled promises returned by the addModule method */ private _workletPromises = new Set>(); /** * Create an audio worklet node from a name and options. The module * must first be loaded using {@link addAudioWorkletModule}. */ createAudioWorkletNode( name: string, options?: Partial ): AudioWorkletNode { return createAudioWorkletNode(this.rawContext, name, options); } /** * Add an AudioWorkletProcessor module * @param url The url of the module */ async addAudioWorkletModule(url: string): Promise { assert( isDefined(this.rawContext.audioWorklet), "AudioWorkletNode is only available in a secure context (https or localhost)" ); const workletPromise = this.rawContext.audioWorklet.addModule(url); this._workletPromises.add(workletPromise); workletPromise.finally(() => this._workletPromises.delete(workletPromise) ); return workletPromise; } /** * Returns a promise which resolves when all of the worklets have been loaded on this context */ protected async workletsAreReady(): Promise { await Promise.all(this._workletPromises); } //--------------------------- // TICKER //--------------------------- /** * How often the interval callback is invoked. * This number corresponds to how responsive the scheduling * can be. Setting to 0 will result in the lowest practical 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; } set updateInterval(interval: Seconds) { this._ticker.updateInterval = interval; } /** * What the source of the clock is, either "worker" (default), * "timeout", or "offline" (none). */ get clockSource(): TickerClockSource { return this._ticker.type; } set clockSource(type: TickerClockSource) { 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 {@link 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 : 0.01; } private _lookAhead!: Seconds; /** * The type of playback, which affects tradeoffs between audio * output latency and responsiveness. * In addition to setting the value in seconds, the latencyHint also * accepts the strings "interactive" (prioritizes low latency), * "playback" (prioritizes sustained playback), "balanced" (balances * latency and performance). * @example * // prioritize sustained playback * const context = new Tone.Context({ latencyHint: "playback" }); * // set this context as the global Context * Tone.setContext(context); * // the global context is gettable with Tone.getContext() * console.log(Tone.getContext().latencyHint); */ get latencyHint(): ContextLatencyHint | Seconds { return this._latencyHint; } /** * The unwrapped AudioContext or OfflineAudioContext */ get rawContext(): AnyAudioContext { return this._context; } /** * The current audio context time plus a short {@link lookAhead}. * @example * setInterval(() => { * console.log("now", Tone.now()); * }, 100); */ now(): Seconds { return this._context.currentTime + this._lookAhead; } /** * The current audio context time without the {@link lookAhead}. * In most cases it is better to use {@link now} instead of {@link immediate} since * with {@link now} the {@link lookAhead} is applied equally to _all_ components including internal components, * to making sure that everything is scheduled in sync. Mixing {@link now} and {@link immediate} * can cause some timing issues. If no lookAhead is desired, you can set the {@link lookAhead} to `0`. */ immediate(): Seconds { return this._context.currentTime; } /** * Starts the audio context from a suspended state. This is required * to initially start the AudioContext. * @see {@link start} */ resume(): Promise { if (isAudioContext(this._context)) { return this._context.resume(); } else { return Promise.resolve(); } } /** * Close the context. Once closed, the context can no longer be used and * any AudioNodes created from the context will be silent. */ async close(): Promise { if ( isAudioContext(this._context) && this.state !== "closed" && !this._closeStarted ) { this._closeStarted = true; await this._context.close(); } if (this._initialized) { closeContext(this); } } /** * **Internal** Generate a looped buffer at some constant value. * @deprecated */ getConstant(val: number): AudioBufferSourceNode { if (this._constants.has(val)) { return this._constants.get(val) as AudioBufferSourceNode; } else { const buffer = this._context.createBuffer( 1, 128, this._context.sampleRate ); const arr = buffer.getChannelData(0); for (let i = 0; i < arr.length; i++) { arr[i] = val; } const constant = this._context.createBufferSource(); constant.channelCount = 1; constant.channelCountMode = "explicit"; constant.buffer = buffer; constant.loop = true; constant.start(0); this._constants.set(val, constant); return constant; } } /** * Clean up. Also closes the audio context. */ dispose(): this { super.dispose(); this._ticker.dispose(); this._timeouts.dispose(); Object.keys(this._constants).map((val) => this._constants[val].disconnect() ); this.close(); return this; } //--------------------------- // TIMEOUTS //--------------------------- /** * The private loop which keeps track of the context scheduled timeouts * Is invoked from the clock source */ private _timeoutLoop(): void { const now = this.now(); this._timeouts.forEachBefore(now, (event) => { try { event.callback(); } finally { this._timeouts.remove(event); } }); } /** * A `setTimeout` which is guaranteed by the clock source. * * Also runs in the offline context. * * @param fn The callback to invoke. * @param timeout The timeout in seconds. * @returns ID to use when invoking {@link clearTimeout}. */ setTimeout(fn: (...args: any[]) => void, timeout: Seconds): number { this._timeoutIds++; const now = this.now(); this._timeouts.add({ callback: fn, id: this._timeoutIds, time: now + timeout, }); return this._timeoutIds; } /** * Clears a previously scheduled timeout with {@link setTimeout}. * @param id The ID returned from {@link setTimeout}. */ clearTimeout(id: number): this { this._timeouts.forEach((event) => { if (event.id === id) { this._timeouts.remove(event); } }); return this; } /** * Clear the function scheduled by {@link setInterval}. * @param id The ID returned from {@link setInterval}. */ clearInterval(id: number): this { return this.clearTimeout(id); } /** * Adds a repeating event to the context's callback clock. * @param fn The callback to invoke. * @param interval The timeout in seconds. * @returns ID to use when invoking {@link clearInterval}. */ setInterval(fn: (...args: any[]) => void, interval: Seconds): number { const id = ++this._timeoutIds; const intervalFn = () => { const now = this.now(); this._timeouts.add({ callback: () => { // invoke the callback fn(); // invoke the event to repeat it intervalFn(); }, id, time: now + interval, }); }; // kick it off intervalFn(); return id; } } ================================================ FILE: Tone/core/context/ContextInitialization.ts ================================================ //------------------------------------- // INITIALIZING NEW CONTEXT //------------------------------------- import type { Context } from "./Context.js"; /** * Array of callbacks to invoke when a new context is created */ const notifyNewContext: Array<(ctx: Context) => void> = []; /** * Used internally to setup a new Context */ export function onContextInit(cb: (ctx: Context) => void): void { notifyNewContext.push(cb); } /** * Invoke any classes which need to also be initialized when a new context is created. */ export function initializeContext(ctx: Context): void { // add any additional modules notifyNewContext.forEach((cb) => cb(ctx)); } /** * Array of callbacks to invoke when a new context is closed */ const notifyCloseContext: Array<(ctx: Context) => void> = []; /** * Used internally to tear down a Context */ export function onContextClose(cb: (ctx: Context) => void): void { notifyCloseContext.push(cb); } export function closeContext(ctx: Context): void { // remove any additional modules notifyCloseContext.forEach((cb) => cb(ctx)); } ================================================ FILE: Tone/core/context/Delay.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { connect } from "../context/ToneAudioNode.js"; import { Delay } from "./Delay.js"; describe("Delay", () => { BasicTests(Delay); it("can be created and disposed", () => { const delay = new Delay(); delay.dispose(); }); it("handles input and output connections", () => { const delay = new Delay(); delay.connect(connectTo()); connectFrom().connect(delay); connectFrom().connect(delay.delayTime); delay.dispose(); }); it("can be constructed with an options object", () => { const delay = new Delay({ delayTime: 0.3, maxDelay: 2, }); expect(delay.delayTime.value).to.be.closeTo(0.3, 0.001); expect(delay.maxDelay).to.equal(2); delay.dispose(); }); it("if the constructor delay time is greater than maxDelay, use that as the maxDelay time", () => { const delay = new Delay(3); expect(delay.delayTime.value).to.be.closeTo(3, 0.001); delay.dispose(); }); it("clamps the delayTime range between 0 and maxDelay", () => { const delay = new Delay({ maxDelay: 1, }); expect(() => { delay.delayTime.value = 2; }).to.throw(RangeError); expect(() => { delay.delayTime.value = -1; }).to.throw(RangeError); expect(delay.delayTime.value).to.be.closeTo(0, 0.001); delay.dispose(); }); it("can set the delayTime value", () => { const delay = new Delay(); expect(delay.delayTime.value).to.be.closeTo(0, 0.001); delay.delayTime.value = 0.2; expect(delay.delayTime.value).to.be.closeTo(0.2, 0.001); delay.dispose(); }); it("can be constructed with options object", () => { const delay = new Delay({ delayTime: 0.4, }); expect(delay.delayTime.value).to.be.closeTo(0.4, 0.001); delay.dispose(); }); it("can be constructed with an initial value", () => { const delay = new Delay(0.3); expect(delay.delayTime.value).to.be.closeTo(0.3, 0.001); delay.dispose(); }); it("can set the units", () => { const delay = new Delay(0); expect(delay.delayTime.value).to.be.closeTo(0, 0.001); delay.dispose(); }); it("can get the value using 'get'", () => { const delay = new Delay(2); const value = delay.get(); expect(value.delayTime).to.be.closeTo(2, 0.001); delay.dispose(); }); it("can set the value using 'set'", () => { const delay = new Delay(5); delay.set({ delayTime: 4, }); expect(delay.delayTime.value).to.be.closeTo(4, 0.001); delay.dispose(); }); it("passes audio through", () => { return PassAudio((input) => { const delay = new Delay().toDestination(); connect(input, delay); }); }); }); ================================================ FILE: Tone/core/context/Delay.ts ================================================ import { Param } from "../context/Param.js"; import { Seconds, Time } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { readOnly } from "../util/Interface.js"; import { ToneAudioNode, ToneAudioNodeOptions } from "./ToneAudioNode.js"; export interface DelayOptions extends ToneAudioNodeOptions { delayTime: Time; maxDelay: Time; } /** * Wrapper around Web Audio's native [DelayNode](http://webaudio.github.io/web-audio-api/#the-delaynode-interface). * @category Core * @example * return Tone.Offline(() => { * const delay = new Tone.Delay(0.1).toDestination(); * // connect the signal to both the delay and the destination * const pulse = new Tone.PulseOscillator().connect(delay).toDestination(); * // start and stop the pulse * pulse.start(0).stop(0.01); * }, 0.5, 1); */ export class Delay extends ToneAudioNode { readonly name: string = "Delay"; /** * Private holder of the max delay time */ private _maxDelay: Seconds; /** * The amount of time the incoming signal is delayed. * @example * const delay = new Tone.Delay().toDestination(); * // modulate the delayTime between 0.1 and 1 seconds * const delayLFO = new Tone.LFO(0.5, 0.1, 1).start().connect(delay.delayTime); * const pulse = new Tone.PulseOscillator().connect(delay).start(); * // the change in delayTime causes the pitch to go up and down */ readonly delayTime: Param<"time">; /** * Private reference to the internal DelayNode */ private _delayNode: DelayNode; readonly input: DelayNode; readonly output: DelayNode; /** * @param delayTime The delay applied to the incoming signal. * @param maxDelay The maximum delay time. */ constructor(delayTime?: Time, maxDelay?: Time); constructor(options?: Partial); constructor() { const options = optionsFromArguments(Delay.getDefaults(), arguments, [ "delayTime", "maxDelay", ]); super(options); const maxDelayInSeconds = this.toSeconds(options.maxDelay); this._maxDelay = Math.max( maxDelayInSeconds, this.toSeconds(options.delayTime) ); this._delayNode = this.input = this.output = this.context.createDelay(maxDelayInSeconds); this.delayTime = new Param({ context: this.context, param: this._delayNode.delayTime, units: "time", value: options.delayTime, minValue: 0, maxValue: this.maxDelay, }); readOnly(this, "delayTime"); } static getDefaults(): DelayOptions { return Object.assign(ToneAudioNode.getDefaults(), { delayTime: 0, maxDelay: 1, }); } /** * The maximum delay time. This cannot be changed after * the value is passed into the constructor. */ get maxDelay(): Seconds { return this._maxDelay; } /** * Clean up. */ dispose(): this { super.dispose(); this._delayNode.disconnect(); this.delayTime.dispose(); return this; } } ================================================ FILE: Tone/core/context/Destination.test.ts ================================================ import { expect } from "chai"; import { warns } from "../../../test/helper/Basic.js"; import { Offline } from "../../../test/helper/Offline.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Oscillator } from "../../source/oscillator/Oscillator.js"; import { getContext } from "../Global.js"; import { DestinationInstance } from "./Destination.js"; describe("Destination", () => { it("creates itself on the context", () => { expect(getContext().destination).instanceOf(DestinationInstance); }); it("can be muted and unmuted", () => { return Offline((context) => { context.destination.mute = false; expect(context.destination.mute).to.equal(false); context.destination.mute = true; expect(context.destination.mute).to.equal(true); }); }); it("passes audio through", () => { return PassAudio((input) => { input.toDestination(); }); }); it("passes no audio when muted", async () => { const buffer = await Offline((context) => { new Oscillator().toDestination().start(0); context.destination.mute = true; }); expect(buffer.isSilent()).to.equal(true); }); it("has a master volume control", () => { return Offline((context) => { context.destination.volume.value = -20; expect(context.destination.volume.value).to.be.closeTo(-20, 0.1); }); }); it("warns when toMaster is called", () => { warns(() => { const osc = new Oscillator().toMaster(); osc.dispose(); }); }); it("can get the maxChannelCount", () => { return Offline( (context) => { expect(context.destination.maxChannelCount).to.equal(4); }, 0.1, 4 ); }); it.skip("can set the audio channel configuration", () => { return Offline( (context) => { expect(context.destination.channelCount).to.equal(4); context.destination.channelCountMode = "explicit"; context.destination.channelInterpretation = "discrete"; expect(context.destination.channelCountMode).to.equal( "explicit" ); expect(context.destination.channelInterpretation).to.equal( "discrete" ); }, 0.1, 4 ); }); it("can pass audio through chained nodes", () => { return PassAudio((input) => { const gain = input.context.createGain(); input.connect(gain); input.context.destination.chain(gain); }); }); }); ================================================ FILE: Tone/core/context/Destination.ts ================================================ import { Volume } from "../../component/channel/Volume.js"; import { Decibels } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { onContextClose, onContextInit } from "./ContextInitialization.js"; import { Gain } from "./Gain.js"; import { Param } from "./Param.js"; import { connectSeries, ToneAudioNode, ToneAudioNodeOptions, } from "./ToneAudioNode.js"; interface DestinationOptions extends ToneAudioNodeOptions { volume: Decibels; mute: boolean; } /** * A single master output that is connected to the * AudioDestinationNode (i.e. your speakers). * * It provides useful conveniences, such as the ability * to set the volume and mute the entire application. * * It also gives you the ability to apply master effects to your application. * * @example * const oscillator = new Tone.Oscillator().start(); * // The audio will go from the oscillator to the speakers. * oscillator.connect(Tone.getDestination()); * // A convenience for connecting to the master output is also provided: * oscillator.toDestination(); * @category Core */ export class DestinationInstance extends ToneAudioNode { readonly name: string = "Destination"; input: Volume = new Volume({ context: this.context }); output: Gain = new Gain({ context: this.context }); /** * The volume of the master output in decibels. * * -Infinity is silent, and 0 is no change. * * @example * const osc = new Tone.Oscillator().toDestination(); * osc.start(); * // Ramp the volume down to silent over 10 seconds. * Tone.getDestination().volume.rampTo(-Infinity, 10); */ volume: Param<"decibels"> = this.input.volume; constructor(options: Partial); constructor() { const options = optionsFromArguments( DestinationInstance.getDefaults(), arguments ); super(options); connectSeries( this.input, this.output, this.context.rawContext.destination ); this.mute = options.mute; this._internalChannels = [ this.input, this.context.rawContext.destination, this.output, ]; } static getDefaults(): DestinationOptions { return Object.assign(ToneAudioNode.getDefaults(), { mute: false, volume: 0, }); } /** * Mute the output. * @example * const oscillator = new Tone.Oscillator().start().toDestination(); * setTimeout(() => { * // Mute the output. * Tone.Destination.mute = true; * }, 1000); */ get mute(): boolean { return this.input.mute; } set mute(mute: boolean) { this.input.mute = mute; } /** * Add a master effects chain. * * NOTE: This will disconnect any nodes that were previously chained in the master effects chain. * * @param args All arguments will be connected in a row, and the master output will be routed through it. * @example * // Route all audio through a filter and compressor. * const lowpass = new Tone.Filter(800, "lowpass"); * const compressor = new Tone.Compressor(-18); * Tone.Destination.chain(lowpass, compressor); */ chain(...args: Array): this { this.input.disconnect(); args.unshift(this.input); args.push(this.output); connectSeries(...args); return this; } /** * The maximum number of channels the system can output. * @example * console.log(Tone.Destination.maxChannelCount); */ get maxChannelCount(): number { return this.context.rawContext.destination.maxChannelCount; } /** * Clean up */ dispose(): this { super.dispose(); this.volume.dispose(); return this; } } //------------------------------------- // INITIALIZATION //------------------------------------- onContextInit((context) => { context.destination = new DestinationInstance({ context }); }); onContextClose((context) => { context.destination.dispose(); }); ================================================ FILE: Tone/core/context/DummyContext.test.ts ================================================ import { DummyContext } from "./DummyContext.js"; describe("DummyContext", () => { it("has all the methods and members", () => { const context = new DummyContext(); context.createAnalyser(); context.createOscillator(); context.createBufferSource(); context.createBiquadFilter(); context.createBuffer(2, 1024, 44100); context.createChannelMerger(); context.createChannelSplitter(); context.createConstantSource(); context.createConvolver(); context.createDelay(); context.createDynamicsCompressor(); context.createGain(); context.createIIRFilter([1, 1, 1], [1, 1, 1]); context.createPanner(); context.createPeriodicWave([1, 1, 1], [1, 1, 1]); context.createStereoPanner(); context.createWaveShaper(); // @ts-ignore context.createMediaStreamSource(); context.decodeAudioData(new Float32Array(100)); context.createAudioWorkletNode("test.js"); context.rawContext; context.addAudioWorkletModule("test.js"); context.resume(); context.setTimeout(() => {}, 1); context.clearTimeout(1); context.setInterval(() => {}, 1); context.clearInterval(1); context.getConstant(1); context.currentTime; context.state; context.sampleRate; context.listener; context.transport; context.draw; context.draw; context.destination; context.now(); context.immediate(); }); }); ================================================ FILE: Tone/core/context/DummyContext.ts ================================================ import type { TransportInstance as Transport } from "../clock/Transport.js"; import { Seconds } from "../type/Units.js"; import type { DrawInstance as Draw } from "../util/Draw.js"; import { AnyAudioContext } from "./AudioContext.js"; import { BaseContext } from "./BaseContext.js"; import type { DestinationInstance as Destination } from "./Destination.js"; import type { ListenerInstance as Listener } from "./Listener.js"; export class DummyContext extends BaseContext { //--------------------------- // BASE AUDIO CONTEXT METHODS //--------------------------- createAnalyser(): AnalyserNode { return {} as AnalyserNode; } createOscillator(): OscillatorNode { return {} as OscillatorNode; } createBufferSource() { return {} as AudioBufferSourceNode; } createBiquadFilter(): BiquadFilterNode { return {} as BiquadFilterNode; } createBuffer( _numberOfChannels: number, _length: number, _sampleRate: number ): AudioBuffer { return {} as AudioBuffer; } createChannelMerger( _numberOfInputs?: number | undefined ): ChannelMergerNode { return {} as ChannelMergerNode; } createChannelSplitter( _numberOfOutputs?: number | undefined ): ChannelSplitterNode { return {} as ChannelSplitterNode; } createConstantSource(): ConstantSourceNode { return {} as ConstantSourceNode; } createConvolver(): ConvolverNode { return {} as ConvolverNode; } createDelay(_maxDelayTime?: number | undefined): DelayNode { return {} as DelayNode; } createDynamicsCompressor(): DynamicsCompressorNode { return {} as DynamicsCompressorNode; } createGain(): GainNode { return {} as GainNode; } createIIRFilter( _feedForward: number[] | Float32Array, _feedback: number[] | Float32Array ): IIRFilterNode { return {} as IIRFilterNode; } createPanner(): PannerNode { return {} as PannerNode; } createPeriodicWave( _real: number[] | Float32Array, _imag: number[] | Float32Array, _constraints?: PeriodicWaveConstraints | undefined ): PeriodicWave { return {} as PeriodicWave; } createStereoPanner(): StereoPannerNode { return {} as StereoPannerNode; } createWaveShaper(): WaveShaperNode { return {} as WaveShaperNode; } createMediaStreamSource(_stream: MediaStream): MediaStreamAudioSourceNode { return {} as MediaStreamAudioSourceNode; } createMediaElementSource( _element: HTMLMediaElement ): MediaElementAudioSourceNode { return {} as MediaElementAudioSourceNode; } createMediaStreamDestination(): MediaStreamAudioDestinationNode { return {} as MediaStreamAudioDestinationNode; } decodeAudioData(_audioData: ArrayBuffer): Promise { return Promise.resolve({} as AudioBuffer); } //--------------------------- // TONE AUDIO CONTEXT METHODS //--------------------------- createAudioWorkletNode( _name: string, _options?: Partial ): AudioWorkletNode { return {} as AudioWorkletNode; } get rawContext(): AnyAudioContext { return {} as AnyAudioContext; } async addAudioWorkletModule(_url: string): Promise { return Promise.resolve(); } lookAhead = 0; latencyHint = 0; resume(): Promise { return Promise.resolve(); } setTimeout(_fn: (...args: any[]) => void, _timeout: Seconds): number { return 0; } clearTimeout(_id: number): this { return this; } setInterval(_fn: (...args: any[]) => void, _interval: Seconds): number { return 0; } clearInterval(_id: number): this { return this; } getConstant(_val: number): AudioBufferSourceNode { return {} as AudioBufferSourceNode; } get currentTime(): Seconds { return 0; } get state(): AudioContextState { return {} as AudioContextState; } get sampleRate(): number { return 0; } get listener(): Listener { return {} as Listener; } get transport(): Transport { return {} as Transport; } get draw(): Draw { return {} as Draw; } set draw(_d) {} get destination(): Destination { return {} as Destination; } set destination(_d: Destination) {} now() { return 0; } immediate() { return 0; } readonly isOffline: boolean = false; } ================================================ FILE: Tone/core/context/Gain.test.ts ================================================ import { expect } from "chai"; import { BasicTests } from "../../../test/helper/Basic.js"; import { connectFrom, connectTo } from "../../../test/helper/Connect.js"; import { PassAudio } from "../../../test/helper/PassAudio.js"; import { Gain } from "./Gain.js"; describe("Gain", () => { BasicTests(Gain); it("can be created and disposed", () => { const gainNode = new Gain(); gainNode.dispose(); }); it("handles input and output connections", () => { const gainNode = new Gain(); gainNode.connect(connectTo()); connectFrom().connect(gainNode); connectFrom().connect(gainNode.gain); gainNode.dispose(); }); it("can set the gain value", () => { const gainNode = new Gain(); expect(gainNode.gain.value).to.be.closeTo(1, 0.001); gainNode.gain.value = 0.2; expect(gainNode.gain.value).to.be.closeTo(0.2, 0.001); gainNode.dispose(); }); it("can be constructed with options object", () => { const gainNode = new Gain({ gain: 0.4, }); expect(gainNode.gain.value).to.be.closeTo(0.4, 0.001); gainNode.dispose(); }); it("can be constructed with an initial value", () => { const gainNode = new Gain(3); expect(gainNode.gain.value).to.be.closeTo(3, 0.001); gainNode.dispose(); }); it("can set the units", () => { const gainNode = new Gain(0, "decibels"); expect(gainNode.gain.value).to.be.closeTo(0, 0.001); expect(gainNode.gain.units).to.equal("decibels"); gainNode.dispose(); }); it("can get the value using 'get'", () => { const gainNode = new Gain(5); const value = gainNode.get(); expect(value.gain).to.be.closeTo(5, 0.001); gainNode.dispose(); }); it("can set the value using 'set'", () => { const gainNode = new Gain(5); gainNode.set({ gain: 4, }); expect(gainNode.gain.value).to.be.closeTo(4, 0.001); gainNode.dispose(); }); it("passes audio through", () => { return PassAudio((input) => { const gainNode = new Gain().toDestination(); input.connect(gainNode); }); }); }); ================================================ FILE: Tone/core/context/Gain.ts ================================================ import { Param } from "../context/Param.js"; import { UnitMap, UnitName } from "../type/Units.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { readOnly } from "../util/Interface.js"; import { ToneAudioNode, ToneAudioNodeOptions } from "./ToneAudioNode.js"; interface GainOptions extends ToneAudioNodeOptions { gain: UnitMap[TypeName]; units: TypeName; convert: boolean; minValue?: number; maxValue?: number; } /** * A thin wrapper around the Native Web Audio GainNode. * The GainNode is a basic building block of the Web Audio * API and is useful for routing audio and adjusting gains. * @category Core * @example * return Tone.Offline(() => { * const gainNode = new Tone.Gain(0).toDestination(); * const osc = new Tone.Oscillator(30).connect(gainNode).start(); * gainNode.gain.rampTo(1, 0.1); * gainNode.gain.rampTo(0, 0.4, 0.2); * }, 0.7, 1); */ export class Gain< TypeName extends "gain" | "decibels" | "normalRange" = "gain", > extends ToneAudioNode> { readonly name: string = "Gain"; /** * The gain parameter of the gain node. * @example * const gainNode = new Tone.Gain(0).toDestination(); * const osc = new Tone.Oscillator().connect(gainNode).start(); * gainNode.gain.rampTo(1, 0.1); * gainNode.gain.rampTo(0, 2, "+0.5"); */ readonly gain: Param; /** * The wrapped GainNode. */ private _gainNode: GainNode = this.context.createGain(); // input = output readonly input: GainNode = this._gainNode; readonly output: GainNode = this._gainNode; /** * @param gain The initial gain of the GainNode * @param units The units of the gain parameter. */ constructor(gain?: UnitMap[TypeName], units?: TypeName); constructor(options?: Partial>); constructor() { const options = optionsFromArguments(Gain.getDefaults(), arguments, [ "gain", "units", ]); super(options); this.gain = new Param({ context: this.context, convert: options.convert, param: this._gainNode.gain, units: options.units, value: options.gain, minValue: options.minValue, maxValue: options.maxValue, }); readOnly(this, "gain"); } static getDefaults(): GainOptions { return Object.assign(ToneAudioNode.getDefaults(), { convert: true, gain: 1, units: "gain", }); } /** * Clean up. */ dispose(): this { super.dispose(); this._gainNode.disconnect(); this.gain.dispose(); return this; } } ================================================ FILE: Tone/core/context/Listener.test.ts ================================================ import { expect } from "chai"; import { Offline } from "../../../test/helper/Offline.js"; import { getContext } from "../Global.js"; import { ListenerInstance } from "./Listener.js"; describe("Listener", () => { it("creates itself on the context", () => { expect(getContext().listener).instanceOf(ListenerInstance); }); it("can get and set values as an object", () => { // can get and set some values Offline(({ listener }) => { expect(listener.get()).to.have.property("positionX"); expect(listener.get()).to.have.property("positionY"); expect(listener.get()).to.have.property("positionZ"); expect(listener.get()).to.have.property("forwardZ"); expect(listener.get()).to.have.property("upY"); }); }); }); ================================================ FILE: Tone/core/context/Listener.ts ================================================ import { onContextClose, onContextInit } from "./ContextInitialization.js"; import { Param } from "./Param.js"; import { ToneAudioNode, ToneAudioNodeOptions } from "./ToneAudioNode.js"; export interface ListenerOptions extends ToneAudioNodeOptions { positionX: number; positionY: number; positionZ: number; forwardX: number; forwardY: number; forwardZ: number; upX: number; upY: number; upZ: number; } /** * Tone.Listener is a thin wrapper around the AudioListener. Listener combined * with {@link Panner3D} makes up the Web Audio API's 3D panning system. Panner3D allows you * to place sounds in 3D and Listener allows you to navigate the 3D sound environment from * a first-person perspective. There is only one listener per audio context. * @category Core */ export class ListenerInstance extends ToneAudioNode { readonly name: string = "Listener"; /** * The listener has no inputs or outputs. */ output: undefined; input: undefined; readonly positionX: Param = new Param({ context: this.context, param: this.context.rawContext.listener.positionX, }); readonly positionY: Param = new Param({ context: this.context, param: this.context.rawContext.listener.positionY, }); readonly positionZ: Param = new Param({ context: this.context, param: this.context.rawContext.listener.positionZ, }); readonly forwardX: Param = new Param({ context: this.context, param: this.context.rawContext.listener.forwardX, }); readonly forwardY: Param = new Param({ context: this.context, param: this.context.rawContext.listener.forwardY, }); readonly forwardZ: Param = new Param({ context: this.context, param: this.context.rawContext.listener.forwardZ, }); readonly upX: Param = new Param({ context: this.context, param: this.context.rawContext.listener.upX, }); readonly upY: Param = new Param({ context: this.context, param: this.context.rawContext.listener.upY, }); readonly upZ: Param = new Param({ context: this.context, param: this.context.rawContext.listener.upZ, }); static getDefaults(): ListenerOptions { return Object.assign(ToneAudioNode.getDefaults(), { positionX: 0, positionY: 0, positionZ: 0, forwardX: 0, forwardY: 0, forwardZ: -1, upX: 0, upY: 1, upZ: 0, }); } dispose(): this { super.dispose(); this.positionX.dispose(); this.positionY.dispose(); this.positionZ.dispose(); this.forwardX.dispose(); this.forwardY.dispose(); this.forwardZ.dispose(); this.upX.dispose(); this.upY.dispose(); this.upZ.dispose(); return this; } } //------------------------------------- // INITIALIZATION //------------------------------------- onContextInit((context) => { context.listener = new ListenerInstance({ context }); }); onContextClose((context) => { context.listener.dispose(); }); ================================================ FILE: Tone/core/context/Offline.test.ts ================================================ import { expect } from "chai"; import { TestAudioBuffer } from "../../../test/helper/compare/TestAudioBuffer.js"; import { ToneOscillatorNode } from "../../source/oscillator/ToneOscillatorNode.js"; import { noOp } from "../util/Interface.js"; import { Offline } from "./Offline.js"; import { ToneAudioBuffer } from "./ToneAudioBuffer.js"; describe("Offline", () => { it("accepts a callback and a duration", () => { return Offline(noOp, 0.01); }); it("returns a promise", () => { const ret = Offline(noOp, 0.01); expect(ret).to.have.property("then"); return ret; }); it("generates a buffer", async () => { const buffer = await Offline(noOp, 0.01); expect(buffer).to.be.instanceOf(ToneAudioBuffer); }); it("silent by default", async () => { const buffer = await Offline(noOp, 0.01, 1); const isSilent = buffer.toArray().every((sample) => sample === 0); expect(isSilent).to.equal(true); }); it("records the master output", async () => { const buffer = await Offline(() => { new ToneOscillatorNode().toDestination().start(); }, 0.01); const testBuff = new TestAudioBuffer(buffer.get() as AudioBuffer); expect(testBuff.isSilent()).is.equal(false); }); it("returning a promise defers the rendering till the promise resolves", async () => { let wasInvoked = false; const buffer = await Offline(() => { new ToneOscillatorNode().toDestination().start(); return new Promise((done) => { setTimeout(done, 100); }).then(() => { wasInvoked = true; }); }, 0.01); const testBuff = new TestAudioBuffer(buffer.get() as AudioBuffer); expect(wasInvoked).is.equal(true); expect(testBuff.isSilent()).to.equal(false); }); it("can schedule specific timing outputs", async () => { const buffer = await Offline(() => { new ToneOscillatorNode().toDestination().start(0.05); }, 0.1); const testBuff = new TestAudioBuffer(buffer.get() as AudioBuffer); expect(testBuff.getTimeOfFirstSound()).to.be.closeTo(0.05, 0.0001); }); }); ================================================ FILE: Tone/core/context/Offline.ts ================================================ import "./Destination.js"; import "./Listener.js"; import { getContext, setContext } from "../Global.js"; import { Seconds } from "../type/Units.js"; import { OfflineContext } from "./OfflineContext.js"; import { ToneAudioBuffer } from "./ToneAudioBuffer.js"; /** * Generate a buffer by rendering all of the Tone.js code within the callback using the OfflineAudioContext. * The OfflineAudioContext is capable of rendering much faster than real time in many cases. * The callback function also passes in an offline instance of {@link Context} which can be used * to schedule events along the Transport. * @param callback All Tone.js nodes which are created and scheduled within this callback are recorded into the output Buffer. * @param duration the amount of time to record for. * @return The promise which is invoked with the ToneAudioBuffer of the recorded output. * @example * // render 2 seconds of the oscillator * Tone.Offline(() => { * // only nodes created in this callback will be recorded * const oscillator = new Tone.Oscillator().toDestination().start(0); * }, 2).then((buffer) => { * // do something with the output buffer * console.log(buffer); * }); * @example * // can also schedule events along the Transport * // using the passed in Offline Transport * Tone.Offline(({ transport }) => { * const osc = new Tone.Oscillator().toDestination(); * transport.schedule(time => { * osc.start(time).stop(time + 0.1); * }, 1); * // make sure to start the transport * transport.start(0.2); * }, 4).then((buffer) => { * // do something with the output buffer * console.log(buffer); * }); * @category Core */ export async function Offline( callback: (context: OfflineContext) => Promise | void, duration: Seconds, channels = 2, sampleRate: number = getContext().sampleRate ): Promise { // set the OfflineAudioContext based on the current context const originalContext = getContext(); const context = new OfflineContext(channels, duration, sampleRate); setContext(context); // invoke the callback/scheduling await callback(context); // then render the audio const bufferPromise = context.render(); // return the original AudioContext setContext(originalContext); // await the rendering const buffer = await bufferPromise; // return the audio return new ToneAudioBuffer(buffer); } ================================================ FILE: Tone/core/context/OfflineContext.test.ts ================================================ import { expect } from "chai"; import { OfflineContext } from "./OfflineContext.js"; context("OfflineContext", () => { it("can be created an disposed", () => { const ctx = new OfflineContext(1, 0.1, 44100); ctx.dispose(); }); it("is setup with 0 lookAhead and offline clockSource", () => { const ctx = new OfflineContext(1, 0.1, 44100); expect(ctx.lookAhead).to.equal(0); expect(ctx.clockSource).to.equal("offline"); return ctx.dispose(); }); it("now = currentTime", () => { const ctx = new OfflineContext(1, 0.1, 44100); expect(ctx.currentTime).to.equal(ctx.now()); return ctx.dispose(); }); it("closing shouldn't do anything", () => { const ctx = new OfflineContext(1, 0.1, 44100); return ctx.close(); }); it("can render audio", async () => { const ctx = new OfflineContext(1, 0.2, 44100); const osc = ctx.createOscillator(); osc.connect(ctx.rawContext.destination); osc.start(0.1); const buffer = await ctx.render(); expect(buffer).to.have.property("length"); expect(buffer).to.have.property("sampleRate"); const array = buffer.getChannelData(0); for (let i = 0; i < array.length; i++) { if (array[i] !== 0) { expect(i / array.length).to.be.closeTo(0.5, 0.01); break; } } }); it("can render audio not async", async () => { const ctx = new OfflineContext(1, 0.2, 44100); const osc = ctx.createOscillator(); osc.connect(ctx.rawContext.destination); osc.start(0.1); const buffer = await ctx.render(false); expect(buffer).to.have.property("length"); expect(buffer).to.have.property("sampleRate"); const array = buffer.getChannelData(0); for (let i = 0; i < array.length; i++) { if (array[i] !== 0) { expect(i / array.length).to.be.closeTo(0.5, 0.01); break; } } }); }); ================================================ FILE: Tone/core/context/OfflineContext.ts ================================================ import { createOfflineAudioContext } from "../context/AudioContext.js"; import { Context } from "../context/Context.js"; import { Seconds } from "../type/Units.js"; import { isOfflineAudioContext } from "../util/AdvancedTypeCheck.js"; import { ToneAudioBuffer } from "./ToneAudioBuffer.js"; /** * Wrapper around the OfflineAudioContext * @category Core * @example * // generate a single channel, 0.5 second buffer * const context = new Tone.OfflineContext(1, 0.5, 44100); * const osc = new Tone.Oscillator({ context }); * context.render().then(buffer => { * console.log(buffer.numberOfChannels, buffer.duration); * }); */ export class OfflineContext extends Context { readonly name: string = "OfflineContext"; /** * A private reference to the duration */ private readonly _duration: Seconds; /** * An artificial clock source */ private _currentTime: Seconds = 0; /** * Private reference to the OfflineAudioContext. */ protected _context!: OfflineAudioContext; readonly isOffline: boolean = true; /** * @param channels The number of channels to render * @param duration The duration to render in seconds * @param sampleRate the sample rate to render at */ constructor(channels: number, duration: Seconds, sampleRate: number); constructor(context: OfflineAudioContext); constructor() { super({ clockSource: "offline", context: isOfflineAudioContext(arguments[0]) ? arguments[0] : createOfflineAudioContext( arguments[0], arguments[1] * arguments[2], arguments[2] ), lookAhead: 0, updateInterval: isOfflineAudioContext(arguments[0]) ? 128 / arguments[0].sampleRate : 128 / arguments[2], }); this._duration = isOfflineAudioContext(arguments[0]) ? arguments[0].length / arguments[0].sampleRate : arguments[1]; } /** * Override the now method to point to the internal clock time */ now(): Seconds { return this._currentTime; } /** * Same as this.now() */ get currentTime(): Seconds { return this._currentTime; } /** * Render just the clock portion of the audio context. */ private async _renderClock(asynchronous: boolean): Promise { let index = 0; while (this._duration - this._currentTime >= 0) { // invoke all the callbacks on that time this.emit("tick"); // increment the clock in block-sized chunks this._currentTime += 128 / this.sampleRate; // yield once a second of audio index++; const yieldEvery = Math.floor(this.sampleRate / 128); if (asynchronous && index % yieldEvery === 0) { await new Promise((done) => setTimeout(done, 1)); } } } /** * Render the output of the OfflineContext * @param asynchronous If the clock should be rendered asynchronously, which will not block the main thread, but be slightly slower. */ async render(asynchronous = true): Promise { await this.workletsAreReady(); await this._renderClock(asynchronous); const buffer = await this._context.startRendering(); return new ToneAudioBuffer(buffer); } /** * Close the context */ close(): Promise { return Promise.resolve(); } } ================================================ FILE: Tone/core/context/OnRunning.test.ts ================================================ import { expect } from "chai"; import { Context } from "./Context.js"; import { OfflineContext } from "./OfflineContext.js"; import { onContextRunning } from "./OnRunning.js"; context("onContextRunning", () => { it("callback is invoked immediately when offline context is used", () => { const ctx = new OfflineContext(1, 0.1, 44100); let wasInvoked = false; onContextRunning(ctx, () => { wasInvoked = true; }); expect(wasInvoked).to.be.true; ctx.dispose(); }); it("callback is invoked immediately when context is already running", async () => { const ctx = new Context(); await ctx.resume(); let wasInvoked = false; onContextRunning(ctx, () => { wasInvoked = true; }); expect(wasInvoked).to.be.true; ctx.dispose(); }); it("callback is invoked when the context starts running", async () => { const ctx = new Context(); let wasInvoked = false; onContextRunning(ctx, () => { wasInvoked = true; }); expect(wasInvoked).to.be.false; await ctx.resume(); expect(wasInvoked).to.be.true; ctx.dispose(); }); it("can remove the callback with the returned function", async () => { const ctx = new Context(); let wasInvoked = false; const remove = onContextRunning(ctx, () => { wasInvoked = true; }); expect(wasInvoked).to.be.false; remove(); await ctx.resume(); expect(wasInvoked).to.be.false; ctx.dispose(); }); }); ================================================ FILE: Tone/core/context/OnRunning.ts ================================================ import { BaseContext } from "./BaseContext.js"; /** * Invoked when the context is started. The callback is invoked immediately * if the context is already started or when the context is an OfflineContext. * Returns a function which can be called to remove the listener. * @internal Used to trigger certain events when the context has been started. */ export function onContextRunning( context: BaseContext, callback: () => void ): () => void { const listener = (state: AudioContextState) => { if (state === "running") { callback(); context.off("statechange", listener); } }; if (context.isOffline || context.state === "running") { callback(); return () => {}; } context.on("statechange", listener); return () => context.off("statechange", listener); } ================================================ FILE: Tone/core/context/Param.test.ts ================================================ import { expect } from "chai"; import { BasicTests, testAudioContext } from "../../../test/helper/Basic.js"; import { Plot } from "../../../test/helper/compare/index.js"; import { atTime, Offline } from "../../../test/helper/Offline.js"; import { Signal } from "../../signal/Signal.js"; import { getContext } from "../Global.js"; import { UnitName } from "../type/Units.js"; import { Gain } from "./Gain.js"; import { Param } from "./Param.js"; import { connect } from "./ToneAudioNode.js"; const audioContext = getContext(); describe("Param", () => { BasicTests(Param, { context: testAudioContext, param: testAudioContext.createOscillator().frequency, }); context("constructor", () => { it("can be created and disposed", async () => { await Offline((context) => { const param = new Param<"time">({ context, param: context.createConstantSource().offset, units: "time", }); expect(param.getValueAtTime(0)).to.equal(1); param.dispose(); }); }); it("can pass in a value", async () => { await Offline((context) => { const param = new Param({ context, param: context.createConstantSource().offset, value: 1.1, }); expect(param.getValueAtTime(0)).to.equal(1.1); param.dispose(); }); }); it("requires a param in the constructor", () => { expect(() => { const param = new Param({ value: 1.1, }); }).throws(Error); }); }); context("Scheduling Curves", () => { const sampleRate = 11025; function matchesOutputCurve(param, outBuffer): void { outBuffer.toArray()[0].forEach((sample, index) => { try { expect( param.getValueAtTime(index / sampleRate) ).to.be.closeTo(sample, 0.1); } catch (e) { throw e; } }); } it("correctly handles setTargetAtTime followed by a ramp", async () => { let param; // this fails on FF const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); param = new Param({ context, param: source.offset, }); param.setTargetAtTime(2, 0.5, 0.1); expect(param.getValueAtTime(0.6)).to.be.closeTo(1.6, 0.1); param.linearRampToValueAtTime(0.5, 0.7); expect(param.getValueAtTime(0.6)).to.be.closeTo(0.75, 0.1); }, 1.5, 1, sampleRate ); document.body.appendChild(await Plot.signal(testBuffer)); matchesOutputCurve(param, testBuffer); }); it("schedules a value curve", async () => { let param; const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); param = new Param({ context, param: source.offset, units: "number", value: 0, }); param.setValueCurveAtTime( [0, 0.5, 0, 1, 1.5], 0.1, 0.8, 0.5 ); expect(param.getValueAtTime(0.91)).to.be.closeTo( 0.75, 0.01 ); }, 1, 1, sampleRate ); matchesOutputCurve(param, testBuffer); }); it("a mixture of scheduling curves", async () => { let param; const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); param = new Param({ context, param: source.offset, value: 0.1, }); param.setValueAtTime(0, 0); param.setValueAtTime(1, 0.1); param.linearRampToValueAtTime(3, 0.2); param.exponentialRampToValueAtTime(0.01, 0.3); param.setTargetAtTime(-1, 0.35, 0.2); param.cancelAndHoldAtTime(0.6); param.rampTo(1.1, 0.2, 0.7); param.exponentialRampTo(0, 0.1, 0.85); param.setValueAtTime(0, 1); param.linearRampTo(1, 0.2, 1); param.targetRampTo(0, 0.1, 1.1); param.setValueAtTime(4, 1.2); param.cancelScheduledValues(1.2); param.linearRampToValueAtTime(1, 1.3); }, 1.5, 1, sampleRate ); matchesOutputCurve(param, testBuffer); }); it.skip("can cancel and hold", async () => { let param; const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); param = new Param({ context, param: source.offset, value: 0.1, }); param.setValueAtTime(0, 0); param.setValueAtTime(1, 0.2); param.cancelAndHoldAtTime(0.1); param.linearRampToValueAtTime(1, 0.3); param.cancelAndHoldAtTime(0.2); expect(param.getValueAtTime(0.2)).to.be.closeTo(0.5, 0.001); param.exponentialRampToValueAtTime(0, 0.4); param.cancelAndHoldAtTime(0.25); expect(param.getValueAtTime(0.25)).to.be.closeTo( 0.033, 0.001 ); param.setTargetAtTime(1, 0.3, 0.1); param.cancelAndHoldAtTime(0.4); expect(param.getValueAtTime(0.4)).to.be.closeTo( 0.644, 0.001 ); param.setValueAtTime(0, 0.45); param.setValueAtTime(1, 0.48); param.cancelAndHoldAtTime(0.45); expect(param.getValueAtTime(0.45)).to.be.closeTo(0, 0.001); }, 0.5, 1, sampleRate ); matchesOutputCurve(param, testBuffer); }); }); context("Units", () => { it("throws an error with invalid values", () => { const osc = audioContext.createOscillator(); const param = new Param<"frequency">({ context: audioContext, param: osc.frequency, units: "frequency", }); expect(() => { // @ts-ignore expect(param.setValueAtTime("bad", "bad")); }).to.throw(Error); expect(() => { // @ts-ignore expect(param.linearRampToValueAtTime("bad", "bad")); }).to.throw(Error); expect(() => { // @ts-ignore expect(param.exponentialRampToValueAtTime("bad", "bad")); }).to.throw(Error); expect(() => { // @ts-ignore expect(param.setTargetAtTime("bad", "bad", 0.1)); }).to.throw(Error); expect(() => { // @ts-ignore expect(param.cancelScheduledValues("bad")); }).to.throw(Error); param.dispose(); }); it("can be created with specific units", () => { const gain = audioContext.createGain(); const param = new Param<"bpm">({ context: audioContext, param: gain.gain, units: "bpm", }); expect(param.units).to.equal("bpm"); param.dispose(); }); it("can evaluate the given units", () => { const gain = audioContext.createGain(); const param = new Param<"decibels">({ context: audioContext, param: gain.gain, units: "decibels", }); param.value = 0.5; expect(param.value).to.be.closeTo(0.5, 0.001); param.dispose(); }); it("can be forced to not convert", async () => { const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); const param = new Param({ context, convert: false, param: source.offset, units: "decibels", }); param.value = -10; expect(param.value).to.be.closeTo(-10, 0.01); }, 0.001, 1 ); expect(testBuffer.getValueAtTime(0)).to.be.closeTo(-10, 0.01); }); }); context("apply", () => { it("can apply a scheduled curve", async () => { let sig; const buffer = await Offline((context) => { const signal = new Signal(); sig = signal; signal.setValueAtTime(0, 0); signal.linearRampToValueAtTime(0.5, 0.1); signal.exponentialRampToValueAtTime(0.2, 0.5); signal.linearRampToValueAtTime(4, 2); signal.cancelScheduledValues(1); signal.setTargetAtTime(4, 1, 0.1); const source = context.createConstantSource(); source.start(0); connect(source, context.destination); return atTime(0.4, () => { signal.apply(source.offset); }); }, 2); for (let time = 0.41; time < 2; time += 0.1) { expect(buffer.getValueAtTime(time)).to.be.closeTo( sig.getValueAtTime(time), 0.01 ); } }); it("can apply a scheduled curve that starts with a setTargetAtTime", async () => { let sig; const buffer = await Offline((context) => { const signal = new Signal(); sig = signal; signal.setTargetAtTime(2, 0, 0.2); const source = context.createConstantSource(); source.start(0); connect(source, context.destination); return atTime(0.4, () => { signal.apply(source.offset); }); }, 2); for (let time = 0.41; time < 2; time += 0.1) { expect(buffer.getValueAtTime(time)).to.be.closeTo( sig.getValueAtTime(time), 0.05 ); } }); it("can apply a scheduled curve that starts with a setTargetAtTime and then schedules other things", async () => { let sig; const buffer = await Offline((context) => { const signal = new Signal(); sig = signal; signal.setTargetAtTime(2, 0, 0.2); signal.setValueAtTime(1, 0.8); signal.linearRampToValueAtTime(0, 2); const source = context.createConstantSource(); source.start(0); connect(source, context.destination); return atTime(0.4, () => { signal.apply(source.offset); }); }, 2); for (let time = 0.41; time < 2; time += 0.1) { expect(buffer.getValueAtTime(time)).to.be.closeTo( sig.getValueAtTime(time), 0.05 ); } }); it("can set the param if the Param is marked as swappable", async () => { const buffer = await Offline((context) => { const constSource = context.createConstantSource(); const param = new Param({ swappable: true, param: constSource.offset, }); param.setValueAtTime(0.1, 0.1); param.setValueAtTime(0.2, 0.2); param.setValueAtTime(0.3, 0.3); const constSource2 = context.createConstantSource(); constSource2.start(0); param.setParam(constSource2.offset); connect(constSource2, context.destination); }, 0.5); expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.1, 0.001); expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.2, 0.001); expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.3, 0.001); }); it("throws an error if the param is not set to swappable", () => { return Offline((context) => { const constSource = context.createConstantSource(); const param = new Param({ param: constSource.offset, }); const constSource2 = context.createConstantSource(); expect(() => { param.setParam(constSource2.offset); }).to.throw(Error); }, 0.5); }); }); context("Unit Conversions", () => { function testUnitConversion( units: UnitName, inputValue: any, inputVerification: number, outputValue: number ): void { it(`converts to ${units}`, async () => { const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); const param = new Param({ context, param: source.offset, units, }); param.value = inputValue; expect(param.value).to.be.closeTo( inputVerification, 0.01 ); }, 0.001, 1 ); expect(testBuffer.getValueAtTime(0)).to.be.closeTo( outputValue, 0.01 ); }); } testUnitConversion("number", 3, 3, 3); testUnitConversion("decibels", -10, -10, 0.31); testUnitConversion("decibels", -20, -20, 0.1); testUnitConversion("decibels", -100, -100, 0); testUnitConversion("gain", 1.2, 1.2, 1.2); testUnitConversion("positive", 1.5, 1.5, 1.5); testUnitConversion("positive", 0, 0, 0); testUnitConversion("time", 2, 2, 2); testUnitConversion("time", 0, 0, 0); testUnitConversion("frequency", 20, 20, 20); testUnitConversion("frequency", 0.1, 0.1, 0.1); testUnitConversion("normalRange", 0, 0, 0); testUnitConversion("normalRange", 0.5, 0.5, 0.5); testUnitConversion("audioRange", -1, -1, -1); testUnitConversion("audioRange", 0.5, 0.5, 0.5); testUnitConversion("audioRange", 1, 1, 1); }); context("min/maxValue", () => { function testMinMaxValue(units: UnitName, min, max): void { it(`has proper min/max for ${units}`, () => { const source = audioContext.createConstantSource(); source.connect(audioContext.rawContext.destination); const param = new Param({ context: audioContext, param: source.offset, units, }); expect(param.minValue).to.be.equal(min); expect(param.maxValue).to.be.equal(max); }); } // number, decibels, normalRange, audioRange, gain // positive, time, frequency, transportTime, ticks, bpm, degrees, samples, hertz const rangeMax = 3.4028234663852886e38; testMinMaxValue("number", -rangeMax, rangeMax); testMinMaxValue("decibels", -Infinity, rangeMax); testMinMaxValue("normalRange", 0, 1); testMinMaxValue("audioRange", -1, 1); testMinMaxValue("gain", -rangeMax, rangeMax); testMinMaxValue("positive", 0, rangeMax); testMinMaxValue("time", 0, rangeMax); testMinMaxValue("frequency", 0, rangeMax); testMinMaxValue("transportTime", 0, rangeMax); testMinMaxValue("ticks", 0, rangeMax); testMinMaxValue("bpm", 0, rangeMax); testMinMaxValue("degrees", -rangeMax, rangeMax); testMinMaxValue("samples", 0, rangeMax); testMinMaxValue("hertz", 0, rangeMax); it("can pass in a min and max value", () => { const source = audioContext.createConstantSource(); source.connect(audioContext.rawContext.destination); const param = new Param({ context: audioContext, param: source.offset, minValue: 0.3, maxValue: 0.5, }); expect(param.minValue).to.be.equal(0.3); expect(param.maxValue).to.be.equal(0.5); }); }); context("defaultValue", () => { it("has the right default value for default units", () => { const source = audioContext.createConstantSource(); source.connect(audioContext.rawContext.destination); const param = new Param({ context: audioContext, param: source.offset, }); expect(param.defaultValue).to.be.equal(1); }); it("has the right default value for default decibels", () => { const source = audioContext.createConstantSource(); source.connect(audioContext.rawContext.destination); const param = new Param({ context: audioContext, param: source.offset, units: "decibels", }); expect(param.defaultValue).to.be.equal(0); }); }); // const allSchedulingMethods = ['setValueAtTime', 'linearRampToValueAtTime', 'exponentialRampToValueAtTime'] context("setValueAtTime", () => { function testSetValueAtTime( units: UnitName, value0: number, value1: number, value2: number ): void { it(`can schedule value with units ${units}`, async () => { const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); const param = new Param({ context, param: source.offset, units, }); param.setValueAtTime(value0, 0); param.setValueAtTime(value1, 0.01); param.setValueAtTime(value2, 0.02); expect(param.getValueAtTime(0)).to.be.closeTo( value0, 0.01 ); expect(param.getValueAtTime(0.01)).to.be.closeTo( value1, 0.01 ); expect(param.getValueAtTime(0.02)).to.be.closeTo( value2, 0.01 ); }, 0.022, 1 ); expect(testBuffer.getValueAtTime(0)).to.be.closeTo(0, 0.01); expect(testBuffer.getValueAtTime(0.011)).to.be.closeTo(1, 0.01); expect(testBuffer.getValueAtTime(0.021)).to.be.closeTo( 0.5, 0.01 ); }); } const allUnits: UnitName[] = [ "number", "decibels", "normalRange", "audioRange", "gain", "positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz", ]; allUnits.forEach((unit) => { if (unit === "decibels") { testSetValueAtTime(unit, -100, 0, -6); } else { testSetValueAtTime(unit, 0, 1, 0.5); } }); it("asserts a value range", async () => { let errored = false; try { const source = new Gain(); const param = new Param({ param: source.gain, units: "normalRange", }); param.setValueAtTime(2, 0); } catch (e) { errored = true; } }); }); ["linearRampToValueAtTime", "exponentialRampToValueAtTime"].forEach( (method) => { context(method, () => { function testRampToValueAtTime( units: UnitName, value0: number, value1: number, value2: number ): void { it(`can schedule value with units ${units}`, async () => { const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); const param = new Param({ context, param: source.offset, units, }); param.setValueAtTime(value0, 0); param[method](value1, 0.01); param[method](value2, 0.02); expect(param.getValueAtTime(0)).to.be.closeTo( value0, 0.01 ); expect( param.getValueAtTime(0.01) ).to.be.closeTo(value1, 0.01); expect( param.getValueAtTime(0.02) ).to.be.closeTo(value2, 0.01); }, 0.022, 1 ); expect(testBuffer.getValueAtTime(0)).to.be.closeTo( 1, 0.01 ); expect(testBuffer.getValueAtTime(0.01)).to.be.closeTo( 0.7, 0.01 ); expect(testBuffer.getValueAtTime(0.02)).to.be.closeTo( 0, 0.01 ); }); } const allUnits: UnitName[] = [ "number", "decibels", "normalRange", "audioRange", "gain", "positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz", ]; allUnits.forEach((unit) => { if (unit === "decibels") { testRampToValueAtTime(unit, 0, -3, -100); } else { testRampToValueAtTime(unit, 1, 0.7, 0); } }); }); } ); ["linearRampTo", "exponentialRampTo", "rampTo", "targetRampTo"].forEach( (method) => { context(method, () => { function testRampToValueAtTime( units: UnitName, value0: number, value1: number, value2: number ): void { it(`can schedule value with units ${units}`, async () => { const testBuffer = await Offline( (context) => { const source = context.createConstantSource(); source.connect(context.rawContext.destination); source.start(0); const param = new Param({ context, param: source.offset, units, value: value0, }); param[method](value1, 0.009, 0); param[method](value2, 0.01, 0.01); expect(param.getValueAtTime(0)).to.be.closeTo( value0, 0.02 ); expect( param.getValueAtTime(0.01) ).to.be.closeTo(value1, 0.02); if (units !== "decibels") { expect( param.getValueAtTime(0.025) ).to.be.closeTo(value2, 0.01); } }, 0.021, 1 ); // document.body.appendChild(await Plot.signal(testBuffer)); expect(testBuffer.getValueAtTime(0)).to.be.closeTo( 1, 0.01 ); expect(testBuffer.getValueAtTime(0.01)).to.be.closeTo( 0.7, 0.01 ); expect(testBuffer.getValueAtTime(0.02)).to.be.closeTo( 0, 0.01 ); }); } const allUnits: UnitName[] = [ "number", "decibels", "normalRange", "audioRange", "gain", "positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz", ]; allUnits.forEach((unit) => { if (unit === "decibels") { testRampToValueAtTime(unit, 0, -3, -100); } else { testRampToValueAtTime(unit, 1, 0.7, 0); } }); }); } ); }); ================================================ FILE: Tone/core/context/Param.ts ================================================ import { AbstractParam } from "../context/AbstractParam.js"; import { dbToGain, gainToDb } from "../type/Conversions.js"; import { Decibels, Frequency, Positive, Time, UnitMap, UnitName, } from "../type/Units.js"; import { isAudioParam } from "../util/AdvancedTypeCheck.js"; import { assert, assertRange } from "../util/Debug.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { EQ } from "../util/Math.js"; import { Timeline } from "../util/Timeline.js"; import { isDefined } from "../util/TypeCheck.js"; import { ToneWithContext, ToneWithContextOptions } from "./ToneWithContext.js"; export interface ParamOptions extends ToneWithContextOptions { units: TypeName; value?: UnitMap[TypeName]; param: AudioParam | Param; convert: boolean; minValue?: number; maxValue?: number; swappable?: boolean; } /** * the possible automation types */ type AutomationType = | "linearRampToValueAtTime" | "exponentialRampToValueAtTime" | "setValueAtTime" | "setTargetAtTime" | "cancelScheduledValues"; interface TargetAutomationEvent { type: "setTargetAtTime"; time: number; value: number; constant: number; } interface NormalAutomationEvent { type: Exclude; time: number; value: number; } /** * The events on the automation */ export type AutomationEvent = NormalAutomationEvent | TargetAutomationEvent; /** * Param wraps the native Web Audio's AudioParam to provide * additional unit conversion functionality. It also * serves as a base-class for classes which have a single, * automatable parameter. * @category Core */ export class Param extends ToneWithContext> implements AbstractParam { readonly name: string = "Param"; readonly input: GainNode | AudioParam; readonly units: UnitName; convert: boolean; overridden = false; /** * The timeline which tracks all of the automations. */ protected _events: Timeline; /** * The native parameter to control */ protected _param: AudioParam; /** * The default value before anything is assigned */ protected _initialValue: number; /** * The minimum output value */ private _minOutput = 1e-7; /** * Private reference to the min and max values if passed into the constructor */ private readonly _minValue?: number; private readonly _maxValue?: number; /** * If the underlying AudioParam can be swapped out * using the setParam method. */ protected readonly _swappable: boolean; /** * @param param The AudioParam to wrap * @param units The unit name * @param convert Whether or not to convert the value to the target units */ constructor(param: AudioParam, units?: TypeName, convert?: boolean); constructor(options: Partial>); constructor() { const options = optionsFromArguments(Param.getDefaults(), arguments, [ "param", "units", "convert", ]); super(options); assert( isDefined(options.param) && (isAudioParam(options.param) || options.param instanceof Param), "param must be an AudioParam" ); while (!isAudioParam(options.param)) { options.param = options.param._param; } this._swappable = isDefined(options.swappable) ? options.swappable : false; if (this._swappable) { this.input = this.context.createGain(); // initialize this._param = options.param; this.input.connect(this._param); } else { this._param = this.input = options.param; } this._events = new Timeline(1000); this._initialValue = this._param.defaultValue; this.units = options.units; this.convert = options.convert; this._minValue = options.minValue; this._maxValue = options.maxValue; // if the value is defined, set it immediately if ( isDefined(options.value) && options.value !== this._toType(this._initialValue) ) { this.setValueAtTime(options.value, 0); } } static getDefaults(): ParamOptions { return Object.assign(ToneWithContext.getDefaults(), { convert: true, units: "number" as UnitName, } as ParamOptions); } get value(): UnitMap[TypeName] { const now = this.now(); return this.getValueAtTime(now); } set value(value) { this.cancelScheduledValues(this.now()); this.setValueAtTime(value, this.now()); } get minValue(): number { // if it's not the default minValue, return it if (isDefined(this._minValue)) { return this._minValue; } else if ( this.units === "time" || this.units === "frequency" || this.units === "normalRange" || this.units === "positive" || this.units === "transportTime" || this.units === "ticks" || this.units === "bpm" || this.units === "hertz" || this.units === "samples" ) { return 0; } else if (this.units === "audioRange") { return -1; } else if (this.units === "decibels") { return -Infinity; } else { return this._param.minValue; } } get maxValue(): number { if (isDefined(this._maxValue)) { return this._maxValue; } else if ( this.units === "normalRange" || this.units === "audioRange" ) { return 1; } else { return this._param.maxValue; } } /** * Type guard based on the unit name */ private _is(arg: any, type: UnitName): arg is T { return this.units === type; } /** * Make sure the value is always in the defined range */ private _assertRange(value: number): number { if (isDefined(this.maxValue) && isDefined(this.minValue)) { assertRange( value, this._fromType(this.minValue), this._fromType(this.maxValue) ); } return value; } /** * Convert the given value from the type specified by Param.units * into the destination value (such as Gain or Frequency). */ protected _fromType(val: UnitMap[TypeName]): number { if (this.convert && !this.overridden) { if (this._is